C#结构体中的数组封送处理

4

Let's say that I have a struct similar to

public struct MyStruct
{
    public float[] a;
}

我想实例化一个包含自定义数组大小的结构体(例如这个例子中是2)。然后,将其编组成一个字节数组。

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();
}

这使得我的byte[]数组只有4个字节,它们看起来像指针值,而不是1.0f或2.0f的值。我已经搜索了一些方法来解决这个问题,但到目前为止,我只能找到类似的例子,在这些例子中,结构体数组大小是预先知道的。难道没有办法解决这个问题吗?

3个回答

8
StructureToPtr只适用于仅包含值类型(int、char、float、其他结构体)的结构体。而float[]是一种引用类型,所以你得到的实际上是一种指针(你不能真正使用它,因为它是一个托管指针)。如果你想将数组复制到固定的内存中,则必须直接使用其中一种Marshal.Copy函数对s.a的浮点数数组进行操作。像这样:

Something like that.(我没有真正测试过)

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

然后

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

更新

我得更正一下。您也可以通过以下方式声明结构体来获得所需的内容:

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

如您所见,您需要在设计时固定浮点数数组的大小。


如果您使用 Buffer.BlockCopy,则无需处理固定缓冲区的问题。 - Ben Voigt
1
不幸的是,我不能使用“SizeConst = N”方法,因为它假定我在编译时知道结构体的大小。然而,我的数组大小在运行时更改。 - Paul Grinberg
我喜欢Marshal.Copy或者Buffer.BlockCopy的想法,但是我担心这可能会破坏尝试使用marshaler的目的。我提供的示例结构体非常简单,在现实中,我的结构体是值类型和引用类型混杂在一起的。似乎唯一的方法是手动迭代结构体中的每个元素,并以适合该元素的方式对其进行序列化。 - Paul Grinberg
这将是使 Marshal.SizeOfMarshal.StructureToPtr 方法在整个结构体上正确工作的唯一方法。 - Fratyx
是的。通常在C#中不应该以这种方式使用结构体。结构体应该非常小,只包含值类型,并且在创建后应该是不可变的(即值仅在构造函数中设置)。如果您以其他方式使用结构体,则经常会遇到难以检测的问题。...但你是对的。由于您的结构体似乎过于复杂,因此应该手动解决您的问题,而不是以自动化方式复制它。 - Fratyx

3

在P/Invoke中,没有直接支持您所期望的场景。我编码的技术类似于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();
            }
        }
    }

我目前已经阻止了使用空数组,但是通过一些额外的处理,您也可以实现。您还可以在结构体构造函数中添加验证以检查array.Length == elementCount等。


1
谢谢你!救了我的一天! 这个应该是最受欢迎的解决方案! - Ladislav

1

Marshal主要用于本地交互(如p/invoke等),本地语言也不允许结构成员在运行时大小变化。(有一种叫做灵活数组成员的技巧只能出现在末尾...你可以通过在结构大小之外分配额外的内存来处理它)。

如果您想将原始数据数组序列化为字节数组,只需使用Buffer.BlockCopy。 原始数据结构的数组更难处理,您可以codegen一个使用cpblk MSIL指令或p/invoke memcpy在msvcrt.dll中或RtlCopyMemory在kernel32.dll中的DynamicMethod。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接