Skip to content

Commit

Permalink
Add support for annotations in classes/interfaces/structs in the link…
Browse files Browse the repository at this point in the history
…er (dotnet/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]>

Commit migrated from dotnet/linker@6944b7c
  • Loading branch information
tlakollo authored Apr 9, 2021
1 parent 89e27a7 commit 3d549a2
Show file tree
Hide file tree
Showing 6 changed files with 1,270 additions and 122 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Mono.Cecil;
using Mono.Linker.Steps;

namespace Mono.Linker.Dataflow
{
class DynamicallyAccessedMembersTypeHierarchy
{
readonly LinkContext _context;
readonly MarkStep _markStep;

// Cache of DynamicallyAccessedMembers annotations applied to types and their hierarchies
// Values
// annotation - the aggregated annotation value from the entire base and interface hierarchy of the given type
// If the type has a base class with annotation a1 and an interface with annotation a2, the stored
// annotation is a1 | a2.
// applied - set to true once the annotation was applied to the type
// This only happens once the right reflection pattern is found.
// If a new type is being marked and one of its base types/interface has the applied set to true
// the new type will apply its annotation and will also set its applied to true.
// Non-interface types
// - Only marked types with non-empty annotation are put into the cache
// - Non-marked types are not stored in the cache
// - Marked types which are not in the cache don't have any annotation
// Interface types
// - All interface types accessible from marked types are stored in the cache
// - If the interface type doesn't have annotation the value None is stored here
//
// It's not possible to use the marking as a filter for interfaces in the cache
// because interfaces are marked late and in effectively random order.
// For this cache to be effective we need to be able to fill it for all base types and interfaces
// of a type which is currently being marked - at which point the interfaces are not yet marked.
readonly Dictionary<TypeDefinition, (DynamicallyAccessedMemberTypes annotation, bool applied)> _typesInDynamicallyAccessedMembersHierarchy;

public DynamicallyAccessedMembersTypeHierarchy (LinkContext context, MarkStep markStep)
{
_context = context;
_markStep = markStep;
_typesInDynamicallyAccessedMembersHierarchy = new Dictionary<TypeDefinition, (DynamicallyAccessedMemberTypes, bool)> ();
}

public (DynamicallyAccessedMemberTypes annotation, bool applied) ProcessMarkedTypeForDynamicallyAccessedMembersHierarchy (TypeDefinition type)
{
// Non-interfaces must be marked already
Debug.Assert (type.IsInterface || _context.Annotations.IsMarked (type));

DynamicallyAccessedMemberTypes annotation = _context.Annotations.FlowAnnotations.GetTypeAnnotation (type);
bool apply = false;

// We'll use the cache also as a way to detect and avoid recursion
// There's no possiblity to have recursion among base types, so only do this for interfaces
if (type.IsInterface) {
if (_typesInDynamicallyAccessedMembersHierarchy.TryGetValue (type, out var existingValue))
return existingValue;

_typesInDynamicallyAccessedMembersHierarchy.Add (type, (annotation, false));
}

// Base should already be marked (since we're marking its derived type now)
// so we should already have its cached values filled.
TypeDefinition baseType = type.BaseType?.Resolve ();
Debug.Assert (baseType == null || _context.Annotations.IsMarked (baseType));
if (baseType != null && _typesInDynamicallyAccessedMembersHierarchy.TryGetValue (baseType, out var baseValue)) {
annotation |= baseValue.annotation;
apply |= baseValue.applied;
}

// For the purposes of the DynamicallyAccessedMembers type hierarchies
// we consider interfaces of marked types to be also "marked" in that
// their annotations will be applied to the type regardless if later on
// we decide to remove the interface. This is to keep the complexity of the implementation
// relatively low. In the future it could be possibly optimized.
if (type.HasInterfaces) {
foreach (InterfaceImplementation iface in type.Interfaces) {
var interfaceType = iface.InterfaceType.Resolve ();
if (interfaceType != null) {
var interfaceValue = ProcessMarkedTypeForDynamicallyAccessedMembersHierarchy (interfaceType);
annotation |= interfaceValue.annotation;
apply |= interfaceValue.applied;
}
}
}

Debug.Assert (!apply || annotation != DynamicallyAccessedMemberTypes.None);

if (apply) {
// One of the base/interface types is already marked as having the annotation applied
// so we need to apply the annotation to this type as well
var reflectionMethodBodyScanner = new ReflectionMethodBodyScanner (_context, _markStep);
var reflectionPatternContext = new ReflectionPatternContext (_context, true, type, type);
reflectionMethodBodyScanner.ApplyDynamicallyAccessedMembersToType (ref reflectionPatternContext, type, annotation);
reflectionPatternContext.Dispose ();
}

// Store the results in the cache
// Don't store empty annotations for non-interface types - we can use the presence of the row
// in the cache as indication of it instead.
// This doesn't work for interfaces, since we can't rely on them being marked (and thus have the cache
// already filled), so we need to always store the row (even if empty) for interfaces.
if (annotation != DynamicallyAccessedMemberTypes.None || type.IsInterface) {
_typesInDynamicallyAccessedMembersHierarchy[type] = (annotation, apply);
}

return (annotation, apply);
}

public DynamicallyAccessedMemberTypes ApplyDynamicallyAccessedMembersToTypeHierarchy (
ReflectionMethodBodyScanner reflectionMethodBodyScanner,
ref ReflectionPatternContext reflectionPatternContext,
TypeDefinition type)
{
Debug.Assert (_context.Annotations.IsMarked (type));

// The type should be in our cache already
(var annotation, var applied) = GetCachedInfoForTypeInHierarchy (type);

// If the annotation was already applied to this type, there's no reason to repeat the operation, the result will
// be no change.
if (applied || annotation == DynamicallyAccessedMemberTypes.None)
return annotation;

// Apply the effective annotation for the type
reflectionMethodBodyScanner.ApplyDynamicallyAccessedMembersToType (ref reflectionPatternContext, type, annotation);

// Mark it as applied in the cache
_typesInDynamicallyAccessedMembersHierarchy[type] = (annotation, true);

// Propagate the newly applied annotation to all derived/implementation types
// Since we don't have a data structure which would allow us to enumerate all derived/implementation types
// walk all of the types in the cache. These are good candidates as types not in the cache don't apply.
foreach (var candidate in _typesInDynamicallyAccessedMembersHierarchy) {
if (candidate.Value.annotation == DynamicallyAccessedMemberTypes.None)
continue;

ApplyDynamicallyAccessedMembersToTypeHierarchyInner (reflectionMethodBodyScanner, ref reflectionPatternContext, candidate.Key);
}

return annotation;
}

bool ApplyDynamicallyAccessedMembersToTypeHierarchyInner (
ReflectionMethodBodyScanner reflectionMethodBodyScanner,
ref ReflectionPatternContext reflectionPatternContext,
TypeDefinition type)
{
(var annotation, var applied) = GetCachedInfoForTypeInHierarchy (type);

if (annotation == DynamicallyAccessedMemberTypes.None)
return false;

if (applied)
return true;

TypeDefinition baseType = type.BaseType?.Resolve ();
if (baseType != null)
applied = ApplyDynamicallyAccessedMembersToTypeHierarchyInner (reflectionMethodBodyScanner, ref reflectionPatternContext, baseType);

if (!applied && type.HasInterfaces) {
foreach (InterfaceImplementation iface in type.Interfaces) {
var interfaceType = iface.InterfaceType.Resolve ();
if (interfaceType != null) {
if (ApplyDynamicallyAccessedMembersToTypeHierarchyInner (reflectionMethodBodyScanner, ref reflectionPatternContext, interfaceType)) {
applied = true;
break;
}
}
}
}

if (applied) {
reflectionMethodBodyScanner.ApplyDynamicallyAccessedMembersToType (ref reflectionPatternContext, type, annotation);
_typesInDynamicallyAccessedMembersHierarchy[type] = (annotation, true);
}

return applied;
}

(DynamicallyAccessedMemberTypes annotation, bool applied) GetCachedInfoForTypeInHierarchy (TypeDefinition type)
{
Debug.Assert (type.IsInterface || _context.Annotations.IsMarked (type));

// The type should be in our cache already
if (!_typesInDynamicallyAccessedMembersHierarchy.TryGetValue (type, out var existingValue)) {
// If it's not in the cache it should be a non-interface type in which case it means there were no annotations
Debug.Assert (!type.IsInterface);
return (DynamicallyAccessedMemberTypes.None, false);
}

return existingValue;
}
}
}
18 changes: 15 additions & 3 deletions src/tools/illink/src/linker/Linker.Dataflow/FlowAnnotations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ public DynamicallyAccessedMemberTypes GetFieldAnnotation (FieldDefinition field)
return DynamicallyAccessedMemberTypes.None;
}

public DynamicallyAccessedMemberTypes GetTypeAnnotation (TypeDefinition type)
{
return GetAnnotations (type).TypeAnnotation;
}

public DynamicallyAccessedMemberTypes GetGenericParameterAnnotation (GenericParameter genericParameter)
{
TypeDefinition declaringType = genericParameter.DeclaringType?.Resolve ();
Expand Down Expand Up @@ -122,6 +127,9 @@ DynamicallyAccessedMemberTypes GetMemberTypesForDynamicallyAccessedMembersAttrib

TypeAnnotations BuildTypeAnnotations (TypeDefinition type)
{
// class, interface, struct can have annotations
DynamicallyAccessedMemberTypes typeAnnotation = GetMemberTypesForDynamicallyAccessedMembersAttribute (type);

var annotatedFields = new ArrayBuilder<FieldAnnotation> ();

// First go over all fields with an explicit annotation
Expand Down Expand Up @@ -338,7 +346,7 @@ TypeAnnotations BuildTypeAnnotations (TypeDefinition type)
}
}

return new TypeAnnotations (type, annotatedMethods.ToArray (), annotatedFields.ToArray (), typeGenericParameterAnnotations);
return new TypeAnnotations (type, typeAnnotation, annotatedMethods.ToArray (), annotatedFields.ToArray (), typeGenericParameterAnnotations);
}

static bool ScanMethodBodyForFieldAccess (MethodBody body, bool write, out FieldDefinition found)
Expand Down Expand Up @@ -518,17 +526,21 @@ void LogValidationWarning (IMetadataTokenProvider provider, IMetadataTokenProvid
readonly struct TypeAnnotations
{
readonly TypeDefinition _type;
readonly DynamicallyAccessedMemberTypes _typeAnnotation;
readonly MethodAnnotations[] _annotatedMethods;
readonly FieldAnnotation[] _annotatedFields;
readonly DynamicallyAccessedMemberTypes[] _genericParameterAnnotations;

public TypeAnnotations (
TypeDefinition type,
DynamicallyAccessedMemberTypes typeAnnotation,
MethodAnnotations[] annotatedMethods,
FieldAnnotation[] annotatedFields,
DynamicallyAccessedMemberTypes[] genericParameterAnnotations)
=> (_type, _annotatedMethods, _annotatedFields, _genericParameterAnnotations)
= (type, annotatedMethods, annotatedFields, genericParameterAnnotations);
=> (_type, _typeAnnotation, _annotatedMethods, _annotatedFields, _genericParameterAnnotations)
= (type, typeAnnotation, annotatedMethods, annotatedFields, genericParameterAnnotations);

public DynamicallyAccessedMemberTypes TypeAnnotation { get => _typeAnnotation; }

public bool TryGetAnnotation (MethodDefinition method, out MethodAnnotations annotations)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public void ScanAndProcessReturnValue (MethodBody methodBody)
var reflectionContext = new ReflectionPatternContext (_context, ShouldEnableReflectionPatternReporting (method), method, method.MethodReturnType);
reflectionContext.AnalyzingPattern ();
RequireDynamicallyAccessedMembers (ref reflectionContext, requiredMemberTypes, MethodReturnValue, method.MethodReturnType);
reflectionContext.Dispose ();
}
}
}
Expand All @@ -92,6 +93,7 @@ public void ProcessAttributeDataflow (IMemberDefinition source, MethodDefinition
var reflectionContext = new ReflectionPatternContext (_context, true, source, methodParameter);
reflectionContext.AnalyzingPattern ();
RequireDynamicallyAccessedMembers (ref reflectionContext, annotation, valueNode, methodParameter);
reflectionContext.Dispose ();
}
}
}
Expand All @@ -105,6 +107,15 @@ public void ProcessAttributeDataflow (IMemberDefinition source, FieldDefinition
var reflectionContext = new ReflectionPatternContext (_context, true, source, field);
reflectionContext.AnalyzingPattern ();
RequireDynamicallyAccessedMembers (ref reflectionContext, annotation, valueNode, field);
reflectionContext.Dispose ();
}

public void ApplyDynamicallyAccessedMembersToType (ref ReflectionPatternContext reflectionPatternContext, TypeDefinition type, DynamicallyAccessedMemberTypes annotation)
{
Debug.Assert (annotation != DynamicallyAccessedMemberTypes.None);

reflectionPatternContext.AnalyzingPattern ();
MarkTypeForDynamicallyAccessedMembers (ref reflectionPatternContext, type, annotation);
}

static ValueNode GetValueNodeForCustomAttributeArgument (CustomAttributeArgument argument)
Expand Down Expand Up @@ -138,6 +149,7 @@ public void ProcessGenericArgumentDataFlow (GenericParameter genericParameter, T
var reflectionContext = new ReflectionPatternContext (_context, enableReflectionPatternReporting, source, genericParameter);
reflectionContext.AnalyzingPattern ();
RequireDynamicallyAccessedMembers (ref reflectionContext, annotation, valueNode, genericParameter);
reflectionContext.Dispose ();
}

ValueNode GetTypeValueNodeFromGenericArgument (TypeReference genericArgument)
Expand Down Expand Up @@ -173,7 +185,7 @@ protected override void WarnAboutInvalidILInMethod (MethodBody method, int ilOff
protected override ValueNode GetMethodParameterValue (MethodDefinition method, int parameterIndex)
{
DynamicallyAccessedMemberTypes memberTypes = _context.Annotations.FlowAnnotations.GetParameterAnnotation (method, parameterIndex);
return new MethodParameterValue (parameterIndex, memberTypes, DiagnosticUtilities.GetMethodParameterFromIndex (method, parameterIndex));
return new MethodParameterValue (method, parameterIndex, memberTypes, DiagnosticUtilities.GetMethodParameterFromIndex (method, parameterIndex));
}

protected override ValueNode GetFieldValue (MethodDefinition method, FieldDefinition field)
Expand All @@ -200,6 +212,7 @@ protected override void HandleStoreField (MethodDefinition method, FieldDefiniti
var reflectionContext = new ReflectionPatternContext (_context, ShouldEnableReflectionPatternReporting (method), method, field, operation);
reflectionContext.AnalyzingPattern ();
RequireDynamicallyAccessedMembers (ref reflectionContext, requiredMemberTypes, valueToStore, field);
reflectionContext.Dispose ();
}
}

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

Expand Down Expand Up @@ -910,36 +924,42 @@ public override bool HandleCall (MethodBody callingMethodBody, MethodReference c
// GetType()
//
case IntrinsicId.Object_GetType: {
// We could do better here if we start tracking the static types of values within the method body.
// Right now, this can only analyze a couple cases for which we have static information for.
TypeDefinition staticType = null;
if (methodParams[0] is MethodParameterValue methodParam) {
if (callingMethodDefinition.HasThis) {
if (methodParam.ParameterIndex == 0) {
staticType = callingMethodDefinition.DeclaringType;
} else {
staticType = callingMethodDefinition.Parameters[methodParam.ParameterIndex - 1].ParameterType.ResolveToMainTypeDefinition ();
}
} else {
staticType = callingMethodDefinition.Parameters[methodParam.ParameterIndex].ParameterType.ResolveToMainTypeDefinition ();
}
} else if (methodParams[0] is LoadFieldValue loadedField) {
staticType = loadedField.Field.FieldType.ResolveToMainTypeDefinition ();
}

if (staticType != null) {
// We can only analyze the Object.GetType call with the precise type if the type is sealed.
// The type could be a descendant of the type in question, making us miss reflection.
bool canUse = staticType.IsSealed;
foreach (var valueNode in methodParams[0].UniqueValues ()) {
TypeDefinition staticType = valueNode.StaticType;
if (staticType is null) {
// 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”
methodReturnValue = MergePointValue.MergeValues (methodReturnValue, new MethodReturnValue (calledMethod.MethodReturnType, DynamicallyAccessedMemberTypes.None));
} else if (staticType.IsSealed || staticType.IsTypeOf ("System", "Delegate")) {
// We can treat this one the same as if it was a typeof() expression

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

if (canUse) {
methodReturnValue = new SystemTypeValue (staticType);
// We ignore the fact that the type can be annotated (see below for handling of annotated types)
// This means the annotations (if any) won't be applied - instead we rely on the exact knowledge
// of the type. So for example even if the type is annotated with PublicMethods
// but the code calls GetProperties on it - it will work - mark properties, don't mark methods
// since we ignored the fact that it's annotated.
// This can be seen a little bit as a violation of the annotation, but we already have similar cases
// where a parameter is annotated and if something in the method sets a specific known type to it
// we will also make it just work, even if the annotation doesn't match the usage.
methodReturnValue = MergePointValue.MergeValues (methodReturnValue, new SystemTypeValue (staticType));
} else {
reflectionContext.AnalyzingPattern ();

// Make sure the type is marked (this will mark it as used via reflection, which is sort of true)
// This should already be true for most cases (method params, fields, ...), but just in case
MarkType (ref reflectionContext, staticType);

var annotation = _markStep.DynamicallyAccessedMembersTypeHierarchy
.ApplyDynamicallyAccessedMembersToTypeHierarchy (this, ref reflectionContext, staticType);

reflectionContext.RecordHandledPattern ();

// Return a value which is "unknown type" with annotation. For now we'll use the return value node
// for the method, which means we're loosing the information about which staticType this
// started with. For now we don't need it, but we can add it later on.
methodReturnValue = MergePointValue.MergeValues (methodReturnValue, new MethodReturnValue (calledMethod.MethodReturnType, annotation));
}
}
}
Expand Down
Loading

0 comments on commit 3d549a2

Please sign in to comment.