在使用 Unity C# 的开发过程中,基于代码效率或者代码保护等方面的考虑,偶尔需要将一些关键代码以 C++ 动态库的方式实现,这就涉及需要在 C# 与 C++ 之间进行数据传递,而一旦数据传递不慎错误,野指针、堆栈溢出甚至堆栈被破坏的问题就会接踵而来。基础类型变量的传递问题不大,最大的还是结构体体传递的问题。
由于 C++ 动态库开放给 C# 调用的接口只能是 C风格的接口(实际不是,只是C++函数编译名字太难认了。。。),所以所有动态库暴露出来的接口必须使用 extern “C” 修饰,不管其最终实现是 C++ 还是 C,因此其参数也都需要符合C接口的规范(这里面容易犯的问题就是 使用引用类型参数)。实际上,所谓的传递问题就是如何将结构体从C#传递到C库。
C# 传递结构体到 C库的第一步就是先将 结构体转为非托管内存,这时可以使用 Marshal 类进行,如下代码所示:
public static byte[] StructBytes(object objStruct)
{
int length = Marshal.SizeOf(objStruct);
IntPtr ptr = Marshal.AllocHGlobal(length);
Marshal.StructureToPtr(objStruct, ptr, false);
byte[] bytes = new byte[length];
Marshal.Copy(ptr, bytes, 0, length);
Marshal.FreeHGlobal(ptr);
return bytes;
}
public static byte[] StructArrayBytes(T[] structArray)
{
if (null == structArray || structArray.Length == 0)
{
return null;
}
int nStructSize = Marshal.SizeOf(typeof(T));
byte[] bytes = new byte[nStructSize * structArray.Length];
for (int i = 0; i < structArray.Length; i++)
{
StructBytes(structArray[i]).CopyTo(bytes, i * nStructSize);
}
return bytes;
}
public static void InitIntPtrFromStructArray(ref IntPtr ptr, [In] T[] structArray)
{
byte[] bytes = StructArrayBytes(structArray);
if (null == bytes)
{
return;
}
ptr = Marshal.AllocHGlobal(bytes.Length);
Marshal.Copy(bytes, 0, ptr, bytes.Length);
}
public static void InitIntPtrFromIntArray(ref IntPtr ptr, [In] int[] intArray)
{
if (null == intArray || intArray.Length == 0)
{
return;
}
ptr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(int)) * intArray.Length);
Marshal.Copy(intArray, 0, ptr, intArray.Length);
}
除了使用一大串代码进行转化外,C#特性亦为我们提供了直接转换的方法: [MarshalAs(UnmanagedType.LPStruct)],如
public delegate bool CB_OnPolygonCreated(IntPtr model,
int siblingIndex,
[MarshalAs(UnmanagedType.LPUTF8Str)]
string pszBrushName,
[MarshalAs(UnmanagedType.LPUTF8Str)]
string pszItemName,
[MarshalAs(UnmanagedType.LPStruct)] // 这里就不需要将 Vector3 转为 IntPtr 了
Vector3 normal,
uint count,
IntPtr points, // Vector3[]
IntPtr uvs, // Vector2[]
IntPtr uvMode, // byte
IntPtr smoothingMask, // int
IntPtr materialId, // int
IntPtr polyFlags, // uint
IntPtr pszMaterialName, // byte[]
int matNameLength);
根据以上两种方法,可以总结出三种传递方式:
以上三种方法在 windows 和 android 上,当所有 C接口 符合规范时,1、2在两种平台上表现是一致的,也是正确的,但3仅在windows平台上有效,在android平台上会出现segment fault,调试可以发现内存数据没有准确拷贝过去。另外,3情况在windows下也可能造成堆被破坏的问题,所以即使它可以实现将结构体数据传入C库,我们也需要尽量避免。