获取non-explicit字段偏移量

Obtain non-explicit field offset

我有以下 class:

[StructLayout(LayoutKind.Sequential)]
class Class
{
    public int Field1;
    public byte Field2;
    public short? Field3;
    public bool Field4;
}

如何从 class 数据(或 object header)的开头获取 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,但它 returns 非托管偏移量,未托管。

如何从 FieldInfo 实例中检索此偏移量?是否有用于此目的的任何 .NET 方法,还是我必须编写自己的方法,同时考虑所有例外情况(类型大小、填充、显式偏移等)?

通过 TypedReference.MakeTypedReference 的一些技巧,可以获得对字段的引用,以及对对象数据开头的引用,然后只需减去即可。该方法可以在 SharpUtils.

中找到

.NET 4.7.2 中 classstruct 中字段的偏移量:

public static int GetFieldOffset(this FieldInfo fi) =>
                    GetFieldOffset(fi.FieldHandle);

public static int GetFieldOffset(RuntimeFieldHandle h) => 
                    Marshal.ReadInt32(h.Value + (4 + IntPtr.Size)) & 0xFFFFFF;

这些 return classstruct 中字段的字节偏移量,相对于某些相应托管实例在运行时的布局。这适用于所有 StructLayout 模式,以及值和 reference-types(包括泛型、reference-containing 或其他 non-blittable)。偏移值是 zero-based 相对于 user-defined 内容的开头或 'data body' 仅 structclass,并且不包括任何 header、前缀或其他填充字节。

讨论

由于struct类型没有header,returned整数偏移值可以直接通过指针运算使用,如果需要System.Runtime.CompilerServices.Unsafe(此处未显示) .另一方面,Reference-type objects 有一个 header,它必须是 skipped-over 才能引用所需的字段。这个objectheader通常是单个IntPtr,也就是说IntPtr.Size需要加上offset值。还需要取消引用 GC(“垃圾 collection”)句柄以首先获取 object 的地址。

考虑到这些因素,我们可以在运行时合成一个跟踪引用GC object的内部,通过组合使用 class 实例(例如 Object 句柄)的字段偏移量(通过上面显示的方法获得)。

以下方法仅对 class(而不是 struct)类型有意义,演示了该技术。为简单起见,它使用 ref-returnSystem.Runtime.CompilerServices.Unsafe 库。为简单起见,也省略了错误检查,例如断言 fi.DeclaringType.IsSubclassOf(obj.GetType())

/// <summary>
/// Returns a managed reference ("interior pointer") to the value or instance of type 'U'
/// stored in the field indicated by 'fi' within 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());
}

此方法 return 是一个指向 garbage-collected object 实例 obj 内部的托管“跟踪”指针。[参见 comment]可以用来任意读取写入字段,所以这个函数代替了传统的一对分开getter/setter函数。尽管 returned 指针不能存储在 GC 堆中,因此其生命周期仅限于当前堆栈帧的范围(即,及以下),但通过简单地调用再次发挥作用。

请注意,此通用方法仅使用 <U>、获取的 pointed-at 值的类型和 而不是 的类型进行参数化 ("<T>", 也许) containing class (这同样适用于下面的 IL 版本)。这是因为 bare-bones 这种技术的简单性不需要它。我们已经知道包含实例必须是引用 (class) 类型,因此在运行时它将通过引用句柄呈现给具有 object header 的 GC object , 这些事实在这里就足够了;关于推定类型“T”。

无需进一步了解

It's a matter of opinion whether adding vacuous <T, … >, which would allow us to indicate the where T: class constraint, would improve the look or feel of the example above. It certainly wouldn't hurt anything; I believe the JIT is smart enough to not generate additional generic method instantiations for generic arguments that have no effect. But since doing so seems chatty (other than for stating the constraint), I opted for the minimalism of strict necessity here.

在我自己的使用中,不是每次都传递一个 FieldInfo 或其各自的 FieldHandle,我实际保留的是各种 整数偏移值 对于 return 从 GetFieldOffset 编辑的感兴趣的字段,因为一旦获得这些字段,它们在运行时也是不变的。这消除了每次获取指针时的额外步骤(调用 GetFieldOffset)。事实上,由于我能够在我的项目中包含 IL 代码,所以这里是我用于上述功能的确切代码。与刚刚显示的 C# 一样,它简单地从包含 GC-object obj 加上一个(保留的)整数偏移量 offs 中合成一个托管指针它。

// Returns a managed 'ByRef' pointer to the (struct or reference-type) instance of type U 
// stored in the field at byte offset 'offs' within reference type instance 'obj'

.method public static !!U& RefFieldValue<U>(object obj, int32 offs) aggressiveinlining
{
    ldarg obj
    ldarg offs
    sizeof object
    add
    add
    ret
}

因此,即使您无法直接合并此 IL,我认为,在此处显示它也很好地说明了这种技术的极低运行时开销和诱人的简单性。

用法示例

class MyClass { public byte b_bar; public String s0, s1; public int iFoo; }

第一个演示在MyClass的实例中获取reference-typed字段s1的整数偏移量,然后使用它来获取和设置字段值。

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: method call used as l-value, 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 移动 containing object,就会自动调整这种类型的指针——保留内部偏移量。这意味着即使您在不知不觉中继续访问该字段,它也将保持有效。作为允许此功能的交换,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 object 一起使用,但如前所述,此处显示的 GetFieldOffset 方法也可以与 struct 完美配合.请确保不要将 RefFieldValue 方法与 value-types 一起使用,因为该代码包括针对预期的 object header 进行调整。对于这种更简单的情况,只需使用 System.Runtime.CompilerServicesUnsafe.AddByteOffset 作为地址算法即可。

不用说,这个技术对某些人来说,e 可能看起来有点激进。我只是注意到它多年来一直完美无缺地工作,特别是在 .NET Framework 4.7.2 上,包括 32 位和 64 位模式、调试与发布,以及我尝试过的各种 JIT 优化设置.