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

Posted

技术标签:

【中文标题】使用反射确定 .Net 类型在内存中的布局方式【英文标题】:Using reflection to determine how a .Net type is layed out in memory 【发布时间】:2013-07-04 19:20:22 【问题描述】:

我正在尝试优化 C# 中的解析器组合器。当序列化格式与内存格式匹配时,一种可能的优化是只对要在该类型的一个实例或什至多个实例上解析的数据进行(不安全的)memcpy。

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

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

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

鉴于我使用 StructLayout 属性强制字段的顺序,有没有办法反映该顺序?

edit 我可以接受所有字段必须为 blittable 的限制。

【问题讨论】:

你不能通过反映这些属性来解决它吗? @newStackExchangeInstance 哪些属性? LayoutKind.Sequential 仅在结构中仅存在 blittable 类型时才控制托管表示。如果存在不可复制的类型,则字段顺序无论如何都由运行时控制。例如。见***.com/q/14024483/11683。 内存中类型的实际布局似乎将完全依赖于实现,因此您建议的优化不是首发。如果实验永远无法在生产代码中使用,那它有什么好处? @CodyGray 我使用 StructLayout 属性来强制布局。它不应该在实现之间改变,除非底层值的大小发生变化(例如指针)。有时人们做事是为了好玩。 【参考方案1】:

供想了解布局顺序和种类的人参考。例如,如果一个类型包含非 blittable 类型。

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 的有用信息。

【讨论】:

return fields.OrderBy(OffsetOf).ToArray() 更简洁一些,并且启动时不可变。【参考方案2】:

如果将 LayoutKind.Sequential 与 blittable 类型一起使用,则这是不必要的

您不需要使用反射或任何其他机制来找出结构字段在内存中的顺序,只要所有字段都是 blittable 即可。

使用LayoutKind.Sequential 声明的结构的可blittable 字段将按照声明字段的顺序在内存中。这就是LayoutKind.Sequential 的意思!

From this documentation:

对于 blittable 类型,LayoutKind.Sequential 控制托管内存中的布局和非托管内存中的布局。对于非 blittable 类型,当类或结构编组为非托管代码时,它控制布局,但不控制托管内存中的布局。

请注意,这并不能告诉您每个字段使用了多少填充。要找出答案,请参见下文。

确定使用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 不允许任何优化,字段被省略或类似的东西...... 当我尝试将 & 运算符应用于像 t.I. 这样的字段时,出现“无法获取给定表达式的地址”编译器错误 @Strilanc 如果您复制并粘贴我的代码,它将正常工作,因此您必须做一些不同的事情。你能提出一个新问题,问为什么你正在做的事情行不通吗?在这里用 cmets 诊断是不可能的。我知道我发布的代码有效,它也不包含代码t.l(注意小写l),所以我知道你必须做一些不同的事情。 :) @Strilanc 这很有趣 - 我从未尝试获取只读字段的地址,所以我不知道! @Strilanc 如果您确实需要这样做,您可以在该结构的构造函数中进行(但在获取如果从构造函数中执行,则为字段地址)。

以上是关于使用反射确定 .Net 类型在内存中的布局方式的主要内容,如果未能解决你的问题,请参考以下文章

Java反射机制

Java 反射在实际开发中的应用

反射实例及概念

如何动态确定类型是不是是使用反射的接口?

C#怎么使用反射获取事件的响应方法

反射与代理