C++/CLI托管C++常用的5种场景 (CLR、托管C++、C++/CLI三者关系)
官网:C++/CLI 任务 | Microsoft Learn
转向.NET后,手头上往往仍有旧的模块要重用。也许这些模块是Delphi写的,也许是C/C++写的,或者是其它编程语言……为了能把它们移植到.NET下,或者是在.NET中调用,To be or not to be, that is a question。
在这里,我笔记了几个在工作中遇到的几个场景。不过,这里不包括完全使用C#来重写原来用C++编写的程序这种变态的需求。当你被要求做这种事的时候,请三思而后行 …… 这简直是种非人的折磨。
为了写好程序,必须理清 原生C++、CLR、.NET、托管C++、C++/CLI之间关系。
一、CLR、托管C++、C++/CLI,三者关系
原生C++(Native C++)
标准/非托管/原生C++, 非托管代码典型的就是Native C++代码。它不受CLR管理,内存管理和垃圾回收需要手动操作。
.NET / CLR / 托管
.NET Framework是微软(Microsoft)开发的软件开发平台,主要包含两个部分: 公共语言运行时(Common Language Runtime,CLR)和.NET Framework类库。
.NET Framework有两大主要特征:跨平台、跨语言。
.NET 框架广义上(此处并不指“.NET Framework”)是支持跨平台(不同OS,如Linux、Mac)的,目前已有跨平台的开源项目Mono。.NET Framework的另外一个特性是跨语言。公共语言规范(Common Language Specification,CLS)使得C++、C#、VB.NET、F#等等语言都可以作为.NETFramework的开发语言。
那么什么是"通用语言运行时CLR"?通用语言运行时是.NET 框架应用程序的执行引挚。它提供了许多服务,其中包括:代码管理(装入和执行)、类型安全性验证、元数据(高级类型信息)访问、为管理对象管理内存、管理代码,COM对象和预生成的DLLs(非管理代码和数据)的交互操作性、对开发人员服务的支持等等。微软的.NET的基础CLR(Common Language Runtime,通用语言运行时)就是CLI的一个实例。
CLR常用简写词语,CLR是公共语言运行时(环境),Common Language Runtime)和Java虚拟机一样也是一个运行时环境,它负责资源管理(内存分配和垃圾收集),并保证应用和底层操作系统之间必要的分离。
最后搞清楚什么是"托管"(Managed)。托管是.NET的一个专门概念,它是融于通用语言运行时(CLR)中的一种新的编程理念,因此我们完全可以把"托管"视为".NET"。
托管代码(managed code),就是交给公共语言运行时(CLR)所管理的代码。CLR实现了内存管理和垃圾回收等机制,因此托管代码一般来说被认为是安全的,而非托管代码是非安全的(unsafe)。
一般情况下用C#、VB.NET、F#这些.NET语言写的代码都是托管代码,因为这些语言的代码都由CLR管理,先编译成微软的中间语言(MSIL),程序运行时再由CLR即时编译(Just-in-time compilation,JIT)生成机器指令。【注意:C#用unsafe关键字也可以写非托管代码】
托管代码与非托管代码两者区别:
1、托管代码是一种中间语言,运行在CLR上;
非托管代码被编译为机器码,运行在机器上。
2、托管代码独立于平台和语言,能更好的实现不同语言平台之间的兼容;
非托管代码依赖于平台和语言。
3、托管代码可享受CLR提供的服务(如安全检测、垃圾回收等),不需要自己完成这些操作;
非托管代码需要自己提供安全检测、垃圾回收等操作。
托管C++(Managed C++)
目前只有托管C++及其后继者C++/CLI可以做到无缝整合托管和非托管代码,而在托管代码中调用COM的速度又相当慢,所以经常被用于其他语言和非托管代码之间的桥梁。
托管C++并非独立存在的编程语言,而仅仅是微软对C++的一个语法扩展,允许C++程序员在.NET框架和CLR的基础上进行托管编程。 是旧有的东西,现在已经被废弃,C++托管代码的现代扩展叫做C++/CLI。
与C#和Visual Basic .NET相比,其主要优点是旧代码可以比较快地移植到新的平台上,而且即使不完全重写代码,也可以通过互操作在同一个模块中无缝整合托管和非托管代码。
标准C+++与托管C+的主要区别 尽管托管C++是从标准C++建立而来的,但它与标准C++有着本质上的区别,这主要体现在以下几个方面:
(1) 广泛采用"名称空间"(namespace)
(2) 基本数据类型的变化
(3) 新增三个托管C++类型:__gc class、__value class和__gc interface
C++/CLI
C++指的是Bjarne Stroustrup在BELL实验室发明的 ISO C++语言,它实现了运行时取得速度和尺寸最佳化的静态对象模型,然而它除了堆分配外不支持程序的动态修改,它准许无限地接近底层设备,但在程序运行过程中几乎无法操作活动类型,也无法操作与程序相关联的底层结构。
Herb Sutter,C++/CLI的主要构造者之一,称C++是一门“混凝土”式的语言。
CLI:全称(Common Language Infrastructure,通用语言框架),一种支持动态组件编程模型的多重结构,在许多情况下,这代表了一个与C++对象模型完全颠倒了的模式。一个时实的软件层,有效地执行系统,在底层操作系统与程序之间运行。操作底层的设备受到一定的限制,操作执行程序中的活动类型及与程序相关联的下部结构得到了支持。微软的.NET的基础CLR(Common Language Runtime,通用语言运行时)就是CLI的一个实例。
C++/CLI:C++/CLI与标准 ISO C++的语法有不同之处。利用它可以编写托管的C++代码,从而成为非托管C++和托管代码的桥梁。
是静态C++对象模型到CLI的动态组件对象编程模型的捆绑,或者说C++/CLI是ISOC++在.NET的扩展,是ISOC++和.NET的无缝结合。简而言之,它就是你如何用C++在.NET中编程,而不是C#或Visual Basic .net。
C++/CLI取代托管C++原因:
C++/CLI 代表托管和本地编程的结合, 混合了Native和Managed两种环境。斜杠(/)代表C++和CLI的捆绑。 托管C++极其难以使用,而C++/CLI虽然仍不完美,但是远比托管C++好用。
使用C++/CLI(取代了托管C++) 的唯一原因是,为C#/.NET 和 原生C++代码提供二者之间的桥梁,除此之外,任何地方都用不到C++/CLI。
C++/CLI是C++的.Net实现,Microsoft为了使C++开发人员能更容易掌握managed framework而设计了C++/CLI,然而如今很少使用,因此要找到好的C++/CLI教程也很难。
Refer:使用 C++/CLI 进行 .NET 编程 | Microsoft Learn
二、通过C++/CLI 托管Managed C++,实现C#与Native C++交互实例
2.1 非托管Native C++(动态链接库DLL)
//cpp.h using CppCallBack = void (__stdcall*)( ); class __declspec(dllexport) NativeClass { public: int CppRegistCall(CppCallBack pFun); //int GetCppData(unsigned char ch[], int len); //void execute(); private: CppCallBack cppCall; }
//Cpp.cpp #include "Cpp.h"
2.2 托管Managed C++(C++/CLI)(CLR .NET)
//Cli.h
#pragma once
#pragma managed
namespace CliDll {
public delegate void CliCallBack(); //在.NET中, 通过”委托delegate“, 声明类似与非托管的函数指针类型,
public ref class CliClass //在C++/CLR中,托管函数指针转化为非托管函数指针,使用Marshal::GetFunctionPointerForDelegate(^pFun).ToPointer()
{ public: int CliRegistCall(CliCallBack ^ pFun); //在C++/CLR中,有两种使用C#对象的手段:句柄 (^) 和追踪引用 (%) ;^类似与C++的指针符,
//int GetCliData(array<unsigned char> ^ ch, int len); //void execute(); }; private: CliCallBack cliCall; }
//Cli.cpp #include "Cli.h" #include "Cpp/Cpp.h"
C#中并没有函数指针的概念,但是可以使用委托(delegate)来传递函数指针。
大家可能会问,为什么要传递函数指针呢?利用 平台调用PInvoke 可以实现C#对C/C++函数的调用,反过来,我们能不能在C/C++程序运行的某一时刻,来调用一个C#对应的函数呢?
(例如在C++中存在一个独立线程,该线程可能在任意时刻触发一个事件,并且需要通知C#)。这个时候,我们就有必要将一个C#中已经指向某一个函数的函数指针(委托)传递给C++.
此外还包括:传递结构体、结构体指针、结构体数组等
编码请参考:1. C#与C++互操作_c++与c#交互-CSDN博客 https://blog.csdn.net/hello_crayon/article/details/80470937
2. C#与C/C++的交互(PInvoke) - 何人之名 - 博客园 (cnblogs.com) https://www.cnblogs.com/herenzhiming/articles/6688804.html
数据类型参考:https://blog.csdn.net/qq_39008744/article/details/105735770
在C++/CLR中,有两种使用C#对象的手段:句柄 (^) 和追踪引用 (%)
1. 句柄使用^符号表示,对应C++中的指针。
使用GCnew可以创建.NET对象,这样的对象不用delete,.NET GC会处理内存。
System::Object ^x = gcnew System::Object();
这样的语句将在.NET运行时内部建立一个托管对象,并可通过句柄x访问,语法是x->item
2. 追踪引用使用%符号表示,对应C++中的引用。
R^ hr = gcnew R; R% rr = *hr;
2.3 托管C#(.NET Framework)
//APP.cs using CliDll; namespace CSharp { public partial class CsClass { static CliClass cliObj = new CliClass(); public static void CsCallBack() { //byte[] data = new byte[26]; //int mark = CsClass.cliObj.GetCliData(data, 26); //for(int i = 0; i < 26; i++) //{ // Console.WriteLine(data[i]); //} } static void Main(string[] args) { CliCallBack cliCall = new CliCallBack(CsCallBack); CsClass.cliObj.CliRegistCall(cliCall); //CsClass.cliObj.execute(); //Console.ReadLine(); } } }
三、托管C++常用的5种场景
场景一:在.NET中静态调用WindowsAPI或DLL
Refer: https://www.cnblogs.com/bobbychencj/p/1756501.html
这是比较普遍的需求。一般来说,简单的函数调用,大可直接用 C# / VB.NET,经过 DllImport 属性包装出函数来调用。如:
using System.Runtime.InteropServices;
[DllImport("KERNEL32.DLL", EntryPoint="MoveFileW", SetLastError=true,CharSet=CharSet.Unicode, ExactSpelling=true, CallingConvention=CallingConvention.StdCall)] public static extern bool MoveFile(String src, String dst);
由于WindowsAPI用到的人实在是多,因此有一个专门的wiki站点,收集这方面的资料:http://www.pinvoke.net/ , 对于常用的函数甚至有完整的应用例子和帮助。当然,如果你有相应的资料和例子,你也可以贡献你的力量,给其它人帮助。
场景二:用托管C++包装现有的DLL,供C#调用
当函数的参数或返回值比较复杂,或函数比较多的时候,这种方法对与人来说,实在是一个折磨。常常这些接口和定义就要用掉几千行的代码,而且还不能保证是正确的。
这些错误往往在运行时才能显现出来,甚至有些错误会引起内存泄漏,或其它更为隐蔽的错误。在这种情况下,使用C++/Managed代码来包装,就成了最合理的选择。
因为托管C++代码可以直接引用原有的头文件,直接调用非托管函数,而不需要声明。这样,既减少了工作量,又避免引入错误。缺点是,这种方法会增加一个DLL。
要注意的是托管字符串和非托管字符串是有区别的,并需要转换(特别要注意的Unicode字符串和多字节字符串的转换)。
仍以MoveFile为例吧,这样比较简单:
#include <windows.h> #include <vcclr.h> using namespace System; namespace wrapper { public ref class ApiWrapper { public: bool static MoveFile(String ^ lpExistingFileName, String ^ lpNewFileName ) { pin_ptr<const wchar_t> src = PtrToStringChars(lpExistingFileName); pin_ptr<const wchar_t> dst = PtrToStringChars(lpNewFileName); return ::MoveFile(src, dst); } }; }
然后在C#中,引用上面代码生成的DLL文件,就可以直接调用了:
wrapper.ApiWrapper.MoveFile( @"c:\debug.log" , @"c:\debug.txt" );
假如原有的代码是基于COM的,那么太好了,VisualStudio等IDE会自动生成一个用于包装的dll,供你调用。当然因特殊需要而手工编码的是另一回事。
场景三:现有C++原代码,包装后供C#调用
C++的源代码,实际上可以直接编译成托管代码。MFC也好ATL也好……这样看起来在.NET中最强大的编程语言就是C++了:它不仅可以编写托管程序,甚至可以将标准C++的代码也编译成托管程序!
其实VC++最强大的地方不止如此,它还在于能够编写混合了托管和非托管的代码的程序!!!这样最大的好处不仅可以将关键代码直接编译成非托管的代码,还可以避免被反编译。
假设现有C++代码如下:
class UnmanagedClass { public: LPCWSTR GetPropertyA() { return L"Hello!"; } void MethodB( LPCWSTR ) {} };
我们只要再增加一个包装类到工程文件中:
namespace wrapper { public ref class ManagedClass { public: // Allocate the native object on the C++ Heap via a constructor ManagedClass() : m_Impl( new UnmanagedClass ) {} // Deallocate the native object on a destructor ~ManagedClass() { delete m_Impl; } protected: // Deallocate the native object on the finalizer just in case no destructor is called !ManagedClass() { delete m_Impl; } public: property String ^ get_PropertyA { String ^ get() { return gcnew String( m_Impl->GetPropertyA()); } } void MethodB( String ^ theString ) { pin_ptr<const WCHAR> str = PtrToStringChars(theString); m_Impl->MethodB(str); } private: UnmanagedClass * m_Impl; }; }
然后,改变编译选项为“使用公共语言扩展 /clr”就可以了。这样,我们把代码编译成DLL文件就可以供.NET其它语言调用了。
最后,C#中可以象如下的代码一样调用C++类了:
ManagedClass mc = new ManagedClass(); mc.MethoB("Hello"); string s = mc.get_PropertyA;
场景四:如何在托管C++代码中混合托管和非托管代码
很简单,只要从#pragma unmanaged编译指示开始的程序,一率编译成非托管代码;要想恢复成托管代码,只要使用#pragma managed就可以了。如:
#pragma unmanaged #include <iostream> using namespace std; template<typename T> void f(T t){ cout << t << endl; } #pragma managed using namespace System; void m(String ^ s){ Console::WriteLine(s); } void main(){ f("Hello"); m("World"); }
生成exe文件后,用反编译程序查看 f 函数:
[PreserveSig, MethodImpl(MethodImplOptions.Unmanaged, MethodCodeType=MethodCodeType.Native), SuppressUnmanagedCodeSecurity] public static unsafe void modopt(CallConvCdecl) f<char const *>(sbyte modopt(IsSignUnspecifiedByte) modopt(IsConst)*);
看不到源码,而方法属性标记为Unmanaged。
如果没有加上#pragma unmanaged,反编译得到的 f 函数为:
internal static unsafe void modopt(CallConvCdecl) f<char const *>(sbyte modopt(IsSignUnspecifiedByte) modopt(IsConst)* t) { std.basic_ostream<char,std::char_traits<char> >.<<(std.operator<<<struct std::char_traits<char> >(*((basic_ostream<char,std::char_traits<char> >* modopt(IsImplicitlyDereferenced)*) &__imp_std.cout), t), (basic_ostream<char,std::char_traits<char> >* modopt(IsImplicitlyDereferenced) modopt(CallConvCdecl) *(basic_ostream<char,std::char_traits<char> >* modopt(IsImplicitlyDereferenced))) __unep@?endl@std@@$$FYAAAV?$basic_ostream@DU?$char_traits@D@std@@@1@AAV21@@Z); }
其中的函数内容一目了然。如果你的函数没有调用operator等不好理解的类库,那么反编译出来的代码简直和源码没差别。
开心一刻:我只会C++不懂.NET不懂C#,怎么编写.NET程序?
很简单,你照样用你的C++写你的程序,然后测试没有错误后,将编译选项改为/clr,好了,Rebuild,你的程序现在是.NET了。
恶搞:“我想问一下,在能将现有的C++代码直接进行封装,被C#进行调用,而不是去调用DLL,也就是不生成DLL,就在C#下能直接调用VC的工程源文件不?”
我想,提问的人是不是指,现有c++源码,但不想费劲去转换成C#源码,但又想能与C#一起编译。
于是我就给了一个极其变态的方法,不过,个人是不建议使用这种变态的方法啊。方法如下:
1 先将C++源码,改用CLR编译选项,编译成.NET的Assembly(DLL文件)。
2 然后用reflector等反编译软件,反编译成C#代码,并导出(reflector有专门的导出插件)。
3 将导出的C#代码,添加上新写的C#代码一起编译。
这种方法生成的代码很是恐怖,强烈建议不要把C++源码就这么丢了,否则后果自负。
场景五:不想要DLL,能不能直接把C++源代码与C#源代码一起编译成一个单独的Assembly呢?
当然是可以的。具体参见:让C++代码与C#代码一起生成一个单一的Assembly - 沐枫 - 博客园 (cnblogs.com)
Refer:您也使用托管C++吗? - 沐枫 - 博客园 (cnblogs.com)
C++/CLI入门系列 第三篇:C#通过C++/CLI,实现C++的回调函数_c# c++ 回调函数-CSDN博客