Skip to content

Commit 6944b7c

Browse files
tlakolloLakshanFtlakollovitek-karas
authored
Add support for annotations in classes/interfaces/structs in the linker (#1929)
Main goal: Adding [DynamicallyAccessedMembers] on a type Base enables code like: ```C# Base b; b.GetType().GetMethods() .... ``` The object.GetType() call will treat the return value as annotated with the annotation from the Base type. Base and all derived types will then include all necessary members according to the annotation. Same will work for interfaces (putting the annotation on an interface). Changes: - Data flow now tracks static type of all values - New class which implements the annotation cache and marking for type hierarchies - Integration from MarkStep and ReflectionMethodBodyScanner - Lot new tests Co-authored-by: Lakshan Fernando <[email protected]> Co-authored-by: tlakollo <[email protected]> Co-authored-by: vitek-karas <[email protected]>
1 parent 3856441 commit 6944b7c

File tree

6 files changed

+1270
-122
lines changed

6 files changed

+1270
-122
lines changed
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Diagnostics.CodeAnalysis;
8+
using Mono.Cecil;
9+
using Mono.Linker.Steps;
10+
11+
namespace Mono.Linker.Dataflow
12+
{
13+
class DynamicallyAccessedMembersTypeHierarchy
14+
{
15+
readonly LinkContext _context;
16+
readonly MarkStep _markStep;
17+
18+
// Cache of DynamicallyAccessedMembers annotations applied to types and their hierarchies
19+
// Values
20+
// annotation - the aggregated annotation value from the entire base and interface hierarchy of the given type
21+
// If the type has a base class with annotation a1 and an interface with annotation a2, the stored
22+
// annotation is a1 | a2.
23+
// applied - set to true once the annotation was applied to the type
24+
// This only happens once the right reflection pattern is found.
25+
// If a new type is being marked and one of its base types/interface has the applied set to true
26+
// the new type will apply its annotation and will also set its applied to true.
27+
// Non-interface types
28+
// - Only marked types with non-empty annotation are put into the cache
29+
// - Non-marked types are not stored in the cache
30+
// - Marked types which are not in the cache don't have any annotation
31+
// Interface types
32+
// - All interface types accessible from marked types are stored in the cache
33+
// - If the interface type doesn't have annotation the value None is stored here
34+
//
35+
// It's not possible to use the marking as a filter for interfaces in the cache
36+
// because interfaces are marked late and in effectively random order.
37+
// For this cache to be effective we need to be able to fill it for all base types and interfaces
38+
// of a type which is currently being marked - at which point the interfaces are not yet marked.
39+
readonly Dictionary<TypeDefinition, (DynamicallyAccessedMemberTypes annotation, bool applied)> _typesInDynamicallyAccessedMembersHierarchy;
40+
41+
public DynamicallyAccessedMembersTypeHierarchy (LinkContext context, MarkStep markStep)
42+
{
43+
_context = context;
44+
_markStep = markStep;
45+
_typesInDynamicallyAccessedMembersHierarchy = new Dictionary<TypeDefinition, (DynamicallyAccessedMemberTypes, bool)> ();
46+
}
47+
48+
public (DynamicallyAccessedMemberTypes annotation, bool applied) ProcessMarkedTypeForDynamicallyAccessedMembersHierarchy (TypeDefinition type)
49+
{
50+
// Non-interfaces must be marked already
51+
Debug.Assert (type.IsInterface || _context.Annotations.IsMarked (type));
52+
53+
DynamicallyAccessedMemberTypes annotation = _context.Annotations.FlowAnnotations.GetTypeAnnotation (type);
54+
bool apply = false;
55+
56+
// We'll use the cache also as a way to detect and avoid recursion
57+
// There's no possiblity to have recursion among base types, so only do this for interfaces
58+
if (type.IsInterface) {
59+
if (_typesInDynamicallyAccessedMembersHierarchy.TryGetValue (type, out var existingValue))
60+
return existingValue;
61+
62+
_typesInDynamicallyAccessedMembersHierarchy.Add (type, (annotation, false));
63+
}
64+
65+
// Base should already be marked (since we're marking its derived type now)
66+
// so we should already have its cached values filled.
67+
TypeDefinition baseType = type.BaseType?.Resolve ();
68+
Debug.Assert (baseType == null || _context.Annotations.IsMarked (baseType));
69+
if (baseType != null && _typesInDynamicallyAccessedMembersHierarchy.TryGetValue (baseType, out var baseValue)) {
70+
annotation |= baseValue.annotation;
71+
apply |= baseValue.applied;
72+
}
73+
74+
// For the purposes of the DynamicallyAccessedMembers type hierarchies
75+
// we consider interfaces of marked types to be also "marked" in that
76+
// their annotations will be applied to the type regardless if later on
77+
// we decide to remove the interface. This is to keep the complexity of the implementation
78+
// relatively low. In the future it could be possibly optimized.
79+
if (type.HasInterfaces) {
80+
foreach (InterfaceImplementation iface in type.Interfaces) {
81+
var interfaceType = iface.InterfaceType.Resolve ();
82+
if (interfaceType != null) {
83+
var interfaceValue = ProcessMarkedTypeForDynamicallyAccessedMembersHierarchy (interfaceType);
84+
annotation |= interfaceValue.annotation;
85+
apply |= interfaceValue.applied;
86+
}
87+
}
88+
}
89+
90+
Debug.Assert (!apply || annotation != DynamicallyAccessedMemberTypes.None);
91+
92+
if (apply) {
93+
// One of the base/interface types is already marked as having the annotation applied
94+
// so we need to apply the annotation to this type as well
95+
var reflectionMethodBodyScanner = new ReflectionMethodBodyScanner (_context, _markStep);
96+
var reflectionPatternContext = new ReflectionPatternContext (_context, true, type, type);
97+
reflectionMethodBodyScanner.ApplyDynamicallyAccessedMembersToType (ref reflectionPatternContext, type, annotation);
98+
reflectionPatternContext.Dispose ();
99+
}
100+
101+
// Store the results in the cache
102+
// Don't store empty annotations for non-interface types - we can use the presence of the row
103+
// in the cache as indication of it instead.
104+
// This doesn't work for interfaces, since we can't rely on them being marked (and thus have the cache
105+
// already filled), so we need to always store the row (even if empty) for interfaces.
106+
if (annotation != DynamicallyAccessedMemberTypes.None || type.IsInterface) {
107+
_typesInDynamicallyAccessedMembersHierarchy[type] = (annotation, apply);
108+
}
109+
110+
return (annotation, apply);
111+
}
112+
113+
public DynamicallyAccessedMemberTypes ApplyDynamicallyAccessedMembersToTypeHierarchy (
114+
ReflectionMethodBodyScanner reflectionMethodBodyScanner,
115+
ref ReflectionPatternContext reflectionPatternContext,
116+
TypeDefinition type)
117+
{
118+
Debug.Assert (_context.Annotations.IsMarked (type));
119+
120+
// The type should be in our cache already
121+
(var annotation, var applied) = GetCachedInfoForTypeInHierarchy (type);
122+
123+
// If the annotation was already applied to this type, there's no reason to repeat the operation, the result will
124+
// be no change.
125+
if (applied || annotation == DynamicallyAccessedMemberTypes.None)
126+
return annotation;
127+
128+
// Apply the effective annotation for the type
129+
reflectionMethodBodyScanner.ApplyDynamicallyAccessedMembersToType (ref reflectionPatternContext, type, annotation);
130+
131+
// Mark it as applied in the cache
132+
_typesInDynamicallyAccessedMembersHierarchy[type] = (annotation, true);
133+
134+
// Propagate the newly applied annotation to all derived/implementation types
135+
// Since we don't have a data structure which would allow us to enumerate all derived/implementation types
136+
// walk all of the types in the cache. These are good candidates as types not in the cache don't apply.
137+
foreach (var candidate in _typesInDynamicallyAccessedMembersHierarchy) {
138+
if (candidate.Value.annotation == DynamicallyAccessedMemberTypes.None)
139+
continue;
140+
141+
ApplyDynamicallyAccessedMembersToTypeHierarchyInner (reflectionMethodBodyScanner, ref reflectionPatternContext, candidate.Key);
142+
}
143+
144+
return annotation;
145+
}
146+
147+
bool ApplyDynamicallyAccessedMembersToTypeHierarchyInner (
148+
ReflectionMethodBodyScanner reflectionMethodBodyScanner,
149+
ref ReflectionPatternContext reflectionPatternContext,
150+
TypeDefinition type)
151+
{
152+
(var annotation, var applied) = GetCachedInfoForTypeInHierarchy (type);
153+
154+
if (annotation == DynamicallyAccessedMemberTypes.None)
155+
return false;
156+
157+
if (applied)
158+
return true;
159+
160+
TypeDefinition baseType = type.BaseType?.Resolve ();
161+
if (baseType != null)
162+
applied = ApplyDynamicallyAccessedMembersToTypeHierarchyInner (reflectionMethodBodyScanner, ref reflectionPatternContext, baseType);
163+
164+
if (!applied && type.HasInterfaces) {
165+
foreach (InterfaceImplementation iface in type.Interfaces) {
166+
var interfaceType = iface.InterfaceType.Resolve ();
167+
if (interfaceType != null) {
168+
if (ApplyDynamicallyAccessedMembersToTypeHierarchyInner (reflectionMethodBodyScanner, ref reflectionPatternContext, interfaceType)) {
169+
applied = true;
170+
break;
171+
}
172+
}
173+
}
174+
}
175+
176+
if (applied) {
177+
reflectionMethodBodyScanner.ApplyDynamicallyAccessedMembersToType (ref reflectionPatternContext, type, annotation);
178+
_typesInDynamicallyAccessedMembersHierarchy[type] = (annotation, true);
179+
}
180+
181+
return applied;
182+
}
183+
184+
(DynamicallyAccessedMemberTypes annotation, bool applied) GetCachedInfoForTypeInHierarchy (TypeDefinition type)
185+
{
186+
Debug.Assert (type.IsInterface || _context.Annotations.IsMarked (type));
187+
188+
// The type should be in our cache already
189+
if (!_typesInDynamicallyAccessedMembersHierarchy.TryGetValue (type, out var existingValue)) {
190+
// If it's not in the cache it should be a non-interface type in which case it means there were no annotations
191+
Debug.Assert (!type.IsInterface);
192+
return (DynamicallyAccessedMemberTypes.None, false);
193+
}
194+
195+
return existingValue;
196+
}
197+
}
198+
}

src/linker/Linker.Dataflow/FlowAnnotations.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ public DynamicallyAccessedMemberTypes GetFieldAnnotation (FieldDefinition field)
6969
return DynamicallyAccessedMemberTypes.None;
7070
}
7171

72+
public DynamicallyAccessedMemberTypes GetTypeAnnotation (TypeDefinition type)
73+
{
74+
return GetAnnotations (type).TypeAnnotation;
75+
}
76+
7277
public DynamicallyAccessedMemberTypes GetGenericParameterAnnotation (GenericParameter genericParameter)
7378
{
7479
TypeDefinition declaringType = genericParameter.DeclaringType?.Resolve ();
@@ -122,6 +127,9 @@ DynamicallyAccessedMemberTypes GetMemberTypesForDynamicallyAccessedMembersAttrib
122127

123128
TypeAnnotations BuildTypeAnnotations (TypeDefinition type)
124129
{
130+
// class, interface, struct can have annotations
131+
DynamicallyAccessedMemberTypes typeAnnotation = GetMemberTypesForDynamicallyAccessedMembersAttribute (type);
132+
125133
var annotatedFields = new ArrayBuilder<FieldAnnotation> ();
126134

127135
// First go over all fields with an explicit annotation
@@ -338,7 +346,7 @@ TypeAnnotations BuildTypeAnnotations (TypeDefinition type)
338346
}
339347
}
340348

341-
return new TypeAnnotations (type, annotatedMethods.ToArray (), annotatedFields.ToArray (), typeGenericParameterAnnotations);
349+
return new TypeAnnotations (type, typeAnnotation, annotatedMethods.ToArray (), annotatedFields.ToArray (), typeGenericParameterAnnotations);
342350
}
343351

344352
static bool ScanMethodBodyForFieldAccess (MethodBody body, bool write, out FieldDefinition found)
@@ -518,17 +526,21 @@ void LogValidationWarning (IMetadataTokenProvider provider, IMetadataTokenProvid
518526
readonly struct TypeAnnotations
519527
{
520528
readonly TypeDefinition _type;
529+
readonly DynamicallyAccessedMemberTypes _typeAnnotation;
521530
readonly MethodAnnotations[] _annotatedMethods;
522531
readonly FieldAnnotation[] _annotatedFields;
523532
readonly DynamicallyAccessedMemberTypes[] _genericParameterAnnotations;
524533

525534
public TypeAnnotations (
526535
TypeDefinition type,
536+
DynamicallyAccessedMemberTypes typeAnnotation,
527537
MethodAnnotations[] annotatedMethods,
528538
FieldAnnotation[] annotatedFields,
529539
DynamicallyAccessedMemberTypes[] genericParameterAnnotations)
530-
=> (_type, _annotatedMethods, _annotatedFields, _genericParameterAnnotations)
531-
= (type, annotatedMethods, annotatedFields, genericParameterAnnotations);
540+
=> (_type, _typeAnnotation, _annotatedMethods, _annotatedFields, _genericParameterAnnotations)
541+
= (type, typeAnnotation, annotatedMethods, annotatedFields, genericParameterAnnotations);
542+
543+
public DynamicallyAccessedMemberTypes TypeAnnotation { get => _typeAnnotation; }
532544

533545
public bool TryGetAnnotation (MethodDefinition method, out MethodAnnotations annotations)
534546
{

src/linker/Linker.Dataflow/ReflectionMethodBodyScanner.cs

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public void ScanAndProcessReturnValue (MethodBody methodBody)
7676
var reflectionContext = new ReflectionPatternContext (_context, ShouldEnableReflectionPatternReporting (method), method, method.MethodReturnType);
7777
reflectionContext.AnalyzingPattern ();
7878
RequireDynamicallyAccessedMembers (ref reflectionContext, requiredMemberTypes, MethodReturnValue, method.MethodReturnType);
79+
reflectionContext.Dispose ();
7980
}
8081
}
8182
}
@@ -92,6 +93,7 @@ public void ProcessAttributeDataflow (IMemberDefinition source, MethodDefinition
9293
var reflectionContext = new ReflectionPatternContext (_context, true, source, methodParameter);
9394
reflectionContext.AnalyzingPattern ();
9495
RequireDynamicallyAccessedMembers (ref reflectionContext, annotation, valueNode, methodParameter);
96+
reflectionContext.Dispose ();
9597
}
9698
}
9799
}
@@ -105,6 +107,15 @@ public void ProcessAttributeDataflow (IMemberDefinition source, FieldDefinition
105107
var reflectionContext = new ReflectionPatternContext (_context, true, source, field);
106108
reflectionContext.AnalyzingPattern ();
107109
RequireDynamicallyAccessedMembers (ref reflectionContext, annotation, valueNode, field);
110+
reflectionContext.Dispose ();
111+
}
112+
113+
public void ApplyDynamicallyAccessedMembersToType (ref ReflectionPatternContext reflectionPatternContext, TypeDefinition type, DynamicallyAccessedMemberTypes annotation)
114+
{
115+
Debug.Assert (annotation != DynamicallyAccessedMemberTypes.None);
116+
117+
reflectionPatternContext.AnalyzingPattern ();
118+
MarkTypeForDynamicallyAccessedMembers (ref reflectionPatternContext, type, annotation);
108119
}
109120

110121
static ValueNode GetValueNodeForCustomAttributeArgument (CustomAttributeArgument argument)
@@ -138,6 +149,7 @@ public void ProcessGenericArgumentDataFlow (GenericParameter genericParameter, T
138149
var reflectionContext = new ReflectionPatternContext (_context, enableReflectionPatternReporting, source, genericParameter);
139150
reflectionContext.AnalyzingPattern ();
140151
RequireDynamicallyAccessedMembers (ref reflectionContext, annotation, valueNode, genericParameter);
152+
reflectionContext.Dispose ();
141153
}
142154

143155
ValueNode GetTypeValueNodeFromGenericArgument (TypeReference genericArgument)
@@ -173,7 +185,7 @@ protected override void WarnAboutInvalidILInMethod (MethodBody method, int ilOff
173185
protected override ValueNode GetMethodParameterValue (MethodDefinition method, int parameterIndex)
174186
{
175187
DynamicallyAccessedMemberTypes memberTypes = _context.Annotations.FlowAnnotations.GetParameterAnnotation (method, parameterIndex);
176-
return new MethodParameterValue (parameterIndex, memberTypes, DiagnosticUtilities.GetMethodParameterFromIndex (method, parameterIndex));
188+
return new MethodParameterValue (method, parameterIndex, memberTypes, DiagnosticUtilities.GetMethodParameterFromIndex (method, parameterIndex));
177189
}
178190

179191
protected override ValueNode GetFieldValue (MethodDefinition method, FieldDefinition field)
@@ -200,6 +212,7 @@ protected override void HandleStoreField (MethodDefinition method, FieldDefiniti
200212
var reflectionContext = new ReflectionPatternContext (_context, ShouldEnableReflectionPatternReporting (method), method, field, operation);
201213
reflectionContext.AnalyzingPattern ();
202214
RequireDynamicallyAccessedMembers (ref reflectionContext, requiredMemberTypes, valueToStore, field);
215+
reflectionContext.Dispose ();
203216
}
204217
}
205218

@@ -211,6 +224,7 @@ protected override void HandleStoreParameter (MethodDefinition method, int index
211224
var reflectionContext = new ReflectionPatternContext (_context, ShouldEnableReflectionPatternReporting (method), method, parameter, operation);
212225
reflectionContext.AnalyzingPattern ();
213226
RequireDynamicallyAccessedMembers (ref reflectionContext, requiredMemberTypes, valueToStore, parameter);
227+
reflectionContext.Dispose ();
214228
}
215229
}
216230

@@ -910,36 +924,42 @@ public override bool HandleCall (MethodBody callingMethodBody, MethodReference c
910924
// GetType()
911925
//
912926
case IntrinsicId.Object_GetType: {
913-
// We could do better here if we start tracking the static types of values within the method body.
914-
// Right now, this can only analyze a couple cases for which we have static information for.
915-
TypeDefinition staticType = null;
916-
if (methodParams[0] is MethodParameterValue methodParam) {
917-
if (callingMethodDefinition.HasThis) {
918-
if (methodParam.ParameterIndex == 0) {
919-
staticType = callingMethodDefinition.DeclaringType;
920-
} else {
921-
staticType = callingMethodDefinition.Parameters[methodParam.ParameterIndex - 1].ParameterType.ResolveToMainTypeDefinition ();
922-
}
923-
} else {
924-
staticType = callingMethodDefinition.Parameters[methodParam.ParameterIndex].ParameterType.ResolveToMainTypeDefinition ();
925-
}
926-
} else if (methodParams[0] is LoadFieldValue loadedField) {
927-
staticType = loadedField.Field.FieldType.ResolveToMainTypeDefinition ();
928-
}
929-
930-
if (staticType != null) {
931-
// We can only analyze the Object.GetType call with the precise type if the type is sealed.
932-
// The type could be a descendant of the type in question, making us miss reflection.
933-
bool canUse = staticType.IsSealed;
927+
foreach (var valueNode in methodParams[0].UniqueValues ()) {
928+
TypeDefinition staticType = valueNode.StaticType;
929+
if (staticType is null) {
930+
// We don’t know anything about the type GetType was called on. Track this as a usual “result of a method call without any annotations”
931+
methodReturnValue = MergePointValue.MergeValues (methodReturnValue, new MethodReturnValue (calledMethod.MethodReturnType, DynamicallyAccessedMemberTypes.None));
932+
} else if (staticType.IsSealed || staticType.IsTypeOf ("System", "Delegate")) {
933+
// We can treat this one the same as if it was a typeof() expression
934934

935-
if (!canUse) {
936935
// We can allow Object.GetType to be modeled as System.Delegate because we keep all methods
937936
// on delegates anyway so reflection on something this approximation would miss is actually safe.
938-
canUse = staticType.IsTypeOf ("System", "Delegate");
939-
}
940937

941-
if (canUse) {
942-
methodReturnValue = new SystemTypeValue (staticType);
938+
// We ignore the fact that the type can be annotated (see below for handling of annotated types)
939+
// This means the annotations (if any) won't be applied - instead we rely on the exact knowledge
940+
// of the type. So for example even if the type is annotated with PublicMethods
941+
// but the code calls GetProperties on it - it will work - mark properties, don't mark methods
942+
// since we ignored the fact that it's annotated.
943+
// This can be seen a little bit as a violation of the annotation, but we already have similar cases
944+
// where a parameter is annotated and if something in the method sets a specific known type to it
945+
// we will also make it just work, even if the annotation doesn't match the usage.
946+
methodReturnValue = MergePointValue.MergeValues (methodReturnValue, new SystemTypeValue (staticType));
947+
} else {
948+
reflectionContext.AnalyzingPattern ();
949+
950+
// Make sure the type is marked (this will mark it as used via reflection, which is sort of true)
951+
// This should already be true for most cases (method params, fields, ...), but just in case
952+
MarkType (ref reflectionContext, staticType);
953+
954+
var annotation = _markStep.DynamicallyAccessedMembersTypeHierarchy
955+
.ApplyDynamicallyAccessedMembersToTypeHierarchy (this, ref reflectionContext, staticType);
956+
957+
reflectionContext.RecordHandledPattern ();
958+
959+
// Return a value which is "unknown type" with annotation. For now we'll use the return value node
960+
// for the method, which means we're loosing the information about which staticType this
961+
// started with. For now we don't need it, but we can add it later on.
962+
methodReturnValue = MergePointValue.MergeValues (methodReturnValue, new MethodReturnValue (calledMethod.MethodReturnType, annotation));
943963
}
944964
}
945965
}

0 commit comments

Comments
 (0)