使用reflection来确定.Net类型在内存中的布局方式
我正在尝试在C#中优化解析器组合器。 当序列化格式与内存格式匹配时,一种可能的优化是仅对要在实例上解析的数据的(不安全)memcpy甚至该类型的许多实例进行解析。
我想编写确定内存中格式是否与序列化格式匹配的代码,以便动态确定是否可以应用优化。 (显然这是一个不安全的优化,可能无法解决一大堆微妙的原因。我只是在尝试,而不是计划在生产代码中使用它。)
我使用属性[StructLayout(LayoutKind.Sequential,Pack = 1)]强制不填充并强制内存顺序匹配声明顺序。 我用reflection检查该属性,但实际上所有这些都证实是“无填充”。 我还需要字段的顺序。 (我非常希望不必为每个字段手动指定FieldOffset属性,因为这很容易出错。)
我假设我可以使用GetFields返回的字段顺序,但文档明确指出订单未指定。
鉴于我使用StructLayout属性强制字段的顺序,有没有办法反映该排序?
编辑我很好,所有字段必须是blittable的限制 。
如果使用带有blittable类型的LayoutKind.Sequential
,则不需要这样做
只要所有字段都是blittable,就不需要使用reflection或任何其他机制来查找内存中struct结构字段的顺序。
使用LayoutKind.Sequential声明的结构的blittable字段将按照声明字段的顺序在内存中。 这就是LayoutKind.Sequential
意思!
从这个文件 :
对于blittable类型,LayoutKind.Sequential控制托管内存中的布局和非托管内存中的布局。 对于非blittable类型,它在将类或结构封送到非托管代码时控制布局,但不控制托管内存中的布局。
请注意,这并不能告诉您每个字段使用的填充量。 要找到它,请参阅下文。
使用LayoutKind.Auto
时确定字段顺序,或使用任何布局时确定字段偏移
如果您乐意使用不安全的代码,并且不使用reflection,那么很容易找到结构字段偏移量。
您只需要获取结构的每个字段的地址,并从结构的开头计算它的偏移量。 知道每个字段的偏移量,您可以计算它们的顺序(以及它们之间的任何填充字节)。 要计算用于最后一个字段(如果有)的填充字节,您还需要使用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
时确定字段偏移量
如果您的struct使用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
代码,并且它更短 – 而且您不需要事先知道字段的名称。 如上所述,不需要确定内存中字段的顺序 – 但如果您需要了解使用了多少填充,这可能很有用。
作为想要了解顺序和布局类型的人的参考。 例如,如果类型包含非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的有用信息。