结构体中存在LayoutKind.Explicit时,必须遵循LayoutKind.Sequential。

13

运行以下代码时:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace StructLayoutTest
{
    class Program
    {
        unsafe static void Main()
        {
            Console.WriteLine(IntPtr.Size);
            Console.WriteLine();


            Sequential s = new Sequential();
            s.A = 2;
            s.B = 3;
            s.Bool = true;
            s.Long = 6;
            s.C.Int32a = 4;
            s.C.Int32b = 5;

            int* ptr = (int*)&s;
            Console.WriteLine(ptr[0]);
            Console.WriteLine(ptr[1]);
            Console.WriteLine(ptr[2]);
            Console.WriteLine(ptr[3]);
            Console.WriteLine(ptr[4]);
            Console.WriteLine(ptr[5]);
            Console.WriteLine(ptr[6]);
            Console.WriteLine(ptr[7]);  //NB!


            Console.WriteLine("Press any key");
            Console.ReadKey();
        }

        [StructLayout(LayoutKind.Explicit)]
        struct Explicit
        {
            [FieldOffset(0)]
            public int Int32a;
            [FieldOffset(4)]
            public int Int32b;
        }

        [StructLayout(LayoutKind.Sequential, Pack = 4)]
        struct Sequential
        {
            public int A;
            public int B;
            public bool Bool;
            public long Long;
            public Explicit C;
        }
    }
}

我期望在x86和x64上都能得到这个输出:
4或8(取决于x86或x64)

2
3
1
6
0
4
5
garbage

而在x86中实际得到的是:
4

6
0
2
3
1
4
5
garbage

而在x64中实际得到的是:
8

6
0
2
3
1
0
4
5

更多信息:
- 当我删除LayoutKind.Explicit和FieldOffset属性时,问题就消失了。
- 当我删除Bool字段时,问题就消失了。
- 当我删除Long字段时,问题也消失了。
- 请注意,在x64上,似乎忽略了Pack=4属性参数?

这适用于.Net3.5和.Net4.0

我的问题是:我缺少什么?还是这个问题是个bug?
我找到了一个类似的问题:
当一个结构体包含DateTime字段时,为什么LayoutKind.Sequential表现不同?
但在我的情况下,即使子结构的属性更改,布局也会发生变化,而没有更改数据类型。所以它看起来不像是优化。此外,我想指出的是,另一个问题仍然没有得到答复。
在那个问题中,他们提到使用Marshalling时会尊重布局。我自己还没有测试过,但我想知道为什么在使用unsafe代码时不尊重布局,因为所有相关属性似乎都已经就位了?文档是否有提到,除非进行Marshalling,否则将忽略这些属性?为什么?
考虑到这一点,我能否期望LayoutKind.Explicit在unsafe代码中可靠地工作呢?
此外,文档提到保持与预期布局的结构体的动机:

为了减少与Auto值相关的布局问题,C#,Visual Basic和C ++编译器为值类型指定了Sequential布局。


但是这个动机显然不适用于unsafe代码?

1个回答

11

从LayoutKind枚举的MSDN Library文章中得知:

在非托管内存中,对象的每个成员的精确位置都被明确控制,取决于StructLayoutAttribute.Pack字段的设置。每个成员必须使用FieldOffsetAttribute来指示该字段在类型中的位置。

相关短语已经突出显示,但此程序并未发生这种情况,指针仍然完全引用托管内存。

是的,你看到的与包含DateTime类型成员的结构体相同,该类型应用了[StructLayout(LayoutKind.Auto)]。在确定布局的字段编组码中,CLR代码会尝试为托管结构体采用LayoutKind.Sequential规则。但是如果遇到任何与此目标冲突的成员,它就会迅速放弃而不会出声。自身不是连续的结构体就足以做到这一点。你可以在SSCLI20源, src/clr/vm/fieldmarshaler.cpp中查看这样的情况,搜索fDisqualifyFromManagedSequential

这将使其切换到自动布局,即适用于类的相同布局规则。它重新排列字段以最小化成员之间的填充。其净效果是所需内存量较小。在"Bool"成员之后有7个字节的填充,这是未使用的空间,以使"Long"成员对齐到8的倍数地址。非常浪费资源,通过将长整型作为布局中的第一个成员来修复它。

因此,与/* 偏移量-大小 */作为注释的明确布局不同:

        public int A;        /*  0 - 4 */
        public int B;        /*  4 - 4 */
        public bool Bool;    /*  8 - 1 */
        // padding           /*  9 - 7 */
        public long Long;    /* 16 - 8 */
        public Explicit C;   /* 24 - 8 */
                     /* Total:  32     */ 

它提供了:

        public long Long;    /*  0 - 8 */
        public int A;        /*  8 - 4 */
        public int B;        /* 12 - 4 */
        public bool Bool;    /* 16 - 1 */
        // padding           /* 17 - 3 */
        public Explicit C;   /* 20 - 8 */
                     /* Total:  28     */ 

仅用4个字节的内存可以轻松实现。64位布局需要额外的填充以确保长整型在存储在数组中时仍然对齐。这一切都是高度不文档化和可能会发生变化的,请务必不要依赖管理的内存布局。只有Marshal.StructureToPtr()可以给您保证。


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