一、什么是CLR
.NET 提供了一个称为公共语言运行时 (Common Language Runtime, CLR) 的运行时环境,它运行代码并提供使开发过程更轻松的服务。
公共语言运行时的功能通过编译器和工具公开,你可以编写利用此托管执行环境的代码。 使用面向运行时的语言编译器开发的代码称为托管代码。 托管代码具有许多优点,例如:跨语言集成、跨语言异常处理、增强的安全性、版本控制和部署支持、简化的组件交互模型、调试和分析服务等。
简单来讲,就是将C++基础设施构建在.NET框架之上,使用.NET运行时来托管C++。托管C++指的就是C++/CLR,非托管C++指的就是运行在MSCRT而非CLR的C++。
举个例子,有些Basic语言的编译器直接生成二进制代码,没有运行时。但Microsoft Visual Basic可运行在.NET运行时上,直接与.NET对象交互。
所以C++/CLR就是微软将C++构筑于.NET运行时的实践,它提供了极好的C#与C++交互的方式。
二、思路概论
简单来讲,可以分为两种思路。
让C++项目直接运行在CLR上。
构建C++/CLR Wrapper库来操作.NET对象,从纯非托管C++调用Wrapper库。
第一种思路是最简单的,将C++项目进行必要的改写以符合C++/CLR语言规范,直接用/clr编译选项进行编译生成即可。但是仅适用于C++项目能被改写运行在CLR上的情况。
第二种思路稍复杂,但适用于所有情况。基本思路是用C++/CLR编写库,对.NET的对象和方法进行重新包装,并向外部暴露这些兼容纯非托管C++的类和函数。非托管代码操作这些类和函数间接实现与C# .NET的交互。
三、C++/CLR调用.NET语法与奇技淫巧
在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;
仅用函数包装的话,这些足够了。但有时我们想包装.NET对象成一个类,就需要用一些奇迹淫巧了。
类的头文件应该被库和非托管C++项目共用,因为没有这些头文件,编译器无法确定对象的成员信息。这就产生了问题:非托管C++编译器不能识别C++/CLR对象,如何在自定义类中包装.NET对象呢?
具体来讲,我们希望这样做:
#ifdef TESTCLASS_EXPORTS
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT __declspec(dllimport)
#endif
class DLL_EXPORT TestClass {
public:
void TestClass();
void PrintTimeString();
private:
System::DateTime^ time; // .NET对象的句柄
};
为了解决这个问题,我们可以“偷梁换柱”。在32位计算机上,.NET句柄和C++指针都是4字节大小。在64位计算机上,.NET句柄和C++指针都是8字节大小,我们假装time是一个intptr_t好了。
#ifdef TESTCLASS_EXPORTS
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT __declspec(dllimport)
#endif
class DLL_EXPORT TestClass {
public:
void TestClass();
void PrintTimeString();
private:
#ifdef _MANAGED // 这个标志仅在C++/CLR编译时定义了
gcroot
#else
intptr_t time;
#endif
};
这样就没问题了,这种技巧常用于间接法包装,在第五部分会介绍。
四、实现细节:直接法
简单起见,我们使用控制台应用程序作示例。在Visual Studio中创建C++控制台应用程序,修改项目属性->高级,打开公共语言运行时支持。
打开公共语言运行时支持
随后在C/C++ ->命令行中添加/Zc:twoPhase-选项。
添加/Zc:twoPhase-选项
注意,我这里仅仅为x64 Debug添加了选项,如果你是为Release或者x86生成,你也需要对它们进行以上修改。
在项目引用中可以添加要调用的C#动态链接库,就可以直接使用C#的对象了。
五、实现细节:间接法
首先编写Wrapper,我们创建一个空C++库TestClass,启用CLR支持(见第四部分)。
建立这样一个头文件TestClass.h,包装.NET的DateTime对象:
#ifdef TESTCLASS_EXPORTS
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT __declspec(dllimport)
#endif
class DLL_EXPORT TestClass {
public:
void TestClass();
void PrintTimeString();
private:
#ifdef _MANAGED // 这个标志仅在C++/CLR编译时定义了
gcroot
#else
intptr_t time;
#endif
};
在源文件TestClass.cpp中实现这个类:
#include
#include "TestClass.h"
TestClass::TestClass()
{
this->time = System::DateTime::Now;
}
void TestClass::PrintTimeString()
{
System::Console::WriteLine(this->datetime->ToString());
}
编译生成库TestClass.dll或TestClass.lib。
新建C++控制台项目,不要启用CLR支持。
在源文件main.cpp中直接这样写:
#include "../TestClass/TestClass.h"
int main()
{
TestClass tc;
tc.PrintTimeString();
return 0;
}
编译运行应该可以看待控制台输出了当前日期,说明包装调用.NET对象成功。
五、随便一提
C++/CLR将C++与强大的.NET框架结合起来,使得反射、事件机制等现代语言特性可以很好地与C++集成。相较JNI来讲,C++与.NET交互更直观、更高效。



