使用反射确定 .Net 类型在内存中的布局

11

我在C#中尝试优化解析器组合器。当序列化格式与内存格式相符时,一种可能的优化是直接(不安全地)将要解析的数据复制到类型实例甚至多个实例上。

我想编写代码来确定内存格式是否与序列化格式匹配,以便动态确定是否可以应用优化。(显然,这是一种不安全的优化,可能由于许多微妙的原因而无法工作。我只是在进行试验,并没有计划在生产代码中使用它。)

我使用属性[StructLayout(LayoutKind.Sequential, Pack = 1)]来强制不填充并强制内存顺序与声明顺序匹配。我用反射检查该属性,但实际上这只确认了“没有填充”。我还需要字段的顺序。(我非常希望不必为每个字段手动指定FieldOffset属性,因为那样很容易出错。)

我原以为可以使用GetFields返回的字段顺序,但文档明确指出该顺序是未指定的。

鉴于我正在使用StructLayout属性强制字段顺序,是否有一种方法可以反映这种排序?

编辑 我可以接受所有字段必须是可按位复制的的限制。


你不能通过反思那些属性来解决它吗? - It'sNotALie.
@newStackExchangeInstance 哪些属性? - Craig Gidney
1
LayoutKind.Sequential 只在结构体中只有 blittable 类型时控制托管表示。如果存在 unblittable 类型,则字段顺序由运行时控制。例如,请参见 https://dev59.com/dmzXa4cB1Zd3GeqPT3jA。 - GSerg
类型在内存中的实际布局似乎完全取决于实现,因此您提出的优化建议是行不通的。如果一个实验永远不能在生产代码中使用,那么它有什么用呢? - Cody Gray
@CodyGray 我使用StructLayout属性来强制布局。除非底层值的大小(例如指针)发生变化,否则它不应该在实现之间改变。有时人们会为了好玩而做一些事情。 - Craig Gidney
显示剩余5条评论
2个回答

5

如果使用blittable类型和LayoutKind.Sequential,则此操作是不必要的。

只要所有字段都是blittable类型,就无需使用反射或任何其他机制来查找结构体字段在内存中的顺序。

使用LayoutKind.Sequential声明的结构体的blittable字段将按照声明字段的顺序存储在内存中。这就是LayoutKind.Sequential的含义!

引用自此文档

对于可按位复制类型,LayoutKind.Sequential控制托管内存中的排列方式和非托管内存中的排列方式。对于不可按位复制类型,它会控制类或结构体被编组到非托管代码时的排列方式,但不控制托管内存中的排列方式。

请注意,这并不告诉您每个字段使用了多少填充空间。为了找出这一点,请参阅下面的内容。

要确定使用LayoutKind.Auto时的字段顺序,或使用任何布局时的字段偏移量

如果您愿意使用不安全代码并且使用反射,那么找到结构体字段的偏移量是相当容易的。

您只需要获取结构体每个字段的地址,并计算其相对于结构体开始位置的偏移量。知道每个字段的偏移量后,您可以计算它们的顺序(以及它们之间的任何填充字节)。要计算用于最后一个字段(如果有)的填充字节,您还需要使用sizeof(StructType)获得结构体的总大小。

以下示例适用于32位和64位。请注意,您无需使用fixed关键字,因为该结构体已经由于存储在堆栈上而被修复(如果尝试使用fixed,则会出现编译错误):

using System;
using System.Runtime.InteropServices;

namespace Demo
{
    [StructLayout(LayoutKind.Auto, Pack = 1)]

    public struct TestStruct
    {
        public int    I;
        public double D;
        public short  S;
        public byte   B;
        public long   L;
    }

    class Program
    {
        void run()
        {
            var t = new TestStruct();

            unsafe
            {
                IntPtr p  = new IntPtr(&t);
                IntPtr pI = new IntPtr(&t.I);
                IntPtr pD = new IntPtr(&t.D);
                IntPtr pS = new IntPtr(&t.S);
                IntPtr pB = new IntPtr(&t.B);
                IntPtr pL = new IntPtr(&t.L);

                Console.WriteLine("I offset = " + ptrDiff(p, pI));
                Console.WriteLine("D offset = " + ptrDiff(p, pD));
                Console.WriteLine("S offset = " + ptrDiff(p, pS));
                Console.WriteLine("B offset = " + ptrDiff(p, pB));
                Console.WriteLine("L offset = " + ptrDiff(p, pL));

                Console.WriteLine("Total struct size = " + sizeof(TestStruct));
            }
        }

        long ptrDiff(IntPtr p1, IntPtr p2)
        {
            return p2.ToInt64() - p1.ToInt64();
        }

        static void Main()
        {
            new Program().run();
        }
    }
}

使用 LayoutKind.Sequential 时确定字段偏移量

如果您的结构体使用 LayoutKind.Sequential,则可以直接使用 Marshal.OffsetOf() 来获取偏移量,但是这在使用 LayoutKind.Auto无法实现:

foreach (var field in typeof(TestStruct).GetFields())
{
    var offset = Marshal.OffsetOf(typeof (TestStruct), field.Name);
    Console.WriteLine("Offset of " + field.Name + " = " + offset);
}

如果你使用LayoutKind.Sequential,这显然是一种更好的方法,因为它不需要unsafe 代码,并且更短 - 而且你不需要预先知道字段的名称。正如我在上面所说的,没有必要确定内存中字段的顺序 - 但如果你需要找出使用了多少填充,这可能很有用。


谢谢,使用指针差异正是我需要的。只要.NET不允许省略字段或类似的优化... - Craig Gidney
当我尝试将&运算符应用于类似t.I的字段时,编译器会报“无法获取给定表达式的地址”的错误。 - Craig Gidney
@Strilanc 如果你复制并粘贴我的代码,它将正常工作,所以你一定是在做一些不同的事情。你能否提出一个新问题,问一下为什么你所做的事情不起作用?这是无法在评论中诊断的。我知道我发布的代码是有效的,它也不包含任何t.l代码(注意小写字母l),所以我知道你一定是在做一些不同的事情。 :) - Matthew Watson
@Strilanc 这很有趣 - 我从未尝试过获取只读字段的地址,所以我不知道这个! - Matthew Watson
1
@Strilanc 如果你确实需要这样做,你可以在该结构体的构造函数内部完成(但如果你从构造函数中执行此操作,则需要在获取字段地址时使用 fixed 关键字)。 - Matthew Watson

3
作为一个参考,针对那些想了解顺序和布局种类的人。例如,如果一个类型包含非可平移类型。
var fields = typeof(T).GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
fields.SortByFieldOffset();

var isExplicit = typeof(T).IsExplicitLayout;
var isSequential = typeof(T).IsLayoutSequential;

它使用了我写的扩展方法:
    public static void SortByFieldOffset(this FieldInfo[] fields) {
        Array.Sort(fields, (a, b) => OffsetOf(a).CompareTo(OffsetOf(b)) );
    }

    private static int OffsetOf(FieldInfo field) {
        return Marshal.OffsetOf(field.DeclaringType, field.Name).ToInt32();
    }

MSDN 上包含了关于 IsLayoutSequential 的有用信息。(链接)

2
return fields.OrderBy(OffsetOf).ToArray() 更加简洁,且是不可变的。 - Craig Gidney

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