.NET8New posture for accessing private members UnsafeAccessor (Part 1)

Foreword

A few days ago, in the .NET performance optimization group, some group members talked about a new feature of .NET8. This class is called UnsafeAccessor. Many group members did not know what this feature was for. So I wanted to write an article to take you through this feature.

In fact, I have paid attention to this special feature a long time ago, but .NET8 had not been officially released at that time, so I did not write an article. Now .NET8 has been released, and the official version will be released soon, and it just happened to be available. Some time, so I can also take you to understand this new feature.

Due to length reasons, this article will be divided into two parts. The first part will take you to understand what UnsafeAccessor does and what its uses are. The next part will take you to understand UnsafeAccessor. Code>’s performance comparison and its implementation principle.

First, before we understand this class, we must assume a scenario. Many times we will encounter such a scenario, that is, we need to access the private members of another class in one class, such as the following code:

var a = new A();
Console.WriteLine(a._value);
public class A
{
    private int _value = 10;
}

In the above code, we access the private member _value of class A in class B. This situation is in our actual development It is very common, but it is not allowed in .NET because private members are not allowed to be accessed externally, so we cannot access class A in class B >’s private member _value, but in actual development, we sometimes need to access the private member _value of class A. At this time What should we do? Let’s take a look at how to access private members.

Solution before .NET8

Before .NET8, we could access private members through the following methods, namely reflection, Emit, and Expression. Let’s take a look at these methods separately.

Reflection

In .NET, there is a technology called reflection, which should be familiar to any .NET development engineer. We can use reflection to access the metadata information of the assembly, call methods, access fields, etc., so we can use reflection to To access private members, for example, in the following code we can access private members through reflection, as shown below:

var a = new A();
// Reflective access to private members
var value = typeof(A).GetField("_value", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(a);
Console.WriteLine(value);
public class A
{
    private int _value = 10;
}

In the above code, we access the private member _value of class A through reflection, so that it can be accessed. However, this method has a disadvantage, which is performance comparison. Poor, because reflection is implemented by looking up metadata and temporary calls, so the performance is relatively poor. In actual development, we generally do not use reflection to access private members.

Emit

Emit is a technology provided by .NET to dynamically generate and compile code. Through Emit, we can dynamically generate a new method, which can directly access private members, thus avoiding the performance problem of reflection. Here is an example of using Emit to access private members:

var a = new A();
//Create a dynamic method with the signature int GetValue(A a)
var method = new DynamicMethod("GetValue", typeof(int), new Type[] { typeof(A) }, typeof(A));
// Get the ILGenerator of the method and generate the method body through Emit
var il = method.GetILGenerator();
//Push the private member _value of parameter a onto the stack
il.Emit(OpCodes.Ldarg_0);
// Push the private member _value onto the stack
il.Emit(OpCodes.Ldfld, typeof(A).GetField("_value", BindingFlags.NonPublic | BindingFlags.Instance));
//Return the value at the top of the stack
il.Emit(OpCodes.Ret);
// Through the method created by Emit, you can directly access the private member _value
var func = (Func<A, int>)method.CreateDelegate(typeof(Func<A, int>));
// call method
var value = func(a);
Console.WriteLine(value);
public class A
{
    private int _value = 10;
}

In the above code, we create a new method through Emit, which can directly access the private member _value of class A. The performance of this method is much better than reflection, but the code is more complex and difficult to maintain.

Expression

Expression is an expression tree technology provided by .NET. Through Expression, we can create an expression tree, and then compile the expression tree to generate a method that can access private members. Here is an example of using Expression to access private members:

var a = new A();
// Create an expression tree and access the private member _value
var parameter = Expression.Parameter(typeof(A), "x");
//Access private member _value
var field = Expression.Field(parameter, typeof(A).GetField("_value", BindingFlags.NonPublic | BindingFlags.Instance));
// Compile the expression tree and generate a method that can access the private member _value
var lambda = Expression.Lambda<Func<A, int>>(field, parameter);
// call method
var func = lambda.Compile();
var value = func(a);
Console.WriteLine(value);
public class A
{
    private int _value = 10;
}

In the above code, we create an expression tree through Expression, and then compile the expression tree to generate a method that can access the private member _value of class A. The performance of this method is better than reflection, and the code is relatively simple, but it is still more complicated than direct access.

.NET8 solution

I think many smart friends have already guessed that the new UnsafeAccessor in .NET8 is used to access private members. We can access private members through UnsafeAccessor , let’s take a look at how to use UnsafeAccessor to access private members.

Private fields

using System.Runtime.CompilerServices;
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_value")]
static extern ref int GetValue(A a);
var a = new A();
var value = GetValue(a);
Console.WriteLine(value);
public class A
{
    private int _value = 10;
}

First we need to introduce the System.Runtime.CompilerServices namespace, and then define a staicexternref method. The return value type of this method is the type of the field, and then its parameters are the corresponding The type of instance. On the method, we need to add a UnsafeAccessor feature. This feature has a parameter UnsafeAccessorKind. This parameter indicates what type of private members we want to access, such as fields, properties, Methods, etc. What we want to access here is the field, so what we pass in is UnsafeAccessorKind.Field, and then we also need to specify the name of the field we want to access. What we want to access here is _value field, so we pass in Name="_value", so that we can access private members through UnsafeAccessor.

Looking at the results of the operation, we can see that 10 is output as we expected.

bf4eab2dab558538e343e1f808a2b65d.png

Because it returns a reference to ref, we can use this reference to modify the value of the private member. For example, we modify the value of _value as follows:

using System.Runtime.CompilerServices;
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_value")]
static extern ref int ValueAccessor(A a);
var a = new A();
ref var value = ref ValueAccessor(a);
Console.WriteLine(value);
value = 20;
Console.WriteLine(ValueAccessor(a));
public class A
{
    private int _value = 10;
}

Looking at the results of the operation, we can see that we modified the value of _value, and the second output became 20.

0518be6f76fe4c3e063f2f0ee0f7f843.png

Private constructor

Similarly, using UnsafeAccessor we can also access the private constructor and private methods in the class. We can see that UnsafeAccessor has a parameter UnsafeAccessorKind , this parameter indicates what type of private members we want to access, such as fields, properties, methods, etc. The following is its definition:

private enum UnsafeAccessorKind
{
    Constructor,
    Method,
    StaticMethod,
    Field,
    StaticField
};

Let’s first look at how to access the private constructor, as shown below:

using System.Runtime.CompilerServices;
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
static extern A CreateA(int value);
var a = CreateA(10);
Console.WriteLine(a.Value);
public class A
{
    public readonly int Value;
    private A(int value)
    {
        Value = value;
    }
}

In the above code, we access the private constructor of class A through UnsafeAccessor. The parameters of this private constructor are of type int. We can see that we accessed the private constructor through UnsafeAccessor, then created an instance of A, and then output the value of Value. You can The output result is 10, so we can access the private constructor through UnsafeAccessor.

eaf22ffefe1ed6103907eb34a14b64b0.png

Private properties

Since attributes are syntactic sugar, the compiler will automatically generate a get and set method for us, such as publicintValue{get;set; }, a get_Value and set_Value method will be automatically generated. I will not demonstrate access to private methods separately here, but directly demonstrate access to private properties. It and Accessing private methods is the same, as follows:

using System.Runtime.CompilerServices;
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_Value")]
static extern int GetValue(A a);
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_Value")]
static extern void SetValue(A a, int value);
var a = new A();
SetValue(a, 10);
Console.WriteLine(GetValue(a));
public class A
{
    public int Value { get; set; }
}

In the above code, we access the private property Value of class A through UnsafeAccessor. This private property has get > and set methods, we access the get and set methods through UnsafeAccessor, and then we can access private properties , so that we can access private properties through UnsafeAccessor.

63450db9d5c9b2884deee4289f8aa18f.png

Limitations

Of course, there are still some limitations in using UnsafeAccessor, which you need to pay attention to when using it.

Universal generics

For example, it does not support universal generics at this stage, and code like the following is not supported:

[UnsafeAccessor(UnsafeAccessorKind.Field, Name="_myList")]
static extern ref List<T> Field<T>(MyClass<T> _this);

But now you can write code like this:

[UnsafeAccessor(UnsafeAccessorKind.Field, Name="_myList")]
static extern ref List<string> StringField(MyClass<string> _this);
[UnsafeAccessor(UnsafeAccessorKind.Field, Name="_myList")]
static extern ref List<double> DoubleField(MyClass<double> _this);

However, this will be solved in .NET9. Interested friends can follow the link below: https://github.com/dotnet/runtime/issues/89439

Private type

For example, here is a sample code:

// Assembly A
private class C
{
    private static int Method(int a) { ... }
}
// Assembly B
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name="Method")]
static extern int CallMethod(? c, int a);

The problem here is that we don’t know how to define the first parameter of CallMethod, because C is private and we can’t enter it in CallMethod Define it in the parameter.

Static class

For example, here is a sample code:

// Assembly A
public static class C
{
    private static int Method(int a) { ... }
}
// Assembly B
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name="Method")]
static extern int CallMethod(? c, int a);

The problem here is that we cannot define the first parameter of CallMethod in B because C is static and we cannot define it in Define it in the input parameters of >CallMethod.

Private class parameters

For example, here is a sample code:

// Assembly A
public class C
{
    private class D { }
    private static int Method(D d) { ... }
}
// Assembly B
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name="Method")]
static extern int CallMethod(? d); // Unable express D type as a parameter.

The problem here is that we cannot define the input parameters of CallMethod in B because D is private and we cannot define it in CallMethod Define it in the input parameters of . Including currently private return value parameters cannot be defined.

However, these problems will also be solved in .NET9. Interested friends can follow the link below:

https://github.com/dotnet/runtime/issues/90081

Summary

In this article, we first introduce several methods of accessing private members before .NET8, including reflection, Emit, and Expression. Although these methods can access private members, they each have their own advantages and disadvantages, such as poor reflection performance and high Emit and Expression code complexity.

Subsequently, we introduced in detail the new feature of .NET8 UnsafeAccessor, which is a more convenient and efficient way to access private members. We demonstrate through example code how to use UnsafeAccessor to access private members, including private fields, private constructors, and private properties. Moreover, UnsafeAccessor also supports modifying the value of private members.

However, UnsafeAccessor currently has some limitations, such as not supporting general generics and being unable to access private types, static classes, and private class parameters. However, these issues are expected to be resolved in .NET9.

Overall, UnsafeAccessor provides .NET developers with a new tool that allows us to access private members more conveniently and efficiently. Although there are still some limitations at present, with the continuous development and progress of .NET, we have reason to believe that these problems will be solved. At the same time, we also look forward to .NET being able to provide more functions and features in the future to meet our growing development needs.

-Technical Group: Add the editor's WeChat and make a note to join the group
Editor's WeChat: mm1552923 Public account: dotNet Programming Encyclopedia