eje211
eje211

Reputation: 2429

Can't marshal array of stucts from C++ to C# in Unity

I'm trying to pass an array of structs from C++ to a Unity script in C#. When I am using the code in production, the size of the array will vary greatly, so I effectively need to pass an array of unknown length.

My clever idea was to store the array on the heap and pass a reference to that array to Unity. I've found StackOverflow posts on how to do that. But then Unity complains about a reference being null.

Here's part of my C++ code:

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;
}

I tried this with ints instead of structs and it worked.

Now, my C# code is:

[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);

The Marshal.PtrToStructure instruction says that the SPointers[i] argument is null. I checked with the Debug.Log command and indeed it does seem null: it prints as 0.

But I tried something similar with an array of ints earlier and that worked. What I'm not sure of is: is my problem in the C++ or in the C#? Am I not passing the right information or am I processing the right information in the wrong way?

Solution 1

This was the first solution I came up with mostly on my own. The second solution is better.

Totally thanks to Alex Skalozub, I figured it out. One level of pointer indirection gets abstracted out during marshalling. So now, my C++ code contains:

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;
}

And my C# code contains:

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

And that's it! All the data is transferred.

Now this does leave me with a memory management problem: every single object created in the C++ code will have to be freed. I'm aware of that, and I'm going to take care of it next.

Solution 2

See the checked answer. Alex's answer is much better as it makes much less use of unnecessary pointers. I used more pointers because I'd just figured out how to use them in C#.

Upvotes: 3

Views: 3902

Answers (2)

Captain GouLash
Captain GouLash

Reputation: 1287

Years Later I find this answer and it helps me a little bit. Altough I have something to add, which is easier and better to use. You don't have to worry about freeing the memory in the C++ Part:

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);

One additional thing to mention is: You need two calls with this approach...first get the size of the array from C++ DLL, allocate the array in C# and let the C++ DLL fill the array you allocated in C# with an additional call

Upvotes: 1

Alex Skalozub
Alex Skalozub

Reputation: 2576

Your ptrNativeData is a pointer to array of structures themselves, not to array of pointers to structures. Copying it to array of pointers and accessing them is incorrect.

I'd also suggest to use out instead of ref when declaring an interop function. This is more accurate and doesn't require marshaller to copy initial values from managed to native code (as they're initialized in native code):

[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
}

Upvotes: 3

Related Questions