I'm trying to pass an array of struct
s 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 int
s instead of struct
s 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 int
s 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?
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.
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#.
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
}