使用数组对结构进行 C# 封送处理

本文关键字:处理 数组 结构 | 更新日期: 2023-09-27 18:33:11

假设我有一个类似于

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 的值。我已经四处寻找方法来完成这项工作,但到目前为止,我所能找到的都是类似的示例,其中结构数组的大小是提前知道的。难道没有办法做到这一点吗?

使用数组对结构进行 C# 封送处理

StructureToPtr仅适用于仅包含值类型的结构(int,char,float,其他结构)。 float[] 是一种引用类型,因此您实际上会得到一种指针(您不能真正使用它,因为它是托管指针)。如果要将数组复制到固定内存,则必须直接在s.a浮点数组上使用Marshal.Copy函数之一。

类似的东西。(我没有真正测试过)

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

如您所见,您必须在设计时修复浮点数组的大小。

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

我目前已经阻止使用空数组,但通过一些额外的处理,您也可以实现这一点。您还可以向结构构造函数添加验证以检查数组。长度 == 元素计数等。

Marshal实际上是用于本机互操作(p/invoke等),并且本机语言也不允许结构成员在运行时大小变化。 (有一个技巧叫做柔性数组成员,它只能在最后出现......您可以仅通过分配超出结构大小的额外内存来处理这个问题)。

如果要将基元数组序列化为字节数组,只需使用 Buffer.BlockCopy 。 原语的结构数组更难,你可以编码一个DynamicMethod,它使用msvcrt中的cpblk MSIL指令或p/invoke memcpy.dll或在kernel32.dll中RtlCopyMemory