Unity c# 与 c++动态库间的结构体传递问题

问题

在使用 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);

根据以上两种方法,可以总结出三种传递方式:

  1. C接口使用结构体指针参数, C#接口结构体参数使用 IntPtr,传递时直接使用 Marshal 创建结构体对象对应的IntPtr
  2. C接口使用结构体指针参数, C#接口结构体参数使用 [MarshalAs(UnmanagedType.LPStruct)]修饰,这样C#会自动将我们的托管内存中的结构体对象转成结构体指针(此处必须保证结构体本身的成员变量都是可自动转为非托管内存的
  3. C接口使用结构体形参,C#接口结构体参数也是直接使用结构体类型

以上三种方法在 windows 和 android 上,当所有 C接口 符合规范时,1、2在两种平台上表现是一致的,也是正确的,但3仅在windows平台上有效,在android平台上会出现segment fault,调试可以发现内存数据没有准确拷贝过去。另外,3情况在windows下也可能造成堆被破坏的问题,所以即使它可以实现将结构体数据传入C库,我们也需要尽量避免

你可能感兴趣的:(Unity3D)