简体   繁体   中英

C# marshaling of a struct with an array

Let's say that I have a struct similar to

public struct MyStruct
{
    public float[] a;
}

and that I want to instantiate one such struct with some custom array size (let's say 2 for this example). Then I marshal it into a byte array.

MyStruct s = new MyStruct();
s.a = new float[2];
s.a[0] = 1.0f;
s.a[1] = 2.0f;

byte[] buffer = new byte[Marshal.SizeOf(typeof(MyStruct))];
GCHandle gcHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned);

try
{
    Marshal.StructureToPtr(s, gcHandle.AddrOfPinnedObject(), false);

    for (int i = 0; i < buffer.Length; i++)
    {
        System.Console.WriteLine(buffer[i].ToString("x2"));
    }
}
finally
{
    gcHandle.Free();
}

This gives me only 4 bytes in my byte[] and they look like a pointer value rather than the value of either 1.0f or 2.0f. I've searched around for ways to make this work, but all I've been able to find so far are similar examples where the struct array size is known ahead of time. Isn't there a way to do this?

The StructureToPtr does only work with structures which contains value types only (int, char, float, other structs). float[] is a reference type and so you really get a kind of pointer (you can't really use it because it is a managed pointer). If you want to copy your array to a pinned memory you have to use one of the Marshal.Copy functions directly on your sa float array.

Something like that. (I didn't really test it)

byte[] buffer = new byte[sizeof(float) * s.a.Length];
GCHandle gcHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned);

and then

Marshal.Copy(s.a, 0, gcHandle.AddrOfPinnedObject(), s.a.Length);

Update

I have to correct myself. You can get also get what you want by declaring your struct in this way:

 [StructLayout(LayoutKind.Sequential)]
 public struct MyStruct
 {
     [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
     public float[] a;
 }

As you see you have to fix the size of the float array at design time.

Marshal is really for native interop (p/invoke and so on), and native languages don't allow structure members to vary in size at runtime either. (There is a trick called a flexible array member which can appear only at the end... you can deal with that merely by allocating extra memory beyond the structure size).

If you want to serialize an array of primitives to a byte array, just use Buffer.BlockCopy . Arrays of structures of primitives are harder, you can codegen a DynamicMethod which uses the cpblk MSIL instruction or p/invoke memcpy in msvcrt.dll or RtlCopyMemory in kernel32.dll.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM