I am trying with (very) limited success to pass an array to a dll I've created. I am working in c# and performing some machine learning operation in c++. The catch here is that I am working with Mono instead of .Net framework (for Unity). I've followed multiple "how-to" links, such as this and this.
The first link actually is very helpful, I can pass an array from c# to c++ with known bounds, without this array being consumed by the garbage collector. However, SAFEARRAY is not supported in Mono (only in .Net)
The relevant part of my c# code is:
[DllImport("Classification.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void DebugStruct([In] IntPtr ptest_struct_with_arrays);
internal struct LabeledDataManaged
{
//[MarshalAs(UnmanagedType.ByValArray,SizeConst = 720)]
public float[] floatArray;
//[MarshalAs(UnmanagedType.ByValArray, SizeConst = 45)]
public uint[] uintArray;
}
static unsafe void TestMarshalMono(float[] testArr, uint[] labelsArr)
{
LabeledDataManaged labledData = new LabeledDataManaged();
labledData.floatArray = testArr;
labledData.uintArray = labelsArr;
int iSizeOfTestStructWithArrays = Marshal.SizeOf(labledData);
IntPtr pStruct = Marshal.AllocHGlobal(iSizeOfTestStructWithArrays);
Marshal.StructureToPtr(labledData, pStruct, false);
DebugStruct(pStruct );
}
My problem: When debugging the dll I see garbage on the c++ side. I've also tried switching from IntPtr to HandleRef but I do not understand what needs to be changed on the c++ side for this to work. By the way, any way of passing an array without it being messed around by the GC is fine, whether it is wrapped within a struct or not.
I'm reaching out to all Stackoverflow geniuses with this irritating problem.
I define the same struct on the c++ side using
std::vector<float>
andstd::vector<unsigned int>
.
Important detail, needs to be in the question. But no, that can never work. C++ language types have no interop story, class implementation details vary entirely too much between compiler implementations. Or for that matter, vary too much in the same compiler. A standard aid like iterator debugging changes the layout of the C++ standard library objects.
Pinvoke interop is based on C language types. Your C# declaration you have now matches
typedef struct LabeledData {
float* floatArray;
unsigned int* uintArray;
}
And you no doubt can see the big, big problem with this structure. There isn't any decent way you can actually use these arrays. You have no idea how big they are. You can use UnmanagedType.ByValArray but then the equivalent C++ declaration needs to look like:
typedef struct LabeledData {
float floatArray[42];
unsigned int uintArray[666];
}
You no doubt see the big, big problem with this structure, the array lengths are fixed. And when you are thinking std::vector<> then you surely don't like fixed array lengths.
Well, that's why .NET likes the COM interop type. SafeArray is safe because it also stores the array length. And has a well-defined way to allocate and destroy them, another big issue with raw arrays. Without them, best you can do is:
typedef struct LabeledData {
size_t floatArraySize;
float* floatArray;
size_t uintArraySize;
unsigned int* uintArray;
}
And match it on the C# side with:
internal struct LabelData {
public int floatArraySize;
public IntPtr floatArray;
public int uintArraySize;
public IntPtr uintArray;
}
You need Marshal.AllocHGlobal() to allocate the arrays, you already know how to do that. And fret about who and when calls Marshal.FreeHGlobal(). Since you probably still want to keep std::vector, you'd copy the arrays so can release memory after the pinvoke call.
The array data copying is very, very ugly of course. Get ahead by not using a structure at all but using four functions. Like CreateLabeledData, DestroyLabeledData, SetLabeledDataFloats, SetLabeledDataIntegers. And a corresponding C++ class with a factory method that implements these functions.