You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Number of existing .NET serializers depend on skipping member visibility checks for data serialization. Examples include System.Text.Json or EF Core. In order to skip the visibility checks, the serializers typically use dynamically emitted code (Reflection.Emit or Linq.Expressions) and classic reflection APIs as slow fallback. Neither of these two options are great for source generated serializers and native AOT compilation. This API proposal introduces a first class zero-overhead mechanism for skipping visibility checks.
API Proposal
namespaceSystem.Runtime.CompilerServices;[AttributeUsage(AttributeTargets.Method,AllowMultiple=false,Inherited=false)]publicclassUnsafeAccessorAttribute:Attribute{publicUnsafeAccessorAttribute(UnsafeAccessorKindkind);publicUnsafeAccessorKindKind{get;}// The name defaults to the annotated method name if not specified.// The name must be null for constructorspublicstring?Name{get;set;}}publicenumUnsafeAccessorKind{Constructor,// call instance constructor (`newobj` in IL)Method,// call instance method (`callvirt` in IL)StaticMethod,// call static method (`call` in IL)Field,// address of instance field (`ldflda` in IL)StaticField// address of static field (`ldsflda` in IL)// Potential additions to handle niche cases// FieldGet, // get value of instance field (`ldfld` in IL). Required for `ref` fields. // FieldSet, // get value of instance field (`stfld` in IL). Required for `ref` fields. // NonVirtualMethod, // call instance method non-virtually (`call` in IL).};
This attribute will be applied on extern static method. The implementation of the extern static method annotated with this attribute will be provided by the runtime based on the information in the attribute and the signature of the method that the attribute is applied to. The runtime will try to find the matching method or field and forward the call to it. If the matching method or field is not found, the body of the extern method will throw MissingFieldException or MissingMethodException.
For UnsafeAccessorKind.{Static}Method and UnsafeAccessorKind.{Static}Field, the type of the first argument of the annotated extern method identifies the owning type. The value of the first argument is treated as @this pointer for instance fields and methods. The first argument must be passed as ref for instance fields and methods on structs. The value of the first argument is not used by the implementation for static fields and methods.
The generic parameters of the extern static method are concatenation of the type and method generic arguments of the target method. For example, extern static void Method1<T1, T2>(Class1<T1> @this) can be used to call Class1<T1>.Method1<T2>(). The generic constraints of the extern static method must match generic constraints of the target type, field or method.
Return type is considered for the signature match. modreqs and modopts are not considered for the signature match.
API Usage
classUserData{privateUserData(){}publicstringName{get;set;}}[UnsafeAccessor(UnsafeAccessorKind.Constructor)]externstaticUserDataCallPrivateConstructor();// This API allows accessing backing fields for auto-implemented properties with unspeakable names[UnsafeAccessor(UnsafeAccessorKind.Field,Name="<Name>k__BackingField")]externstaticrefstringGetName(UserData@this);UserDataud=CallPrivateConstructor();GetName(ud)="Joe";
Alternative Designs
Expand unsafe accessors in Roslyn The primary disadvantages is that compile time decision can be invalidated at runtime because the assembly can change. For example, Roslyn could be looking at a ref assembly where privates are stripped, NuGet version changes could cause compiler to see version 1 of an assembly at one phase in the build but at runtime we end up using version 2, etc.
Sidecar .dll with suppressed visibility checks. Runtime allows suppressing visibility checks in IL via IgnoresAccessChecksToAttribute. Build can generate sidecar .dll with trivial wrappers to access the private fields and methods with visibility checks suppressed. The source generated serializer would then call these wrappers to access the private members. It would be zero-cost way to circumvent visibility checks, assuming that the trivial wrapper methods get inlined by the JIT. The challenge with this option is integration into the overall build flow.
Better Reflection APIs. We have work planned on new faster reflection APIs in .NET 8. The challenge with this option is that reflection APIs are always going to be significantly slower than a direct method invocation or a simple field access. We would be leaving performance on the table.
Optimize reflection invoke call via pattern-matching The challenge with this option is that reflection invoke tends to be a complex code. Pattern matching them in the JIT or some other dedicated optimization step would be complex and fragile. For example, consider what it would take to pattern match typeof(MyType).GetMethod("set_IntProperty", BindingFlags.Public | BindingFlags.Instance).Invoke(BindingFlags.DoNotWrapExceptions , ptr, new object[] { (object)intValue });, optimize it to p.IntProperty = intValue;.
Do nothing Live with the fact that there is no cheap way to circumvent visibility checks without Reflection.Emit. Encourage serializers that want to be lean, fast and AOT friendly to avoid these patterns.
Risks
The proposed API has to be recognized in all linker, AOT compilers and runtimes.
The proposed API addressed skipping member visibility checks only. Skipping of type visibility checks can be added later (look for UnsafeAccessorType in the discussion below for the potential design).
Background and motivation
Number of existing .NET serializers depend on skipping member visibility checks for data serialization. Examples include System.Text.Json or EF Core. In order to skip the visibility checks, the serializers typically use dynamically emitted code (Reflection.Emit or Linq.Expressions) and classic reflection APIs as slow fallback. Neither of these two options are great for source generated serializers and native AOT compilation. This API proposal introduces a first class zero-overhead mechanism for skipping visibility checks.
API Proposal
This attribute will be applied on
extern staticmethod. The implementation of theextern staticmethod annotated with this attribute will be provided by the runtime based on the information in the attribute and the signature of the method that the attribute is applied to. The runtime will try to find the matching method or field and forward the call to it. If the matching method or field is not found, the body of the extern method will throwMissingFieldExceptionorMissingMethodException.For
UnsafeAccessorKind.{Static}MethodandUnsafeAccessorKind.{Static}Field, the type of the first argument of the annotated extern method identifies the owning type. The value of the first argument is treated as@thispointer for instance fields and methods. The first argument must be passed asreffor instance fields and methods on structs. The value of the first argument is not used by the implementation for static fields and methods.The generic parameters of the
extern staticmethod are concatenation of the type and method generic arguments of the target method. For example,extern static void Method1<T1, T2>(Class1<T1> @this)can be used to callClass1<T1>.Method1<T2>(). The generic constraints of theextern staticmethod must match generic constraints of the target type, field or method.Return type is considered for the signature match. modreqs and modopts are not considered for the signature match.
API Usage
Alternative Designs
typeof(MyType).GetMethod("set_IntProperty", BindingFlags.Public | BindingFlags.Instance).Invoke(BindingFlags.DoNotWrapExceptions , ptr, new object[] { (object)intValue });, optimize it top.IntProperty = intValue;.Risks
UnsafeAccessorTypein the discussion below for the potential design).