Accessing private members without reflection in .NET 8.0

Β· 624 words Β· 3 minutes to read

One of the cool new features coming in .NET 8.0 is the ability to take advantage of a zero-overhead approach to access private members, via the UnsafeAccessorAttribute. This is a great improvement over the traditional, slow, reflection-based approach, as the new functionality is not only fast (compile-time) but also compatible with Native AOT.

Let’s have a quick look at the feature, which was originally tracked by this Github issue.

Sample code πŸ”—

Consider the following C# code, which will be the basis for our experimentation:

class Dog
{
    private string _name;

    private Dog(string name)
    {
        _name = name ?? throw new ArgumentNullException(nameof(name));
    }
    
    public Dog() : this("Pluto") 
    {}

    private void Meow(int times)
    {
        Console.WriteLine($"{_name} meows!");
        for (var i = 0; i < times; i++)
        {
            Console.Write("Meow! ");
        }
        Console.WriteLine();
    }

    public void Bark(int times)
    {
        Console.WriteLine($"{_name} barks!");
        for (var i = 0; i < times; i++)
        {
            Console.Write("Woof! ");
        }
        Console.WriteLine();
    }
}

This is a Dog class, which can only be instantiated using the public constructor, in which case the dog’s name defaults to Pluto. The constructor that allows setting the dog’s name is market to be private only. The dog implementation surfaces a single public method, Bark, which can be used to make the dog bark a specific amount of times. The dog can also meow, however, since dogs generally do not meow (duh!), that method is private and impossible to call in a normal way.

Accessing private members using UnsafeAccessorAttribute and UnsafeAccessorKind πŸ”—

Now let’s consider the following code:

var dog = new Dog();
dog.Bark(2);

Of course, based on what we just saw, this will print:

Pluto barks!
Woof! Woof! 

Next, let’s change the dog’s name, by updating the private field. This can be done by adding the appropriate extern signature, annotating it with UnsafeAccessorAttribute and defining the relevant UnsafeAccessorKind. From that perspective the approach is conceptually similar (and should feel familiar) to calling into native code, e.g. C++.

The argument corresponds to the object instance being used.

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_name")]
static extern ref string DogNameField(Dog dog);

The extern definition in my case is internal and just added to the Program class, but you are free to define its accessibility and place it wherever it makes sense in your code.

This allows us to change the name in the following way:

DogNameField(dog) = "Minnie";
dog.Bark(2);

This now prints (after the earlier output):

Minnie barks!
Woof! Woof! 

So we successfully changed the private field name, in a really neat fashion, and without any extra allocations. Let’s now call the private Meow method. In order to achieve that, we need another extern, this time with UnsafeAccessorKind.Method:

[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "Meow")]
static extern void CallDogMeowMethod(Dog dog, int times);

The first argument still corresponds to the type being used, while the subsequent ones are the arguments to be passed into the method call itself. With this in place, we can just call the private method directly:

CallDogMeowMethod(dog, 2);

This now prints:

Minnie meows!
Meow! Meow! 

Finally, we can also use the private constructor, which would require us to annotate the extern with UnsafeAccessorKind.Constructor:

[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
static extern Dog CallDogConstructor(string name);

Notice that this time we do not use Dog as an argument in the extern anymore, but instead it’s the return type only. Once this has been added, we can access the private constructor that allows us to give the dog a name:

var anotherDog = CallDogConstructor("Mickey");
anotherDog.Bark(2);

This code prints:

Mickey barks!
Woof! Woof! 

The same approach also works with static fields and methods. The current limitation of the feature is that it does not support generics - however this is coming in the future and is tracked in a separate Github issue.

The demo code from this article can be, as usually, found on Github.

About


Hi! I'm Filip W., a software architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP