无法在 Unity 中将 C++ 的 stuct 数组封送到 C#

本文关键字:数组 stuct Unity 中将 C++ | 更新日期: 2023-09-27 18:33:42

我正在尝试将一个struct数组从 C++ 传递到 C# 中的 Unity 脚本。当我在生产中使用代码时,数组的大小会有很大差异,因此我实际上需要传递一个未知长度的数组。

我的聪明想法是将数组存储在堆上,并将对该数组的引用传递给 Unity。我找到了关于如何做到这一点的StackOverflow帖子。但随后 Unity 抱怨引用为空。

这是我C++代码的一部分:

extern "C" struct S; // Not currently used.
struct S {
  int i;
  float f;
};
extern "C" bool helloWorld(S ** a, int * i);
S * s;
bool helloWorld(S ** a, int * i) {
  s = new S[4];
  for (int j = 0; j < 4; j++) {
    s[j].i = j;           // Fill in placeholder
    s[j].f = (float) j;   // data for now.
  }
  *a = s; // I don't know if this works.
  *i = 4; // Works.
  return true;
}

我用 int s 而不是 struct s 尝试了这个,它奏效了。

现在,我的 C# 代码是:

[StructLayout(LayoutKind.Sequential),Serializable]
public struct S {
  int i;
  float f;
};
void Start() {
  IntPtr ptrNativeData = IntPtr.Zero;
  int itemsLength = 0;
  bool success = helloWorld(ref ptrNativeData, ref itemsLength);
  if (!success) {
    return;
  }
  S[] SArray = new S[itemsLength];  // Where the final data will be stored.
  IntPtr[] SPointers = new IntPtr[itemsLength];
  Debug.Log("Length: " + itemsLength); // Works!
  Marshal.Copy(ptrNativeData, SPointers, 0, itemsLength); // Seems not to work.
  for (int i = 0; i < itemsLength; i++) {
    Debug.Log("Pointer: " + SPointers[i]); // SPointers[0] prints 0.
    Marshal.PtrToStructure(SPointers[i], SArray[i]); // Crashes here. Boom.
  }
}
[DllImport("TestUnity")]
private static extern bool helloWorld(ref IntPtr ptrResultVerts,
    ref int resultVertLength);

Marshal.PtrToStructure指令说 SPointers[i] 参数为空。我用 Debug.Log 命令进行了检查,它确实看起来为空:它打印为 0。

但是我之前尝试过一系列int类似的东西,并且奏效了。我不确定的是:我的问题出在C++还是 C# 中?是我没有传递正确的信息,还是以错误的方式处理了正确的信息?

解决方案 1

这是我主要靠自己想出的第一个解决方案。第二种解决方案更好。

完全感谢Alex Skalozub,我想通了。在封送期间,指针间接寻址的一个级别被抽象出来。所以现在,我的C++代码包含:

S ** s; // Not a pointer to Ss, but a pointer to pointers to Ss.
bool helloWorld(S *** a, int * i) { // Pass a triple pointer.
  s = new S * [4]; // Create array of pointers.
  for (int j = 0; j < 4; j++) {
    s[j] = new S; // Actually create each object.
    s[j]->i = j;
    s[j]->f = (float) j;
  }
  *a = s;
  *i = 4;
  return true;
}

我的 C# 代码包含:

for (int i = 0; i < itemsLength; i++) {
  Debug.Log("Pointer: " + SPointers[i]);
  SArray[i] = (S) Marshal.PtrToStructure(SPointers[i], typeof(S));
}

就是这样!传输所有数据。

现在这确实给我留下了一个内存管理问题:在C++代码中创建的每个对象都必须被释放。我知道这一点,接下来我会处理它。

解决方案 2

请参阅选中的答案。亚历克斯的答案要好得多,因为它减少了不必要的指针的使用。我使用了更多的指针,因为我刚刚弄清楚如何在 C# 中使用它们。

无法在 Unity 中将 C++ 的 stuct 数组封送到 C#

您的ptrNativeData是指向结构数组本身的指针,而不是指向结构的指针数组。将其复制到指针数组并访问它们是不正确的。

我还建议在声明互操作函数时使用out而不是ref。这更准确,并且不需要编组程序将初始值从托管代码复制到本机代码(因为它们在本机代码中初始化(:

[DllImport("TestUnity")]
private static extern bool helloWorld(out IntPtr ptrResultVerts, out int resultVertLength);
void Start() {
    IntPtr ptrNativeData;
    int itemsLength;
    bool success = helloWorld(out ptrNativeData, out itemsLength);
    if (!success) {
        return;
    }
    S[] SArray = new S[itemsLength];  // Where the final data will be stored.
    Debug.Log("Length: " + itemsLength);
    IntPtr p = ptrNativeData;
    for (int i = 0; i < itemsLength; i++) {
        Marshal.PtrToStructure(p, SArray[i]);
        p += Marshal.SizeOf(typeof(S)); // move to next structure
    }
    // todo: free ptrNativeData later
}

多年后,我找到了这个答案,它对我有所帮助。好吧,我有一些东西要补充,它更容易使用。您不必担心释放C++部分中的内存:

C++

#pragma pack(push, 1)
struct Joint
{
    int id = 0;
    float posX = 0.0f;
    float posY = 0.0f;
    float posZ = 0.0f;
};
#pragma pack(pop)
extern "C" void __declspec(dllexport) __stdcall GetJointArray(Joint* newJoints, int* length)
{
    Joint theNewJoints[2];
    Joint theJoint;
    theJoint.id = 20;
    theJoint.posX = 21;
    theJoint.posY = 22;
    theJoint.posZ = 23;
    theNewJoints[0] = theJoint;
    theNewJoints[1] = theJoint;
    memcpy(newJoints, &theNewJoints, 2 * sizeof (theJoint));
    *length = 2;
}

C#

[StructLayout(LayoutKind.Sequential, Pack = 0)]
internal unsafe struct Joint
{
    [MarshalAs(UnmanagedType.I4)]
    public int id;
    [MarshalAs(UnmanagedType.R4)]
    public float posX;
    [MarshalAs(UnmanagedType.R4)]
    public float posY;
    [MarshalAs(UnmanagedType.R4)]
    public float posZ;
}
[DllImport("TheDLL")]
static extern void GetJointArray(Joint[] jointArray, out int length);
int itemsLength = 0;
Joint[] result = new Joint[2]; // Of course you need an additional dll function call to get the size of the array before
GetJointArray( result, out itemsLength);
Debug.Log("ITEMS LENGTH:" + itemsLength);
Debug.Log("JOINT1 ID:" + result[0].id);

还有一件事要提一下:使用这种方法需要两次调用......首先从 DLL 中获取数组的大小C++,在 C# 中分配数组,然后让C++ DLL 使用额外的调用填充您在 C# 中分配的数组