blog post image
Andrew Lock avatar

Andrew Lock

~12 min read

Easier reflection with [UnsafeAccessorType] in .NET 10

Exploring the .NET 10 preview - Part 9

Share on:

In this post I describe some of the improvements to the [UnsafeAccessor] mechanism that was introduced in .NET 8. [UnsafeAccessor] allows you to easily access private fields and invoke private methods of types, without needing to use the reflection APIs. In .NET 9 there are some limitations to the methods and types this will work with. In .NET 10, some of those gaps have been closed by the introduction of [UnsafeAccessorType], and it's those improvements we'll look at in this post.

Calling private members with [UnsafeAccessor] in .NET 8 and 9

The [UnsafeAccessor] mechanism was introduced in .NET 8, with support for generics added in .NET 9.

I'm going to ignore the question of why you might want to access private members like this as a tangential discussion. You've always been able to do this via the reflection APIs, [UnsafeAccessor] just makes it a bit easier and faster.

For example, let's say you want to retrieve the private _items field in List<T>:

public class List<T>
{
    T[]? _items;
    // .. other members
}

You could do this using reflection with code like this:

// Get a FieldInfo object for accessing the value
var itemsFieldInfo = typeof(List<int>)
    .GetField("_items", BindingFlags.NonPublic | BindingFlags.Instance);

// Create an instance of the list
var list = new List<int>(16);

// Retreive the list using reflection
var items = (int[])itemsFieldInfo.GetValue(list);

Console.WriteLine($"{items.Length} items"); // Prints "16 items"

To use [UnsafeAccessor] you must create a special extern method, decorated with the attribute, that has the correct signature for accessing the member you want. In the case of a field, that means a method that takes a single parameter of the target type, and returns an instance of the field's target type as a ref. The name of the method itself is not important.

// Create an instance of the list
var list = new List<int>(16);

// Invoke the method to retrieve the list
int[] items = Accessors<int>.GetItems(list);

Console.WriteLine($"{items.Length} items"); // Prints "16 items"

static class Accessors<T>
{
    [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_items")]
    public static extern ref T[] GetItems(List<T> list);
}

Note that as List<T> is a generic type, we must use a "container" type to declare the accessor method with the correct signature. Making the method itself generic, (GetItems<T>(List<T>)) will not work, nor will using a closed type GetItems(List<int>).

There are some more examples later in this post on calling generic methods, as well as members on generic types.

It's also worth noting that the GetItems() signature returns a ref (and it must when you're accessing a field), which means you can also change the field. The example below uses the same accessor method but this time replaces the field:

// Create an instance of the list
var list = new List<int>(16);
Console.WriteLine($"Capacity: {list.Capacity}"); // Prints "Capacity: 16"

// Invoke the method to retrieve the field ref and set the value of the field to an empty array
Accessors<int>.GetItems(list) = Array.Empty<int>();
Console.WriteLine($"Capacity: {list.Capacity}"); // Prints "Capacity: 0"

// Same accessor as before:
static class Accessors<T>
{
    [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_items")]
    public static extern ref T[] GetItems(List<T> list);
}

Of course it's not just fields that you can invoke, you can also call methods (and therefore properties) and constructors; anything defined on UnsafeAccesorType:

public enum UnsafeAccessorKind
{
  Constructor,
  Method,
  StaticMethod,
  Field,
  StaticField,
}

For example, the following shows an example of invoking a static method defined on List<T>:

// Invoking private static methods
// We can pass `null` as the instance argument because these are static methods
bool isCompat1 = Accessors<int?>.IsCompatibleObject(null, 123); // true
bool isCompat2 = Accessors<int?>.IsCompatibleObject(null, null); // true
bool isCompat3 = Accessors<int?>.IsCompatibleObject(null, 1.23); // false

static class Accessors<T>
{
    // The method we're invoking has this signature:
    //     private static bool IsCompatibleObject(object? value)
    // 
    // Our extern signature must include the target type as the first method parameter
    [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "IsCompatibleObject")]
    public static extern bool CheckObject(List<T> instance, object? value);
}

As you can see above, for both instance and static methods you include the "target" instance as the first parameter on the method (and pass null as the argument when invoking static methods). That way the runtime knows which Type it's working with: it's whatever the type of the first argument is. But what if you can't specify this type, because the type itself is private?

Limitations of [UnsafeAccessor] in .NET 9

The big limitation around using [UnsafeAccessor] in .NET 9 is that you must be able to directly reference all the types that are part of the target signature. As a concrete example, imagine a library you're using has a type that looks something like this.

public class PublicClass
{
    private readonly PrivateClass _private = new("Hello world!");

    internal PrivateClass GetPrivate() => _private;
}

internal class PrivateClass(string someValue)
{
    internal string SomeValue { get; } = someValue;
}

It's very contrived, but it'll do for our purposes. Imagine you have an instance of PublicClass but what you really need is SomeValue that's held on the _private field. However, PrivateClass is marked internal so you can't directly reference it. That means that none of these accessors will work, because there's no way to use PrivateClass in the signature:

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_private")]
static extern ref readonly PrivateClass GetByField(PublicClass instance);
//                         👆 ❌ Can't reference PrivateClass

[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GetPrivate")]
static extern PrivateClass GetByMethod(PublicClass instance);
//            👆 ❌ Can't reference PrivateClass

[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_SomeValue")]
static extern string GetSomeValue(PrivateClass instance);
//                               👆 ❌ Can't reference PrivateClass

The example above is where you can't reference the type in your signature due to its visibility. There's an additional scenario that won't work in .NET 9: where you don't have access to the types you're working with at compile time.

That second scenario will likely be rarer, but there's a couple of examples of this that come to mind:

  • The .NET runtime has this scenario due to circular-dependencies between different libraries, for example the HTTP and Cryptography libraries.
  • The Datadog instrumentation libraries need to access internal properties of libraries we're instrumenting, but we can't reference those libraries directly due to version compatibility constraints.

In .NET 9, the only solution to these problems was to fallback to "normal" reflection, to use System.Reflection.Emit, or to use System.Linq.Expressions, all of which are significantly slower than [UnsafeAccessor], and much more cumbersome.

Luckily, in .NET 10, we have a way to get the best of both worlds: we can use [UnsafeAccessor] even with types we can't reference!

Accessing unreferenced types with [UnsafeAccessorType] in .NET 10

In .NET 9 you must be able to directly reference all the types used in the method signature of a [UnsafeAccessor] method. .NET 10 introduces the [UnsafeAccessorType] attribute, which lets us use [UnsafeAccessor] even with types we can't reference.

Using [UnsafeAccessorType] with [UnsafeAccessor]

.NET 10 introduces a new attribute, [UnsafeAccessorType], which allows you to specify the expected type for an [UnsafeAccessor] parameter as a string, which solves both of the scenarios described in the previous section. It's easiest to see this in action, so let's look at the same example as before. Let's say we have this hierarchy.

public class PublicClass
{
    private readonly PrivateClass _private = new("Hello world!");

    internal PrivateClass GetPrivate() => _private;
}

internal class PrivateClass(string someValue)
{
    internal string SomeValue { get; } = someValue;
}

And we'll create some unsafe accessor methods to retrieve that pesky SomeValue:

[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GetPrivate")]
[return: UnsafeAccessorType("PrivateClass")] // 👈 Specify target return type as a string
static extern object GetByMethod(PublicClass instance);
//            👆 use object as return type

[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_SomeValue")]
static extern string GetSomeValue([UnsafeAccessorType("PrivateClass")] object instance);
// Specify target type in attribute 👆 and use object as instance type 👆

As you can see in the examples above, instead of referencing the type directly, you use object instead and add an [UnsafeAccessorType] with the "real" type (more on that shortly).

Once we have the above definitions, we can chain these two methods to retrieve the value stored in a PrivateClass instance, even though we can't reference it directly:

// Create the target instance
var publicClass = new PublicClass();

// Invoke GetPrivate(), and return the result as an object
object privateClass = GetByMethod(publicClass);

// Pass the object and invoke the SomeValue getter method
string value = GetSomeValue(privateClass);
Console.WriteLine(value); // Hello world!

And there you have it, the ability to use [UnsafeAccessor] with types that you can't reference.

Specifying type names with [UnsafeAccessorType]

The type name I used in the previous example was very simple, just PrivateClass, but this hides the fact that this is actually a fully qualified type name, just as you might use in Type.GetType(name) for example. This needs to be fully qualified though it doesn't have to be Assembly qualified, though doing so is generally more robust.

Also note that for generic types and methods, these strings must be in either the open or closed generic format, depending on their usage, e.g. List``1[[!0]]. Similarly, you need the + for handling nested classes.

To make that all a little more concrete, I've included some examples below taken from the runtime's unit tests for [UnsafeAccessor], which demonstrates the use of UnsafeAccessorType] for referencing types in accessor methods. Note that all the types are defined in an assembly called PrivateLib, and all the classes and members are internal, so can't be referenced directly.

namespace PrivateLib;

internal class Class1
{
    static int StaticField = 123;
    int InstanceField = 456;

    Class1() { }

    static Class1 GetClass() => new Class1();

    private Class1[] GetArray(ref Class1 a) => new[] { a };
}

internal class GenericClass<T>
{
    List<Class1> ClosedGeneric() => new List<Class1>();

    List<U> GenericMethod<U>() => new List<U>();

    bool GenericWithConstraints<V, W>(List<T> a, List<V> b, List<W> c, List<Class1> d)
        where W : T
         => true;
}

The following are a bunch of representative examples of accessors, in increasing complexity. Each one shows a different example of an [UnsafeAccessorType] usage. This also demonstrates different kinds of accessors, such as constructors.

// 1. Calling the Class1 constructor, returned type name is assembly qualified
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
[return: UnsafeAccessorType("PrivateLib.Class1, PrivateLib")]
extern static object CreateClass();

// 2. Calling a static method. Both the return type and the "target" parameter are assembly qualified
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "GetClass")]
[return: UnsafeAccessorType("PrivateLib.Class1, PrivateLib")]
extern static object CallGetClass([UnsafeAccessorType("PrivateLib.Class1, PrivateLib")] object a);

// 3. Returning a ref to the static field
[UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "StaticField")]
extern static ref int GetStaticField([UnsafeAccessorType("PrivateLib.Class1, PrivateLib")] object a);

// 4. Returning a ref to an instance field
// Note that we cannot use [UnsafeAccessorType] on the return type. More on that later.
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "InstanceField")]
extern static ref int GetInstanceField([UnsafeAccessorType("PrivateLib.Class1, PrivateLib")] object a);

// 5. Passing an object by reference and returning an array
// Note the `&` in the signature when passing by reference.
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GetArray")]
[return: UnsafeAccessorType("PrivateLib.Class1[], PrivateLib")]
extern static object CallM_RC1(TargetClass tgt, [UnsafeAccessorType("PrivateLib.Class1&, PrivateLib")] ref object a);

// 6. Invoking a method on a generic type, and returning a closed-generic type
// The return type uses a mix of fully qualified (for BCL types) and assembly qualified types
// Note that the open generic definition uses !0 to indicate an unspecified type parameter
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ClosedGeneric")]
[return: UnsafeAccessorType("System.Collections.Generic.List`1[[PrivateLib.Class1, PrivateLib]]")]
extern static object CallGenericClassClosedGeneric([UnsafeAccessorType("PrivateLib.GenericClass`1[[!0]], PrivateLib")] object a);

// 7. Invoking a generic method on a generic type.
// This is similar to the above, but we use !!0 to indicate an unspecified generic method type parameter.
// Note that the accessor method itself must be generic.
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GenericMethod")]
[return: UnsafeAccessorType("System.Collections.Generic.List`1[[!!0]]")]
extern static object CallGenericClassGenericMethod<U>([UnsafeAccessorType("PrivateLib.GenericClass`1[[!0]], PrivateLib")] object a);

// 8. Invoking a generic method, on a generic type, with type constraints
// This is a more complex version of the above, but additionally specifies
// type constraints which match the target method's constraints.
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GenericWithConstraints")]
public extern static bool CallGenericClassGenericWithConstraints<V,  W>(
    [UnsafeAccessorType("PrivateLib.GenericClass`1[[!0]], PrivateLib")] object tgt,
    [UnsafeAccessorType("System.Collections.Generic.List`1[[!0]]")]
    object a,
    [UnsafeAccessorType("System.Collections.Generic.List`1[[!!0]]")]
    object b,
    List<W> c,
    [UnsafeAccessorType("System.Collections.Generic.List`1[[PrivateLib.Class1, PrivateLib]]")]
    object d) where W : T;

These examples show the wide variety of invocations you can make using [UnsafeAccessorType] which makes this a very powerful approach. And what's more, it's fast. Using [UnsafeAccessor] in general is much faster than traditional reflection.

Unfortunately, even in .NET 10, there's still a couple of gaps with what we can do.

Limitations of [UnsafeAccessorType]

As I showed in the previous section, you can use [UnsafeAccessorType] in conjunction with [UnsafeAccessor] to access private members of types that you can't reference at runtime, but there's still a few gaps where you can't replace the use of traditional reflection. In summary, these are:

  • You can't call an accessor on a generic type if you can't represent the generic type argument.
  • You can't call fields where the field needs to be marked with [UnsafeAccessorType].
  • You can't call methods that return a ref where the return needs to be marked with [UnsafeAccessorType].

To clarify those cases, I'll provide some examples of types and accessors that don't work.

1. Unable to represent the type argument

The first case is pretty simple. Imagine we have the Generic<T> type, plus a helper class, Class1:

internal class Generic<T> { }
internal class Class1 { }

We need to create instances of the Generic<T> type even though it's marked internal, so we create an accessor for it:

static class Accessors<T>
{
    [UnsafeAccessor(UnsafeAccessorKind.Constructor)]
    [return: UnsafeAccessorType("Generic`1[[!0]]")]
    public static extern object Create();
}

This works fine when the T we need can be referenced:

object instance = Accessors<int>.Create();
Console.WriteLine(generic.GetType()); // Generic`1[System.Int32]

but what happens if we need to create an instance of Generic<Class1>? The simple answer is we can't. We would need to call Accessors<Class1>.Create(), but that won't compile as we can't reference Class1. So if we have this pattern then we have to fallback to traditional reflection.

What's more, even if we created an instance of Generic<Class1> using "traditional" reflection, we wouldn't be able to use [UnsafeAccessor] methods to interact with the object, because we would always need to do so through a method defined on Accessors<Class1>, which is again not possible because we can't reference Class1.

2. Unable to represent field return types

We'll extend the previous example to add a new type, Class2, which has a couple of fields that reference the Class1 type:

internal class Class1 { }

internal class Class2
{
    private Class1 _field1 = new();
    private readonly Class1 _field2 = new();
}

If _field1 and _field2 referenced known types, then we could create [UnsafeAccessor]s for them without issue. You might think that accessors like the following would work:

// Helper for creating a C2 instance
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
[return: UnsafeAccessorType("Class2")]
static extern object Create();

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_field1")]
[return: UnsafeAccessorType("Class1")]
static extern ref object CallField1([UnsafeAccessorType("Class2")] object instance);

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_field2")]
[return: UnsafeAccessorType("Class1")]
static extern ref readonly object CallField2([UnsafeAccessorType("Class2")] object instance);

However, if you try to use CallField1() or CallField2() you'll get an error at runtime:

object class2 = Create();
object field1 = CallField1(class2); // throws System.NotSupportedException: Invalid usage of UnsafeAccessorTypeAttribute
object field2 = CallField2(class2); // throws System.NotSupportedException: Invalid usage of UnsafeAccessorTypeAttribute

In both cases, you get a System.NotSupportedException stating Invalid usage of UnsafeAccessorTypeAttribute: you simply can't access fields unless you can represent the types.

As an aside, this really confused me when I was first trying out the feature. Accessing a field was the first thing I tried and I assumed I was doing it wrong. 😅

Just to repeat, this works fine if the field you're accessing is not using [UnsafeAccessorType], so if _field1 was an int for example. It's only trying to use [UnsafeAccessorType] with the field which fails.

3. Unable to represent ref method returns

Similar to the previous issue, if you have a ref returning method, and you can't represent the type, then you can't use [UnsafeAccessor]. For example, let's add a GetField1() method, which returns a ref to _field1:

internal class Class1 { }

internal class Class2
{
    private Class1 _field1 = new();
    private ref Class1 GetField1(Class2 a) => ref _field1;
}

An accessor for this might look like the following:

[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GetField1")]
[return: UnsafeAccessorType("Class1&")] // ref return 
static extern ref object CallGetField1([UnsafeAccessorType("Class2")] object instance);

But trying to call this accessor fails in the same way at runtime as trying to access the field directly:

object class2 = Create();
object field1 = CallGetField1(class2); // throws System.NotSupportedException: Invalid usage of UnsafeAccessorTypeAttribute

Those are all the limitations I found, so as long as what you're trying to do doesn't fall into one of these camps, then you should hopefully be ok!

Summary

In this post I described some improvements to the [UnsafeAccessor] mechanism that was introduced in .NET 8. I showed how [UnsafeAccessor] allows you to easily access private fields and invoke private members of types, without needing to use the reflection APIs in .NET 8 and .NET 9. I then described some of the limitations, namely that you need to be able to reference the types used by the members you're accessing.

Next I introduced the [UnsafeAccessorType] attribute, and showed how you could use it to invoke methods on types that you can't reference at compile time. I showed how you can use this to invoke methods, constructors, and fields, and how to work with generic types and generic methods. Finally, I described the limitations of [UnsafeAccessorType], namely that you can't use it to work with instances of generic types where you can't reference the type parameter, and that you can't use [UnsafeAccessorType] with fields or ref returning methods.

  • Buy Me A Coffee
  • Donate with PayPal
Andrew Lock | .Net Escapades
Want an email when
there's new posts?