[原]C#与非托管——封送和自动封送

之前说到了如何从C函数声明通过简单的查找替换生成一份C#的静态引用声明(C#与非托管——初体验),因为只是简单说明,所以全部采用的是基础类型匹配和自动封送。自动封送虽然能省去我们不少编码时间,但如果不理解自动封送背后的实际行为,那就如同看魔术师的黑盒子,知其然不知其所以然。而且,自动封送也不是永远有效的万能药,因此这里记录一下封送相关的理解。

非基础类型在C#与Native代码交互时需要进行封送处理,一般的封送处理方式有内存拷贝、固定内存地址,总结如下:

  1. 针对string,可以使用Marshal.StringToHGlobalAnsi、Marshal.StringToHGlobalAuto、Marshal.StringToHGlobalUni把string封送到非托管函数,之后需要调用Marshal.FreeHGlobal释放内存;或者使用Marshal.PtrToStringAnsi、Marshal.PtrToStringAuto、Marshal.PtrToStringUni从非托管函数里面取出string。

  2. 针对byte[](或基础类型的数组),可以使用Marshal.Copy从byte[]拷贝封送到非托管函数或者从非托管函数拷贝封送到byte[]。此外从byte[]封送到非托管函数还可以采取固定内存直接传送byte[]的原始地址(省去了申请内存和拷贝的开销,速度更快),方法是使用GCHandle并指定GCHandleType.Pinned类型。

  3. 针对委托,可以使用Marshal.GetFunctionPointerForDelegate从delegate封送到非托管函数,或者使用Marshal.GetDelegateForFunctionPointer从非托管函数里面取出。和byte[]一样,在封送到非托管函数时需要确保GC不要回收委托(需要保持引用但不需要Pinned)。

  4. 针对结构体,可以在C#中按一样的顺序和对应的数据类型声明一个同样的struct(只能使用基础类型和固定长度的数组),并且标记StructLayout的LayoutKind.Sequential属性,然后使用Marshal.StructureToPtr和Marshal.PtrToStructure进行封送。

  5. C#的对象不能在Native中进行处理,只能保存以待之后再传回给C#调用,保存的方式为C#用GCHandle引用对象,然后把GCHandle转成IntPtr,传给Native作为指针保存。

当C#声明的extern函数返回类型和参数类型中有托管类型时(如string、byte[]、委托等),就会进行自动封送处理,因此可以省写大部分的封送代码。不过有几种情况还是需要手动进行封送的,如:

  1. string的编码是utf8之类的非ansi非utf16编码,则必须手动进行封送同时转换编码。

  2. 委托转换成函数指针的操作比较耗时,如果有频繁的对同一委托进行封送调用,则预存转换后的结果能够很显著的提升性能。