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

时间:2013-07-07 07:13:18

标签: c# serialization reflection memory-layout bit-packing

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

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

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

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

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

编辑我很好,所有字段必须为blittable

2 个答案:

答案 0 :(得分:5)

如果将LayoutKind.Sequential与blittable类型

一起使用,则不需要这样做

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

使用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代码,并且它更短 - 而且您不需要知道名称这些领域提前。如上所述,不需要确定内存中字段的顺序 - 但如果您需要了解使用的填充量,这可能很有用。

答案 1 :(得分:2)

作为想要了解顺序和布局类型的人的参考。例如,如果类型包含非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的有用信息。