C#中使用联合体时出现奇怪的反序列化行为

3
我想将类似C语言的union导出为字节数组,像这样:
[StructLayout(LayoutKind.Explicit)]
struct my_struct
{
    [FieldOffset(0)]
    public UInt32 my_uint;

    [FieldOffset(0)]
    public bool other_field;
}

public static void Main()
{
    var test = new my_struct { my_uint = 0xDEADBEEF };
    byte[] data = new byte[Marshal.SizeOf(test)];

    IntPtr buffer = Marshal.AllocHGlobal(data.Length);
    Marshal.StructureToPtr(test, buffer, false);
    Marshal.Copy(buffer, data, 0, data.Length);
    Marshal.FreeHGlobal(buffer);

    foreach (byte b in data)
    {
        Console.Write("{0:X2} ", b);
    }
    Console.WriteLine();
}

我们得到的输出是 01 00 00 00,而不是预期的 EF BE AD DE。(https://dotnetfiddle.net/gb1wRf)
现在,如果我们将 other_field 类型更改为 byte(例如)会发生什么?
奇怪的是,我们第一次想要的输出结果是 EF BE AD DE。(https://dotnetfiddle.net/DnXyMP
此外,如果我们交换原来的两个字段,我们再次得到了我们想要的相同的输出结果。(https://dotnetfiddle.net/ziSQ5W
这是为什么?为什么字段的顺序很重要?有没有更好(可靠)的解决方案可以做同样的事情?
2个回答

5
这是结构体序列化方式不可避免的副作用。起点是结构体值不是可平坦化的,这是包含布尔类型(bool)的副作用。在托管结构中,布尔类型占用1个字节的存储空间,但在序列化结构中(UnmanagedType.Bool),它占用4个字节。
因此,结构体值不能一次性复制,编组程序需要转换每个单独的成员。所以首先处理my_uint,产生4个字节。接下来处理other_field,在完全相同的地址处也产生4个字节。这会覆盖my_uint产生的所有内容。
布尔类型总体上是一个奇怪的类型,它从不生成可平坦化的结构体。即使应用[MarshalAs(UnmanagedType.U1)]也是如此。这本身对您的测试有一个有趣的影响,现在您将看到my_int产生的3个高位字节被保留。但结果仍然是垃圾,因为成员仍然逐个转换,现在在偏移量0处产生了一个字节值0x01。
您可以通过将其声明为字节来轻松获得所需的内容,现在结构体是可平坦化的。
    [StructLayout(LayoutKind.Explicit)]
    struct my_struct {
        [FieldOffset(0)]
        public UInt32 my_uint;

        [FieldOffset(0)]
        private byte _other_field;

        public bool other_field {
            get { return _other_field != 0; }
            set { _other_field = (byte)(value ? 1 : 0); }
        }
    }

1
我承认,我没有一个权威的答案来解释为什么Marshal.StructureToPtr()会以这种方式运作,除了明显的它不仅仅是复制字节。相反,它必须解释struct本身,通过解释该字段的正常规则,逐个地将每个字段单独编组到目标中。由于bool被定义为只有两个值之一,非零值被映射到true,这将编组为原始字节0x00000001
请注意,如果您真的只想要struct值的原始字节,您可以自己进行复制,而不必使用Marshal类。例如:
var test = new my_struct { my_uint = 0xDEADBEEF };
byte[] data = new byte[Marshal.SizeOf(test)];

unsafe
{
    byte* pb = (byte*)&test;

    for (int i = 0; i < data.Length; i++)
    {
        data[i] = pb[i];
    }
}

Console.WriteLine(string.Join(" ", data.Select(b => b.ToString("X2"))));

当然,为了让它工作,您需要为您的项目启用unsafe代码。您可以为相关项目执行此操作,或将上述内容构建为单独的帮助程序集,在该程序集中unsafe不那么危险(即您不介意为其他代码启用它和/或不关心程序集是否可验证等)。

啊,我只是为了好玩而已,没有完全阅读你的答案 :D 哎呀! - leppie
1
@leppie: BTDT。至少你玩得开心。 :) - Peter Duniho

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