使用数组进行联合编组

6

我遇到了一个奇怪的情况,即在C#/.NET中编组包含数组的联合体。考虑以下程序:

namespace Marshal
{
    class Program
    {
        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        struct InnerType
        {
            byte Foo;
            //[MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.Struct, SizeConst = 1)]
            //byte[] Bar;
        }


        [StructLayout(LayoutKind.Explicit, Pack = 1)]
        struct UnionType
        {
            [FieldOffset(0)]
            InnerType UnionMember1;

            [FieldOffset(0)]
            [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.Struct, SizeConst = 1)]
            byte[] UnionMember2;
        }

        static void Main(string[] args)
        {
            Console.WriteLine(@"SizeOf UnionType: {0}", System.Runtime.InteropServices.Marshal.SizeOf(typeof(UnionType)));
        }
    }
}

如果您运行此程序,将会出现以下异常:
Could not load type 'UnionType' from assembly 'Marshal, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' because it contains an object field at offset 0 that is incorrectly aligned or overlapped by a non-object field.

如果您取消注释这两行代码,则程序将运行良好。我在想这是为什么。为什么将额外的数组添加到InnerType中可以解决问题?顺便说一下,无论您使数组多大都没有关系。如果没有数组,则UnionMember1和UnionMember2应该具有相同的大小。但是,如果有数组,则它们的大小不匹配,但不会引发任何异常。
更新:将InnerType更改为以下内容也会导致异常(这次是在InnerType上)。
[StructLayout(LayoutKind.Explicit, Pack = 1)]
struct InnerType
{
    [FieldOffset(0)]
    byte Foo;

    [FieldOffset(1)]
    [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.Struct, SizeConst = 1)]
    byte[] Bar;
}

我认为这应该等同于原始代码(使用LayoutKind.Sequential),其中未注释byte[] Bar。我不认为这里的问题与单词边界有任何关系--我正在使用Pack = 1。相反,我认为异常的第二部分是问题所在:“...它包含一个偏移量为0的对象字段,该字段被非对象字段重叠。”byte[]是引用类型,而byte本身是值类型。我可以看到“byte Foo”最终会与“byte[] UnionMember2”重叠。然而,这仍然无法解释为什么取消注释原始代码中的“byte[] bar”会使异常消失。

这些文章应该会有所帮助:https://dev59.com/dHM_5IYBdhLWcg3w3nXz,http://stackoverflow.com/questions/4673099/unions-in-c-sharp-incorrectly-aligned-or-overlapped-with-a-non-object-field。将UnionMember2的字段偏移更改为8可以解决此问题。 - David Venegoni
@David Venegoni - 谢谢!不过我已经看过这些了。#1190079是针对CF marshaller的特定情况,我没有使用CF而是使用Pack。#4673099解决了该用户的问题,但并没有解释造成异常的确切原因。使用8的偏移量可以消除异常(和union),但仍然没有任何线索说明为什么添加InnerType.bar会抑制异常。 - watkipet
PInvoke marshaller 不允许将结构映射到 byte[],因为这不是有意义的转换。大小为 1 也很奇怪,那只是一个普通的字节。您还不能将引用类型(如 byte[])与值类型重叠,垃圾回收器无法准确地查看字段是否存储引用。要解决这个问题,需要将该字段声明为固定大小缓冲区。 - Hans Passant
1
@Hans Passant - 你关于重叠引用和值类型的观点非常到位。然而,这仍然无法解释为什么添加“byte[] Bar”会使异常消失。至于一个字节的数组,那只是为了让例子简单明了。这是一个人为制造的例子。 - watkipet
只有提供真实的示例,才能得到好的帮助。 - Hans Passant
1个回答

0

我的假设是,顺序布局被覆盖,就像这个SO答案中描述的那样:当子结构具有LayoutKind.Explicit时未遵循LayoutKind.Sequential

请注意,pack设置为1并不会消除填充,即Bar没有FieldOffset为1。它在4字节(在x32上)对齐,并且根据Marshal.OffsetOf(),它应该在预期的位置4处。

然而,.NET运行时实际上可能会将引用类型Bar放在托管内存中的字节Foo之前,在这种情况下,它将正确地与UnionMember2重叠。

有趣的是,使用Foo int和float也会发生同样的事情,但是使用long和double又会再次出现异常。似乎它按大小对字段进行排序,但如果大小相等,则首先放置引用类型。

当我切换到x64时,long Foo也可以工作,这支持了这个理论。最后,我打开了一个内存窗口(调试->窗口->内存)并输入了位置&instance.UnionMember1.Foo,向上滚动一点以显示Foo之前的字节。然后使用立即窗口设置了FooBar的值,证明了Bar在0处,Foo在4处。(在Main中添加var instance = new UnionType()

请记住,这可能不是您想要的,Byte[]只被视为引用类型。您可以将其替换为object。根据您的目标,您可能能够使用fixed byte Bar[1]


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