获取非显式字段偏移

Ill*_*ack 6 .net c# reflection clr

我有以下课程:

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

如何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);
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,结果偏移量为5,因为运行时实际上移动Field3到类型的末尾(并填充它),可能是因为它的类型是通用的.我知道有Marshal.OffsetOf,但它返回非托管偏移,不管理.

如何从FieldInfo实例中检索此偏移量?是否有任何.NET方法用于此,或者我是否必须编写自己的方法,考虑所有异常(类型大小,填充,显式偏移等)?

Ill*_*ack 5

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


Gle*_*den 5

.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;
Run Code Online (Sandbox Code Playgroud)

这些返回一个classor 中字段的字节偏移量struct,相对于运行时某个相应托管实例的布局。这适用于所有StructLayout模式,以及值类型和引用类型(包括泛型、包含引用或其他非 blittable)。偏移值相对于用户定义的内容或“数据主体”的开头structclass仅从零开始,并且不包括任何标题、前缀或其他填充字节。

讨论

由于struct类型没有标头,返回的整数偏移值可以通过指针运算直接使用,如有必要(此处未显示)和System.Runtime.CompilerServices.Unsafe。另一方面,引用类型的对象有一个标题,必须跳过它才能引用所需的字段。这个对象头通常是单个IntPtr,这意味着IntPtr.Size需要添加到偏移值中。还需要取消引用 GC(“垃圾收集”)句柄以首先获取对象的地址。

考虑到这些因素,我们可以在运行时通过将字段偏移量(通过上述方法获得)与 的实例(例如句柄)组合来合成对GC 对象内部的跟踪引用classObject

以下方法仅对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());
}
Run Code Online (Sandbox Code Playgroud)

此方法返回一个托管的“跟踪”指针,指向垃圾收集对象实例的内部obj。它可以用于任意读取写入字段,因此这个函数取代了传统的一对单独的getter / setter函数。尽管返回的指针无法存储在 GC 堆中,因此其生命周期仅限于当前堆栈帧的范围(即,及以下),但通过简单地再次调用该函数随时获得它的成本非常低。

请注意,此泛型方法仅使用 参数化<U>,即获取的指向值的类型,而不是包含类的类型(<T>可能是“ ”)(这同样适用于下面的 IL 版本)。这是因为这种技术的基本简单性不需要它。我们已经知道包含实例必须是引用 ( ) 类型,因此在运行时它将通过引用句柄呈现给带有头的 GC 对象,仅这些事实在这里就足够了;无需进一步了解推定类型“ ”。classobjectT

添加 vacuous <T, … >,这将允许我们指示where T: class约束,是否会改善上述示例的外观或感觉,这是一个意见问题。它当然不会伤害任何东西;我相信 JIT 足够聪明,不会无效的泛型参数生成额外的泛型方法实例。但是由于这样做似乎很健谈(除了说明约束之外),我在这里选择了严格必要的极简主义。

在我自己的使用中,不是每次都传递 aFieldInfo或其各自的值FieldHandle,我实际保留的是从 返回的感兴趣字段的各种整数偏移值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
}
Run Code Online (Sandbox Code Playgroud)

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

示例用法

class MyClass { public byte b_bar; public String s0, s1; public int iFoo; }
Run Code Online (Sandbox Code Playgroud)

第一个演示获取 的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
Run Code Online (Sandbox Code Playgroud)

如果这看起来有点混乱,您可以通过将托管指针保留为ref 局部变量来显着清理它。如您所知,每当 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
Run Code Online (Sandbox Code Playgroud)

概括

使用示例侧重于将该技术与class对象一起使用,但如前所述,GetFieldOffset此处显示的方法也非常struct适用。请确保不要将该RefFieldValue方法与值类型一起使用,因为该代码包括针对预期的对象头进行调整。对于这种更简单的情况,只需使用System.Runtime.CompilerServicesUnsafe.AddByteOffset您的地址算术即可。

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

  • C# 版本的“RefFieldValue”绝对不安全,但可以通过始终管理内部引用来使其安全。您可以重新解释为已知布局类型以获取起始托管引用,然后将偏移量添加到其中。类似于:`class AnyClass { public byte FirstField; }` 然后: `static ref U RefFieldValue&lt;U&gt;(object obj, FieldInfo fi) =&gt; ref Unsafe.As&lt;byte, U&gt;(ref Unsafe.AddByteOffset(ref Unsafe.As&lt;AnyClass&gt;(obj).FirstField, GetFieldOffset(fi)));` (2认同)