Skip to content

Commit d58c541

Browse files
[CoreCLR and native AOT] UnsafeAccessorAttribute supports generic parameters (#99468)
* Generic type support Instance and static fields on generic types Re-enable and add to field tests Generic types with non-generic methods. Generic static methods * Native AOT support * Add design document Add strict check for precisely matched Generic constraints
1 parent 21bfbd5 commit d58c541

20 files changed

+1296
-215
lines changed
+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# `UnsafeAccessorAttribute`
2+
3+
## Background and motivation
4+
5+
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.
6+
7+
## Semantics
8+
9+
This attribute will be applied to an `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 static` method will throw `MissingFieldException` or `MissingMethodException`.
10+
11+
For `Method`, `StaticMethod`, `Field`, and `StaticField`, the type of the first argument of the annotated `extern static` method identifies the owning type. Only the specific type defined will be examined for inaccessible members. The type hierarchy is not walked looking for a match.
12+
13+
The value of the first argument is treated as `this` pointer for instance fields and methods.
14+
15+
The first argument must be passed as `ref` for instance fields and methods on structs.
16+
17+
The value of the first argument is not used by the implementation for static fields and methods.
18+
19+
The return value for an accessor to a field can be `ref` if setting of the field is desired.
20+
21+
Constructors can be accessed using Constructor or Method.
22+
23+
The return type is considered for the signature match. Modreqs and modopts are initially not considered for the signature match. However, if an ambiguity exists ignoring modreqs and modopts, a precise match is attempted. If an ambiguity still exists, `AmbiguousMatchException` is thrown.
24+
25+
By default, the attributed method's name dictates the name of the method/field. This can cause confusion in some cases since language abstractions, like C# local functions, generate mangled IL names. The solution to this is to use the `nameof` mechanism and define the `Name` property.
26+
27+
Scenarios involving generics may require creating new generic types to contain the `extern static` method definition. The decision was made to require all `ELEMENT_TYPE_VAR` and `ELEMENT_TYPE_MVAR` instances to match identically type and generic parameter index. This means if the target method for access uses an `ELEMENT_TYPE_VAR`, the `extern static` method must also use an `ELEMENT_TYPE_VAR`. For example:
28+
29+
```csharp
30+
class C<T>
31+
{
32+
T M<U>(U u) => default;
33+
}
34+
35+
class Accessor<V>
36+
{
37+
// Correct - V is an ELEMENT_TYPE_VAR and W is ELEMENT_TYPE_VAR,
38+
// respectively the same as T and U in the definition of C<T>::M<U>().
39+
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "M")]
40+
extern static void CallM<W>(C<V> c, W w);
41+
42+
// Incorrect - Since Y must be an ELEMENT_TYPE_VAR, but is ELEMENT_TYPE_MVAR below.
43+
// [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "M")]
44+
// extern static void CallM<Y, Z>(C<Y> c, Z z);
45+
}
46+
```
47+
48+
Methods with the `UnsafeAccessorAttribute` that access members with generic parameters are expected to have the same declared constraints with the target member. Failure to do so results in unspecified behavior. For example:
49+
50+
```csharp
51+
class C<T>
52+
{
53+
T M<U>(U u) where U: Base => default;
54+
}
55+
56+
class Accessor<V>
57+
{
58+
// Correct - Constraints match the target member.
59+
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "M")]
60+
extern static void CallM<W>(C<V> c, W w) where W: Base;
61+
62+
// Incorrect - Constraints do not match target member.
63+
// [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "M")]
64+
// extern static void CallM<W>(C<V> c, W w);
65+
}
66+
```
67+
68+
## API
69+
70+
```csharp
71+
namespace System.Runtime.CompilerServices;
72+
73+
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
74+
public class UnsafeAccessorAttribute : Attribute
75+
{
76+
public UnsafeAccessorAttribute(UnsafeAccessorKind kind);
77+
78+
public UnsafeAccessorKind Kind { get; }
79+
80+
// The name defaults to the annotated method name if not specified.
81+
// The name must be null for constructors
82+
public string? Name { get; set; }
83+
}
84+
85+
public enum UnsafeAccessorKind
86+
{
87+
Constructor, // call instance constructor (`newobj` in IL)
88+
Method, // call instance method (`callvirt` in IL)
89+
StaticMethod, // call static method (`call` in IL)
90+
Field, // address of instance field (`ldflda` in IL)
91+
StaticField // address of static field (`ldsflda` in IL)
92+
};
93+
```
94+
95+
## API Usage
96+
97+
```csharp
98+
class UserData
99+
{
100+
private UserData() { }
101+
public string Name { get; set; }
102+
}
103+
104+
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
105+
extern static UserData CallPrivateConstructor();
106+
107+
// This API allows accessing backing fields for auto-implemented properties with unspeakable names.
108+
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "<Name>k__BackingField")]
109+
extern static ref string GetName(UserData userData);
110+
111+
UserData ud = CallPrivateConstructor();
112+
GetName(ud) = "Joe";
113+
```
114+
115+
Using generics
116+
117+
```csharp
118+
class UserData<T>
119+
{
120+
private T _field;
121+
private UserData(T t) { _field = t; }
122+
private U ConvertFieldToT<U>() => (U)_field;
123+
}
124+
125+
// The Accessors class provides the generic Type parameter for the method definitions.
126+
class Accessors<V>
127+
{
128+
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
129+
extern static UserData<V> CallPrivateConstructor(V v);
130+
131+
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ConvertFieldToT")]
132+
extern static U CallConvertFieldToT<U>(UserData<V> userData);
133+
}
134+
135+
UserData<string> ud = Accessors<string>.CallPrivateConstructor("Joe");
136+
Accessors<string>.CallPrivateConstructor<object>(ud);
137+
```

src/coreclr/tools/Common/TypeSystem/IL/Stubs/ILEmitter.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -686,27 +686,27 @@ private ILToken NewToken(object value, int tokenType)
686686

687687
public ILToken NewToken(TypeDesc value)
688688
{
689-
return NewToken(value, 0x01000000);
689+
return NewToken(value, 0x01000000); // mdtTypeRef
690690
}
691691

692692
public ILToken NewToken(MethodDesc value)
693693
{
694-
return NewToken(value, 0x0a000000);
694+
return NewToken(value, 0x0a000000); // mdtMemberRef
695695
}
696696

697697
public ILToken NewToken(FieldDesc value)
698698
{
699-
return NewToken(value, 0x0a000000);
699+
return NewToken(value, 0x0a000000); // mdtMemberRef
700700
}
701701

702702
public ILToken NewToken(string value)
703703
{
704-
return NewToken(value, 0x70000000);
704+
return NewToken(value, 0x70000000); // mdtString
705705
}
706706

707707
public ILToken NewToken(MethodSignature value)
708708
{
709-
return NewToken(value, 0x11000000);
709+
return NewToken(value, 0x11000000); // mdtSignature
710710
}
711711

712712
public ILLocalVariable NewLocal(TypeDesc localType, bool isPinned = false)

src/coreclr/tools/Common/TypeSystem/IL/UnsafeAccessors.cs

+89-27
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,6 @@ public static MethodIL TryGetIL(EcmaMethod method)
2929
return GenerateAccessorBadImageFailure(method);
3030
}
3131

32-
// Block generic support early
33-
if (method.HasInstantiation || method.OwningType.HasInstantiation)
34-
{
35-
return GenerateAccessorBadImageFailure(method);
36-
}
37-
3832
if (!TryParseUnsafeAccessorAttribute(method, decodedAttribute.Value, out UnsafeAccessorKind kind, out string name))
3933
{
4034
return GenerateAccessorBadImageFailure(method);
@@ -54,7 +48,7 @@ public static MethodIL TryGetIL(EcmaMethod method)
5448
firstArgType = sig[0];
5549
}
5650

57-
bool isAmbiguous = false;
51+
SetTargetResult result;
5852

5953
// Using the kind type, perform the following:
6054
// 1) Validate the basic type information from the signature.
@@ -77,9 +71,10 @@ public static MethodIL TryGetIL(EcmaMethod method)
7771
}
7872

7973
const string ctorName = ".ctor";
80-
if (!TrySetTargetMethod(ref context, ctorName, out isAmbiguous))
74+
result = TrySetTargetMethod(ref context, ctorName);
75+
if (result is not SetTargetResult.Success)
8176
{
82-
return GenerateAccessorSpecificFailure(ref context, ctorName, isAmbiguous);
77+
return GenerateAccessorSpecificFailure(ref context, ctorName, result);
8378
}
8479
break;
8580
case UnsafeAccessorKind.Method:
@@ -105,9 +100,10 @@ public static MethodIL TryGetIL(EcmaMethod method)
105100
}
106101

107102
context.IsTargetStatic = kind == UnsafeAccessorKind.StaticMethod;
108-
if (!TrySetTargetMethod(ref context, name, out isAmbiguous))
103+
result = TrySetTargetMethod(ref context, name);
104+
if (result is not SetTargetResult.Success)
109105
{
110-
return GenerateAccessorSpecificFailure(ref context, name, isAmbiguous);
106+
return GenerateAccessorSpecificFailure(ref context, name, result);
111107
}
112108
break;
113109

@@ -136,9 +132,10 @@ public static MethodIL TryGetIL(EcmaMethod method)
136132
}
137133

138134
context.IsTargetStatic = kind == UnsafeAccessorKind.StaticField;
139-
if (!TrySetTargetField(ref context, name, ((ParameterizedType)retType).GetParameterType()))
135+
result = TrySetTargetField(ref context, name, ((ParameterizedType)retType).GetParameterType());
136+
if (result is not SetTargetResult.Success)
140137
{
141-
return GenerateAccessorSpecificFailure(ref context, name, isAmbiguous);
138+
return GenerateAccessorSpecificFailure(ref context, name, result);
142139
}
143140
break;
144141

@@ -232,6 +229,12 @@ private static bool ValidateTargetType(TypeDesc targetTypeMaybe, out TypeDesc va
232229
targetType = null;
233230
}
234231

232+
// We do not support signature variables as a target (for example, VAR and MVAR).
233+
if (targetType is SignatureVariable)
234+
{
235+
targetType = null;
236+
}
237+
235238
validated = targetType;
236239
return validated != null;
237240
}
@@ -366,7 +369,45 @@ private static bool DoesMethodMatchUnsafeAccessorDeclaration(ref GenerationConte
366369
return true;
367370
}
368371

369-
private static bool TrySetTargetMethod(ref GenerationContext context, string name, out bool isAmbiguous, bool ignoreCustomModifiers = true)
372+
private static bool VerifyDeclarationSatisfiesTargetConstraints(MethodDesc declaration, TypeDesc targetType, MethodDesc targetMethod)
373+
{
374+
Debug.Assert(declaration != null);
375+
Debug.Assert(targetType != null);
376+
Debug.Assert(targetMethod != null);
377+
378+
if (targetType.HasInstantiation)
379+
{
380+
Instantiation declClassInst = declaration.OwningType.Instantiation;
381+
var instType = targetType.Context.GetInstantiatedType((MetadataType)targetType.GetTypeDefinition(), declClassInst);
382+
if (!instType.CheckConstraints())
383+
{
384+
return false;
385+
}
386+
387+
targetMethod = instType.FindMethodOnExactTypeWithMatchingTypicalMethod(targetMethod);
388+
}
389+
390+
if (targetMethod.HasInstantiation)
391+
{
392+
Instantiation declMethodInst = declaration.Instantiation;
393+
var instMethod = targetType.Context.GetInstantiatedMethod(targetMethod, declMethodInst);
394+
if (!instMethod.CheckConstraints())
395+
{
396+
return false;
397+
}
398+
}
399+
return true;
400+
}
401+
402+
private enum SetTargetResult
403+
{
404+
Success,
405+
Missing,
406+
Ambiguous,
407+
Invalid,
408+
}
409+
410+
private static SetTargetResult TrySetTargetMethod(ref GenerationContext context, string name, bool ignoreCustomModifiers = true)
370411
{
371412
TypeDesc targetType = context.TargetType;
372413

@@ -399,23 +440,39 @@ private static bool TrySetTargetMethod(ref GenerationContext context, string nam
399440
// We have detected ambiguity when ignoring custom modifiers.
400441
// Start over, but look for a match requiring custom modifiers
401442
// to match precisely.
402-
if (TrySetTargetMethod(ref context, name, out isAmbiguous, ignoreCustomModifiers: false))
403-
return true;
443+
if (SetTargetResult.Success == TrySetTargetMethod(ref context, name, ignoreCustomModifiers: false))
444+
return SetTargetResult.Success;
404445
}
405-
406-
isAmbiguous = true;
407-
return false;
446+
return SetTargetResult.Ambiguous;
408447
}
409448

410449
targetMaybe = md;
411450
}
412451

413-
isAmbiguous = false;
452+
if (targetMaybe != null)
453+
{
454+
if (!VerifyDeclarationSatisfiesTargetConstraints(context.Declaration, targetType, targetMaybe))
455+
{
456+
return SetTargetResult.Invalid;
457+
}
458+
459+
if (targetMaybe.HasInstantiation)
460+
{
461+
TypeDesc[] methodInstantiation = new TypeDesc[targetMaybe.Instantiation.Length];
462+
for (int i = 0; i < methodInstantiation.Length; ++i)
463+
{
464+
methodInstantiation[i] = targetMaybe.Context.GetSignatureVariable(i, true);
465+
}
466+
targetMaybe = targetMaybe.Context.GetInstantiatedMethod(targetMaybe, new Instantiation(methodInstantiation));
467+
}
468+
Debug.Assert(targetMaybe is not null);
469+
}
470+
414471
context.TargetMethod = targetMaybe;
415-
return context.TargetMethod != null;
472+
return context.TargetMethod != null ? SetTargetResult.Success : SetTargetResult.Missing;
416473
}
417474

418-
private static bool TrySetTargetField(ref GenerationContext context, string name, TypeDesc fieldType)
475+
private static SetTargetResult TrySetTargetField(ref GenerationContext context, string name, TypeDesc fieldType)
419476
{
420477
TypeDesc targetType = context.TargetType;
421478

@@ -431,10 +488,10 @@ private static bool TrySetTargetField(ref GenerationContext context, string name
431488
&& fieldType == fd.FieldType)
432489
{
433490
context.TargetField = fd;
434-
return true;
491+
return SetTargetResult.Success;
435492
}
436493
}
437-
return false;
494+
return SetTargetResult.Missing;
438495
}
439496

440497
private static MethodIL GenerateAccessor(ref GenerationContext context)
@@ -486,7 +543,7 @@ private static MethodIL GenerateAccessor(ref GenerationContext context)
486543
return emit.Link(context.Declaration);
487544
}
488545

489-
private static MethodIL GenerateAccessorSpecificFailure(ref GenerationContext context, string name, bool ambiguous)
546+
private static MethodIL GenerateAccessorSpecificFailure(ref GenerationContext context, string name, SetTargetResult result)
490547
{
491548
ILEmitter emit = new ILEmitter();
492549
ILCodeStream codeStream = emit.NewCodeStream();
@@ -496,14 +553,19 @@ private static MethodIL GenerateAccessorSpecificFailure(ref GenerationContext co
496553

497554
MethodDesc thrower;
498555
TypeSystemContext typeSysContext = context.Declaration.Context;
499-
if (ambiguous)
556+
if (result is SetTargetResult.Ambiguous)
500557
{
501558
codeStream.EmitLdc((int)ExceptionStringID.AmbiguousMatchUnsafeAccessor);
502559
thrower = typeSysContext.GetHelperEntryPoint("ThrowHelpers", "ThrowAmbiguousMatchException");
503560
}
561+
else if (result is SetTargetResult.Invalid)
562+
{
563+
codeStream.EmitLdc((int)ExceptionStringID.InvalidProgramDefault);
564+
thrower = typeSysContext.GetHelperEntryPoint("ThrowHelpers", "ThrowInvalidProgramException");
565+
}
504566
else
505567
{
506-
568+
Debug.Assert(result is SetTargetResult.Missing);
507569
ExceptionStringID id;
508570
if (context.Kind == UnsafeAccessorKind.Field || context.Kind == UnsafeAccessorKind.StaticField)
509571
{

0 commit comments

Comments
 (0)