There's no straight-forward support in P/Invoke to enable your expected scenario. What I've coded is a technique similar to the one used for storing the Variable length array in a struct in C.
[StructLayout(LayoutKind.Sequential)]
public struct VarLenStruct
{
public int elementCount;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
public float[] array;
public static GCHandle GetUnmanagedStruct(VarLenStruct managedStruct)
{
if (managedStruct.elementCount < 1)
throw new ArgumentOutOfRangeException("The array size must be non-zero");
// Array is a contiguous data structure, so assign the memory appended to the struct for the elements other than the first
int managedBufferSize = Marshal.SizeOf(managedStruct) + Marshal.SizeOf(typeof(float)) * (managedStruct.elementCount - 1);
byte[] managedBuffer = new byte[managedBufferSize];
var handle = GCHandle.Alloc(managedBuffer, GCHandleType.Pinned);
try
{
IntPtr unmgdStructPtr = handle.AddrOfPinnedObject();
Marshal.StructureToPtr(managedStruct, unmgdStructPtr, fDeleteOld: false);
IntPtr unmgdArrAddr = unmgdStructPtr + Marshal.OffsetOf(typeof(VarLenStruct), "array").ToInt32();
Marshal.Copy(source: managedStruct.array, startIndex: 0, destination: unmgdArrAddr, length: managedStruct.elementCount);
}
catch
{
// The handle must be freed in case of any error, since it won't be visible outside this method in this case
handle.Free();
throw;
}
return handle; // Make sure to free this handle at the end of usage!
}
public static VarLenStruct GetManagedStruct(IntPtr unmanagedStructPtr)
{
VarLenStruct resultStruct = (VarLenStruct)Marshal.PtrToStructure(unmanagedStructPtr, typeof(VarLenStruct));
if (resultStruct.elementCount < 1)
throw new NotSupportedException("The array size must be non-zero");
Array.Resize(ref resultStruct.array, newSize: resultStruct.elementCount); // Since the above unmarshalling always gives us an array of Size 1
IntPtr unmgdArrAddr = unmanagedStructPtr + Marshal.OffsetOf(typeof(VarLenStruct), "array").ToInt32();
Marshal.Copy(source: unmgdArrAddr, destination: resultStruct.array, startIndex: 0, length: resultStruct.elementCount);
return resultStruct;
}
}
public static void TestVarLengthArr()
{
VarLenStruct[] structsToTest = new VarLenStruct[]{
new VarLenStruct() { elementCount = 1, array = new float[] { 1.0F } },
new VarLenStruct() { elementCount = 2, array = new float[] { 3.5F, 6.9F } },
new VarLenStruct() { elementCount = 5, array = new float[] { 1.0F, 2.1F, 3.5F, 6.9F, 9.8F } }
};
foreach (var currStruct in structsToTest)
{
var unmgdStructHandle = VarLenStruct.GetUnmanagedStruct(currStruct);
try
{
var ret = VarLenStruct.GetManagedStruct(unmgdStructHandle.AddrOfPinnedObject());
if (!ret.array.SequenceEqual(currStruct.array))
throw new Exception("Code fail!");
}
finally
{
unmgdStructHandle.Free();
}
}
}
I've currently blocked the use of empty array, but with some additional handling you can achieve that too. You can also add validations to your struct constructor in order to check array.Length == elementCount, etc.