我有以下课程:
[StructLayout(LayoutKind.Sequential)]
class Class
{
public int Field1;
public byte Field2;
public short? Field3;
public bool Field4;
}
如何从类数据(或对象标题)的开头获取Field4
的字节偏移量?
举例说明:
Class cls = new Class();
fixed(int* ptr1 = &cls.Field1) //first field
fixed(bool* ptr2 = &cls.Field4) //requested field
{
Console.WriteLine((byte*)ptr2-(byte*)ptr1);
}
结果偏移量在这种情况下为5,因为运行时实际上将Field3
移动到类型的末尾(并填充它),可能是因为它的类型是通用的。我知道有Marshal.OffsetOf
,但它返回非托管偏移,而不是托管。
如何从FieldInfo
实例中检索此偏移量?是否有任何.NET方法,或者我必须编写自己的方法,考虑所有异常(类型大小,填充,显式偏移等)?
答案 0 :(得分:4)
使用TypedReference.MakeTypedReference
周围的一些技巧,可以获得对该字段的引用,以及对象数据的开头,然后只需减去。该方法可在SharpUtils中找到。
答案 1 :(得分:1)
.NET 4.7.2中类或结构中字段的偏移量:
public static int GetFieldOffset(this FieldInfo fi) => GetFieldOffset(fi.FieldHandle);
public static int GetFieldOffset(RuntimeFieldHandle h) =>
Marshal.ReadInt32(h.Value + (4 + IntPtr.Size)) & 0xFFFFFF;
它们返回class
或struct
内某个字段的字节偏移量,相对于运行时某些托管实例的布局。这适用于所有StructLayout
模式,并且适用于值类型和引用类型(包括泛型,包含引用或其他不可引用的类型)。偏移值相对于struct
或class
的用户定义内容或“数据主体”的开头,从零开始,并且不包括任何标题,前缀或其他填充字节。
讨论
由于struct
类型没有标头,因此可以直接通过指针算术直接使用返回的整数偏移值,如果需要,可以直接使用System.Runtime.CompilerServices.Unsafe(此处未显示)。另一方面,引用类型的对象具有标头,必须将其跳过才能引用所需的字段。该对象标头通常是单个IntPtr
,这意味着需要将IntPtr.Size
添加到偏移量值。还必须首先取消引用GC(“垃圾收集”)句柄,以获取对象的地址。
考虑到这些因素,我们可以在运行时通过将字段偏移量(通过上述方法获得)与 GC对象的内部合成跟踪参考 class
的实例(例如Object
句柄)。
以下方法(仅对class
(而不是struct
)类型有意义)说明了该技术。为简单起见,它使用ref-return和System.Runtime.CompilerServices.Unsafe库。为了简单起见,也省略了错误检查,例如断言fi.DeclaringType.IsSubclassOf(obj.GetType())
。
/// <summary>
/// Returns a managed reference ("interior pointer") to field 'fi' of type 'U' in
/// managed object instance 'obj'
/// </summary>
public static unsafe ref U RefFieldValue<U>(Object obj, FieldInfo fi)
{
var pobj = Unsafe.As<Object, IntPtr>(ref obj);
pobj += IntPtr.Size + GetFieldOffset(fi.FieldHandle);
return ref Unsafe.AsRef<U>(pobj.ToPointer());
}
此方法将托管指针返回到垃圾回收对象实例obj
的“内部”。它可用于任意读取 或 字段,因此该功能代替了传统的一对独立的 getter / setter < / em>函数。
用法示例
class MyClass { public byte b_bar; public String s0, s1; public int iFoo; }
第一个演示获取s1
实例中引用类型的字段MyClass
的整数偏移量,然后使用它来获取和设置字段值。
var fi = typeof(MyClass).GetField("s1");
// note that we can get a field offset without actually having any instance of 'MyClass'
var offs = GetFieldOffset(fi);
// i.e., later...
var mc = new MyClass();
RefFieldValue<String>(mc, offs) = "moo-maa"; // field "setter"
// note the use of method calls as l-values (on the left-hand side of '=' assignment)
RefFieldValue<String>(mc, offs) += "!!"; // in-situ access
Console.WriteLine(mc.s1); // --> moo-maa!! (in the original)
// can be used as a non-ref "getter" for by-value access
var _ = RefFieldValue<String>(mc, offs) + "%%"; // 'mc.s1' not affected
如果这看起来有些混乱,则可以通过将托管指针保留为ref local变量来显着清除它。如您所知,每当GC移动包含 对象时,都会自动调整这种类型的指针(保留内部偏移)。这意味着,即使您继续无意识地访问该字段,它也将保持有效。作为允许使用此功能的交换,CLR要求不允许ref
局部变量本身逸出其堆栈帧,在这种情况下,这是由C#编译器强制执行的。
// demonstrate using 'RuntimeFieldHandle', and accessing a value-type field (int) this time
var h = typeof(MyClass).GetField(nameof(mc.iFoo)).FieldHandle;
// later... (still using 'mc' instance created above)
// acquire managed pointer to 'mc.iFoo'
ref int i = ref RefFieldValue<int>(mc, h);
i = 21; // directly affects 'mc.iFoo'
Console.WriteLine(mc.iFoo == 21); // --> true
i <<= 1; // operates directly on 'mc.iFoo'
Console.WriteLine(mc.iFoo == 42); // --> true
// any/all 'ref' uses of 'i' just affect 'mc.iFoo' directly:
Interlocked.CompareExchange(ref i, 34, 42); // 'mc.iFoo' (and 'i' also): 42 -> 34
摘要
这些用法示例着重于将技术与class
对象一起使用,但是如前所述,此处显示的GetFieldOffset
方法对于struct
也可以很好地工作。只要确保不要对值类型使用RefFieldValue
方法,因为该代码包括针对预期对象标头的调整。对于这种简单的情况,只需将System.Runtime.CompilerServicesUnsafe.AddByteOffset
用于地址算术即可。
不用说,对于某些人来说,这种技术似乎有点激进。我会注意到,它多年来一直完美地为我工作,特别是在.NET Framework 4.7.2上,并且包括32位和64位模式,调试与发行版以及我尝试过的各种JIT优化设置