Introducing the PrivateProxy Library Utilizing .NET 8 UnsafeAccessor

Yoshifumi Kawai
5 min readSep 25, 2023

--

I am excited to announce the release of the PrivateProxy library. In essence, it is a library that provides access to private fields/properties/methods. By leveraging the new UnsafeAccessor feature in .NET 8, it achieves No Reflection, high performance, and is AOT-safe.

GitHub — Cysharp/PrivateProxy

Note: It exclusively runs on .NET 8! So, remember to give it a try once .NET 8 gets its official release. If you’re an enthusiast, why not test it out right now?

Here’s a brief overview: When you want to access private members of a particular type, you prepare a type annotated with [GeneratePrivateProxy(type)].

using PrivateProxy;

public class Sample
{
int _field1;
int PrivateAdd(int x, int y) => x + y;
}

[GeneratePrivateProxy(typeof(Sample))]
public partial struct SampleProxy;

Then, you can smoothly access those members.

// You can access like this.
var sample = new Sample();
sample.AsPrivateProxy()._field1 = 10;

One of the major advantages is that it’s based on the Source Generator. This means that it provides type-safety, allows for code completion, and if you change a variable name, it will be detectable as a compile-time error.

Up until now, even if we based our work on the Source Generator, there wasn’t something we couldn’t do. However, the beauty of UnsafeAccessor is that it doesn’t produce any objects; it allows you to use the original method’s type directly. When you look at the generated code, it reflects this.

// Source Generator generate this type
partial struct SampleProxy(Sample target)
{
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_field1")]
static extern ref int ___field1__(Sample target);

[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "PrivateAdd")]
static extern int __PrivateAdd__(Sample target, int x, int y);

public ref int _field1 => ref ___field1__(target);
public int PrivateAdd(int x, int y) => __PrivateAdd__(target, x, y);
}

public static class SamplePrivateProxyExtensions
{
public static SampleProxy AsPrivateProxy(this Sample target)
{
return new SampleProxy(target);
}
}

This ensures that language features such as ref, readonly, and others are accurately represented. It can also naturally handle mutable structs, and above all, there is absolutely no performance degradation.

The primary use case for this is likely unit testing. Therefore, one might argue that performance isn’t of paramount importance. However, in terms of performance, it’s designed to be reliable enough to be used during application runtime without any issues.

Testing Private Method

Kent Beck says NO.

However, I think it’s okay. The style of not testing private stems simply from language constraints. In environments like Rust, where you can smoothly test private methods, no one would think twice about it.

The desire to test private methods is natural. However, using reflection can increase the noise in the code and reduce its resilience to changes, so it’s probably best to avoid it. Using internal and InternalVisibleTo is a common method. But, there are cases where internal might broaden the scope too much.

This is where PrivateProxy comes in. With it, you can naturally write tests for private methods.

C# 12

By the way, in the initial mention of SampleProxy, I subtly used the syntax of C# 12. Did you notice?

[GeneratePrivateProxy(typeof(Sample))]
public partial struct SampleProxy;

In the “SampleProxy;” part, when creating an empty class, you can now just use “;” instead of “{ }”. Although this might seem like a minor feature, I believe it’s quite a valuable one. Especially with Source Generators, there was often a need to allocate empty classes. And while it’s just a change from two characters to one, “{ }” does affect code formatting. Should it be expressed in three lines with breaks, or should it be appended to the end? Such decisions are no longer a concern with “;”. So, the impact of reducing two characters to one is significant. I think it’s a great feature addition.

ref field

PrivateProxy supports static methods and is also compatible with mutable structs.

using PrivateProxy;

public struct MutableStructSample
{
int _counter;
void Increment() => _counter++;

// static and ref sample
static ref int GetInstanceCounter(ref MutableStructSample sample) => ref sample._counter;
}

// use ref partial struct
[GeneratePrivateProxy(typeof(MutableStructSample))]
public ref partial struct MutableStructSampleProxy;
var sample = new MutableStructSample();
var proxy = sample.AsPrivateProxy();
proxy.Increment();
proxy.Increment();
proxy.Increment();

// call private static method.
ref var counter = ref MutableStructSampleProxy.GetInstanceCounter(ref sample);

Console.WriteLine(counter); // 3
counter = 9999;
Console.WriteLine(proxy._counter); // 9999

Supporting mutable structs is quite challenging. The reason being, when a struct is stored in a field, a copy is passed. So, when written ordinarily, any changes don’t get reflected in the original struct. PrivateProxy has resolved this issue using the C# 11 ref field.

For MutableStructSampleProxy, the Source Generator generates code as follows:

ref partial struct MutableStructSampleProxy
{
ref MutableStructSample target;

public MutableStructSampleProxy(ref MutableStructSample target)
{
this.target = ref target;
}

[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "Increment")]
static extern void __Increment__(ref MutableStructSample target);

public void Increment() => __Increment__(ref this.target);
}

public static class MutableStructSamplePrivateProxyExtensions
{
public static MutableStructSampleProxy AsPrivateProxy(this ref MutableStructSample target)
{
return new MutableStructSampleProxy(ref target);
}
}

When a struct is passed using AsPrivateProxy (this essentially refers to “this ref”, but in the case of extension methods, there’s no need to explicitly write “ref” on the caller side, so it can be used naturally), it is retained as a ref throughout. As a result, if there are any state changes in the struct during method invocation, the modifications are seamlessly shared.

UnsafeAccessor for InternalCall

I think one good use (?) of UnsafeAccessor is to forcefully call the InternalCall in the corelib. For instance, at the root of string creation, there’s something called FastAllocateString. Under normal circumstances, users cannot call this.

[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "FastAllocateString")]
static extern string FastAllocateString(string _, int length);

var rawString = FastAllocateString(null!, 10);
var mutableSpan = MemoryMarshal.CreateSpan(ref MemoryMarshal.GetReference(rawString.AsSpan()), rawString.Length);

"abcde".CopyTo(mutableSpan);
"fghij".CopyTo(mutableSpan.Slice(5));

Console.WriteLine(rawString); // abcdefghij

However, with UnsafeAccessor, you can pretty much do whatever you want.

As for the String, to prevent you from doing whatever you want, there’s a method called String.Create. It prepares an immutable String by modifying it through an Action callback. However, there are quite a few cases where you can’t use it when you want to use it, like when you can’t pass a Span<T> to the TState with an Action. Especially, it’s often the case that you can’t use it exactly when you want to.

Moreover, calling this FastAllocateString and modifying the String has been decisively rejected in dotnet/runtime#36989 Make string.FastAllocateString public. In other words, they’re telling you not to do it.

It is never safe or supported to mutate the contents of a returned string instance. If you mutate a string instance within your own library or application, you are entering unsupported territory. A future framework update could break you. Or — more likely — you’ll encounter memory corruption that will be very painful for you or your customers to diagnose.

Your frustration is understandable. However, saying it’s okay just because there’s a way to pass an Action with String.Create is a bit oversimplified. After all, what they’re doing is essentially the same, so I wish they would allow us to modify it. I hope you understand that some of us just want to tinker with it (I’m one of those people!). So, let’s proceed at our own risk; in other words, let’s just do it…!

PrivateProxy only works with .NET 8! .NET 8 is going to be released in November, so don’t forget until then!

--

--

Yoshifumi Kawai
Yoshifumi Kawai

Written by Yoshifumi Kawai

a.k.a. neuecc. Creator of UniRx, UniTask, MessagePack for C#, MagicOnion etc. Microsoft MVP for C#. CEO/CTO of Cysharp Inc. Live and work in Tokyo, Japan.