For good reasons, many folks don't want to use reflection. Since .NET 8, we have a better way of dealing with this in most of the cases (when known in advance). But we can go the opposite: Make it even unsafer for a tiny bit performance!
Disclaimer: Needless to say this blog post is just for fun, and is just for educational and "entertaining" purposes.
Objects with private fields
Imagine we have the following object:
public class SecretContainer(int initialValue)
{
private int _otherField = 2;
private int _secret = initialValue;
}
And we want to get the value of _secret
. With .NET 8, we can do things like:
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_secret")]
static extern ref int GetCountField(SecretContainer secret);
And that gives us the correct answer. But let's make it unsecure as hack! Just because it is possible
Memory-Layout
We can use our knowledge about the object itself. We know that the second field is indeed the thing we are looking for. So we can get the "raw" (managed) memory of the object and get the slice we need.
static unsafe void DisplayPrivateField(SecretContainer secret)
{
var handle = GCHandle.Alloc(secret, GCHandleType.Pinned);
try
{
// Get pointer to the start of the object
var ptr = handle.AddrOfPinnedObject();
// Create Span<byte> over object's memory
// As we have two int fields, we need to allocate 8 bytes at least
// And hopefully there is no padding between the fields, ...
var memory = new Span<byte>((void*)ptr, sizeof(int) * 2);
// Skip 4 bytes because of the "_otherField"
const int numberOffset = sizeof(int);
var number = MemoryMarshal.Read<int>(memory[numberOffset..]);
Console.WriteLine($"number = {number}");
}
finally
{
handle.Free();
}
}
This "beauty" gives us the value of _secret
without using reflection. Of course this has more problems than solutions:
- The object must be pinned (otherwise the GC can move it)
- We are relying on the memory layout of the object (which can change in future versions - imagine someone moving the field). But not only that, imagine things like the new
field
keyword in C# 14. We might be at the mercy of the compiler.
But interesting nevertheless.