Today topic is class or struct fields and how to get their offsets.
Field offsets can help you in numerous ways, first what comes in mind is that you can set fields of the struct or class instance without using TypedReference (doesn’t work in IL2CPP) and without GC allocs which comes with boxing when either instance or a value is a struct. Second, it could help you with unmanaged access to the class/struct for custom binary serialization, etc.
Why?
Field offset is a offset in bytes from the object address to the specific field. In .NET it can be specified explicitly, or the compiler could layout the structure by itself.
In Unity (at least in the newer versions) we have UnsafeUtility.GetFieldOffset(FieldInfo fld) which does exactly what we want, but what if we want cross-runtime support for field offsets and don’t want to make a call to an engine (given that it will eventually go to Mono and ask it about offset anyway)?
Then we have a great opportunity to lookup some Mono code.
Mono source code
Mono source code is a big helper for me when I want to understand how something works internally. Also this is a source of having unsafe memory layouts for class objects, arrays, strings, etc, where you can look exactly how does it work in Mono/IL2CPP.
Let’s try find something in there, GitHub allows us to search in code, let’s start with GetFieldOffset. After couple of managed methods we will get directly to the our precious C code.
gint32
ves_icall_RuntimeFieldInfo_GetFieldOffset (MonoReflectionFieldHandle field, MonoError *error)
{
MonoClassField *class_field = MONO_HANDLE_GETVAL (field, field);
mono_class_setup_fields (class_field->parent);
return class_field->offset - MONO_ABI_SIZEOF (MonoObject);
}
So, what do we have there? Not really much, we know that some class_field will have a field named offset and we should subtract IntPtr.Size * 2 from it (the size of MonoObject). MonoClassField is a typedef for _MonoClassField, so let’s search for that.
struct _MonoClassField {
/* Type of the field */
MonoType *type;
const char *name;
/* Type where the field was defined */
MonoClass *parent;
/*
* Offset where this field is stored; if it is an instance
* field, it's the offset from the start of the object, if
* it's static, it's from the start of the memory chunk
* allocated for statics for the class.
* For special static fields, this is set to -1 during vtable construction.
*/
int offset;
};
Oh, here it is! Not really hard to find. So we take MonoReflectionFieldHandle (which is basically a RuntimeFieldHandle.Value) skip three pointers (type, name and parent) and then take int32.
Let’s write it down:
public static unsafe int GetFieldOffset(this FieldInfo field){
var rhv = (IntPtr*)field.FieldHandle.Value; // pointer to MonoClassField
rhv += 3; // skip three pointers.
return *(int*)rhv - IntPtr.Size * 2; //load the value of a pointer (4 bytes, int32), then subtracting 16 bytes from it.
}
Now we can check our results, let’s write a test structures:
public class TestWithinClassOffset{
private IntPtr _foo;
private byte _bar;
public byte targetField; // should be 9 on x64 platform
}
public struct TestWithinStructOffset{
private int _foo;
public int targetField; // should be 4
}
I’ll test that in Unity:
Sounds correct. But… As we know from previous article, and the Mono code above, we subtract IntPtr.Size * 2. What does it mean?
It means that every class object contains a header in which there are two pointers, one for vtable and one for threadsync. This is implementation-dependent, but let’s take that for granted. This means that for class object fields we should add those 16 bytes (on 64-bit platform) back. So the real offset of the field within class would be 25.
Output:
WithinClass offset: 25 (25)
WithinStruct offset: 4 (4)
Right, now it’s absolutely correct. Value in the braces is the output of UnsafeUtility.GetFieldOffset().
Make it cross-runtime
What if we also want to have this helper function in real dotnet family? .NET, .NET Framework, and .NET Core.
Usually .NET and Mono implementations are quite simillar, so I took that fact as baseline for my research. Don’t bother yourself, I’ve already found that out:
public static unsafe int GetFieldOffsetRealDotNet(this FieldInfo field){
var rhv = (byte*)field.FieldHandle.Value;
// 0x3FFFFF = 22 bits, field offset use at least 22 bits.
// https://github.com/dotnet/runtime/blob/62d33ee48d57feba67b261b55db666bdc202b1c1/src/coreclr/vm/field.h#L36
return *(int*)(rhv + IntPtr.Size + sizeof(int)) & 0x3FFFFF;
}
Turns out that in real .NET we only need to skip 1 pointer and 1 integer, and here it comes, our field offset. Note that there are no subtraction, that’s it, .NET store field offsets a little bit different.
Make it REALLY cross-runtime?
Ok, now we have two different methods, but how do we know which runtime we are currently in? Of course, you can use current runtime name and check its name for “Mono”, but that’s not the way we are going in. We need it to be compile-time static, right?
Turns out, that there’s a little different between the real dotnets and Mono/IL2CPP/Burst, and it could be resolved in compile-time.
sizeof(TypedReference)!
Sounds stupid, but on .NET it’s always 2 * IntPtr.Size, and in Mono/IL2CPP it’s always 3 * IntPtr.Size, so you can branch between two functions in your code.
Full source code could be found here: https://gist.github.com/Meetem/55775f2d9e05eb4c6739d57f648eaafb
In some of the next articles I will show you how to set a value of a field using that way, but I think you are smart enough already to do it yourself.
hii,
did you try NativeAOT as well, maybe? Apparently it has a yet another special magic offset to get the field offset, or this trickery doesn’t work due to something getting stripped. I tried to find magic combination for a bit but now I think it’s for the best to wait for dotnet team to implement an official solution, hopefully in net 9
Hello, thank you for the insight! I’ve only tried non-AOT version. It seems like static reflection in .NET NativeAOT works differently. I’ll try to look into that when free minute will be available!
You can also take a look into dotnet clr sources, this is a good spot for digging down.