在前面一篇博文里介绍了用C#封装C++ SDK时需要避免的情况,那么这篇将简要介绍下如果不可避免的碰到前述情况时,该如何解决。
首先对于#2, 也就是某个字符串的指针被C++函数保留其值(而非其指向的内容),并作在随后非此函数内用到。由于一般C# String是通过marshaling机制转换为C++的字符串指针的(例如const char*),那么此C++函数得到的字符串指针就是由marshaller分配的某临时空间的指针,在此P/Invoke函数完成后就可能被收回,所以如果C++函数保存其指针的值并后续引用之指向的字符串数据必然会导致访问异常,轻则得到的是乱码重则crash应用程序。那么解决方法其实很简单,通过GCHandler来把此对象pin住并将其native指针传递给C++函数。比如:
_gcHandle = GCHandle.Alloc(myString);
IntPtr nativeStrPtr = GCHandle.ToIntPtr(_gcHandle);
// Pass the native pointer to C++ function
myPInvokeCall(nativeStrPtr);
但这样会导致内存泄漏,但也没什么更好的解决办法了:(
其次对于#3,也就是需要用一个C# delegate去实现一个C++的以by-value方式返回类对象的回调函数。比如在C++ SDK里有这样的API:
class MyClass
{
...
}
typedef MyClass (*PFN_Callback)();
void SetCallback(PFN_Callback pCB);
注意到此回调函数要求以by-value方式返回一个MyClass对象(前提当然是MyClass实现了拷贝构造)。那么如何用C#来实现一个等价的函数呢?开始我认为这似乎不可能,因为C#完全不知道C++对象的memory layout是如何,但后来想想还是有办法的:
注意到C#的structure是完全可以marshal成一个C++的structure的,并且还可以指定成员变量的memory offset。注意到这里structure的marshaling是通过by-value方式为成员变量赋值的,那么对于类,其实缺少的机制就是给一块sizeof(MyClass)大小的内存,让你构造它的问题。这不就是可以通过placement new来实现么?所以对于我的情况,就需要通过一个P/Invoke函数来专门构造此对象,而此P/Invoke的函数的实现就是通过placement new来在一块由marshaler分配的内存上构造C++对象。值得指出的是,C# delegate函数的原型必须返回结构,且此结构的内存大小必须和C++回调函数的返回类内存大小完全一致。否则delegate被JIT出来后的函数调用必然导致stack异常。
这个方法在MS CLR上可以完美实现,可如果要碰Mono的话,还是别想了。原因是Mono JIT出的函数在ABI上和当前系统(比如Linux平台)有严重不一致,就算你是按照structure marshaling的标准做法去用delegate实现一个native callback,也会碰到奇葩的问题,而且极难调式。所以对于Mono,最好完全避免delegate到native function pointer的转换为妙。