Reputation: 2429
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#.
Upvotes: 3
Views: 3902
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
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