Skip to content

Commit 0d803c3

Browse files
authored
Use compiler-lowered DAM when trimming (#117624)
This uses compiler-lowered DAM for trimming compiler-generated code, and adds test infrastructure and coverage for DynamicallyAccessedMembers that is lowered by us for the compiler. We also want to use the compiler-lowered attributes for generated state machines, when trimming a .NET10+ assembly (leaving the old heuristics for .NET9 and below, to avoid breaking trim analysis for existing assemblies). However, the tests uncovered an issue with the Roslyn implementation: dotnet/roslyn#79333. Until that is fixed, we don't rely on the lowered attribute for mapping type parameters by default (the new behavior is opt-in under a new flag). Once we get a fix we can remove this. The old heuristics are still run for all TFMs simply for coverage, but we don't use the result. This adds test infrastructure to let us test against a polyfilled DAM with CompilerLoweringPreserve (since we don't run ILLink/ILC tests against a live corelib). The existing ILLink tests (including those shared with ILC trimming tests) are now built with TargetFrameworkAttribute marking the assembly as targeting .NET 10. The compiler-generated code tests have been duplicated for .NET 9 and .NET 10 to test both the old and new behaviors. Also fixes a test infrastructure issue where in a "copy" assembly, the DAM and CompilerLoweringPreserve types that are compiled in are kept, but the test infrastructure was filtering these out of the "linkedMembers", leading to validation errors.
1 parent 757fb9d commit 0d803c3

File tree

45 files changed

+625
-103
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+625
-103
lines changed

docs/design/tools/illink/compiler-generated-code-handling.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,24 @@ static IEnumerable<int> TestLocalVariable ()
9595
}
9696
```
9797

98+
## Attribute propagation via CompilerLoweringPreserveAttribute
99+
100+
To address the challenges of propagating user-authored attributes to compiler-generated code, .NET 10 introduced a general mechanism: `[CompilerLoweringPreserveAttribute]`. This attribute can be applied to other attribute types to instruct compilers to propagate those attributes to compiler-generated code.
101+
102+
`DynamicallyAccessedMembersAttribute` is now marked with `[CompilerLoweringPreserve]`, so when the compiler generates new fields or type parameters (such as for local functions, iterator/async state machines, or primary constructor parameters), the relevant `DynamicallyAccessedMembers` annotations are automatically applied to the generated members. This allows trimming tools to directly use the annotations present in the generated code, without needing to reverse-engineer the mapping to user code.
103+
104+
### .NET 10 and later
105+
106+
For .NET 10 and later, trimming tools should rely on the compiler to propagate attributes such as `DynamicallyAccessedMembersAttribute` to all relevant compiler-generated code, as indicated by `[CompilerLoweringPreserve]`. No heuristics are needed for these assemblies. This isn't perfect because it's possible for such assemblies to be compiled with new Roslyn versions that could use different lowering strategies, so it's possible that the existing heuristics will break for new releases of a pre-`net10.0` assembly.
107+
108+
To mitigate this there are a few options:
109+
110+
1. Multitarget the library to `net10.0` (so that it is built with the new `CompilerLoweringPreserve` behavior and will avoid the heuristics)
111+
2. Fix the heuristics to work for code produced by new Roslyn versions
112+
3. The trimming tools could detect the presence of a polyfilled `DynamicallyAccessedMembersAttribute` type with `CompilerLoweringPreserve`. When present this would turn off the heuristics for the containing assembly.
113+
114+
Another issue is that .NET 10 libraries might be built with `<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>`, and the tooling would not be able to detect the TargetFramework. Aside from setting `<GenerateTargetFrameworkAttribute>true</GenerateTargetFrameworkAttribute>`, mitigations 1. and 2. above would also apply to this scenario.
115+
98116
### Compiler dependent behavior
99117

100118
Since the problems are all caused by compiler generated code, the behaviors depend on the specific compiler in use. The main focus of this document is the Roslyn C# compiler right now. Mainly since it's by far the most used compiler for .NET code. That said, we would like to design the solution in such a way that other compilers using similar patterns could also benefit from it.
@@ -560,9 +578,9 @@ and fields on the closure types.
560578
### Long term solution
561579

562580
Detecting which compiler generated items are used by any given user method is currently relatively tricky.
563-
There's no definitive marker in the IL which would let the trimmer confidently determine this information.
564-
Good long term solution will need the compilers to produce some kind of marker in the IL so that
565-
static analysis tools can reliably detect all of the compiler generated items.
581+
There's no definitive marker in the IL which would let the trimmer confidently determine this information
582+
for all of the above cases. Good long term solution will need the compilers to produce some kind of marker
583+
in the IL so that static analysis tools can reliably detect all of the compiler generated items.
566584

567585
This ask can be described as:
568586
For a given user method, ability to determine all of the items (methods, fields, types, IL code) which were
@@ -573,6 +591,10 @@ helpers and other infrastructure which may be needed but is not directly attribu
573591

574592
This should be enough to implement solutions for both suppression propagation and data flow analysis.
575593

594+
For `DynamicallyAccessedMembersAttribute`, we have a long-term solution that relies on the
595+
`[CompilerLoweringPreserve]` attribute, which tells Roslyn to propagate `DynamicallyAccessedMembers`
596+
annotations to compiler-generated code.
597+
576598
### Possible short term solution
577599

578600
#### Heuristic based solution

src/coreclr/tools/aot/ILCompiler.Compiler.Tests/DependencyGraphTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public void TestDependencyGraphInvariants(EcmaMethod method)
6767
CompilationModuleGroup compilationGroup = new SingleFileCompilationModuleGroup();
6868

6969
NativeAotILProvider ilProvider = new NativeAotILProvider();
70-
CompilerGeneratedState compilerGeneratedState = new CompilerGeneratedState(ilProvider, Logger.Null);
70+
CompilerGeneratedState compilerGeneratedState = new CompilerGeneratedState(ilProvider, Logger.Null, disableGeneratedCodeHeuristics: true);
7171

7272
UsageBasedMetadataManager metadataManager = new UsageBasedMetadataManager(compilationGroup, context,
7373
new FullyBlockedMetadataBlockingPolicy(), new FullyBlockedManifestResourceBlockingPolicy(),
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
4+
using System;
5+
using System.Diagnostics;
6+
using System.Reflection.Metadata;
7+
using Internal.TypeSystem;
8+
using Internal.TypeSystem.Ecma;
9+
10+
#nullable enable
11+
12+
namespace ILCompiler
13+
{
14+
public static class AssemblyExtensions
15+
{
16+
public static Version? GetTargetFrameworkVersion(this EcmaAssembly assembly)
17+
{
18+
// Get the custom attributes from the assembly's metadata
19+
MetadataReader reader = assembly.MetadataReader;
20+
CustomAttributeHandle attrHandle = reader.GetCustomAttributeHandle(assembly.AssemblyDefinition.GetCustomAttributes(),
21+
"System.Runtime.Versioning", "TargetFrameworkAttribute");
22+
if (!attrHandle.IsNil)
23+
{
24+
CustomAttribute attr = reader.GetCustomAttribute(attrHandle);
25+
CustomAttributeValue<TypeDesc> decoded = attr.DecodeValue(new CustomAttributeTypeProvider(assembly));
26+
if (decoded.FixedArguments.Length == 1 && decoded.FixedArguments[0].Value is string tfm && !string.IsNullOrEmpty(tfm))
27+
{
28+
var versionPrefix = "Version=v";
29+
var idx = tfm.IndexOf(versionPrefix);
30+
if (idx >= 0)
31+
{
32+
var versionStr = tfm.Substring(idx + versionPrefix.Length);
33+
if (Version.TryParse(versionStr, out var version))
34+
return version;
35+
}
36+
}
37+
}
38+
return null;
39+
}
40+
}
41+
}

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/CompilerGeneratedState.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,20 @@ public class CompilerGeneratedState
2424
private readonly record struct TypeArgumentInfo(
2525
/// <summary>The method which calls the ctor for the given type</summary>
2626
MethodDesc CreatingMethod,
27-
/// <summary>Attributes for the type, pulled from the creators type arguments</summary>
27+
/// <summary>Generic parameters of the creator used as type arguments for the type</summary>
2828
IReadOnlyList<GenericParameterDesc?>? OriginalAttributes);
2929

3030
private readonly TypeCacheHashtable _typeCacheHashtable;
3131

3232
private readonly Logger _logger;
3333

34-
public CompilerGeneratedState(ILProvider ilProvider, Logger logger)
34+
private readonly bool _disableGeneratedCodeHeuristics;
35+
36+
public CompilerGeneratedState(ILProvider ilProvider, Logger logger, bool disableGeneratedCodeHeuristics)
3537
{
3638
_typeCacheHashtable = new TypeCacheHashtable(ilProvider);
3739
_logger = logger;
40+
_disableGeneratedCodeHeuristics = disableGeneratedCodeHeuristics;
3841
}
3942

4043
private sealed class TypeCacheHashtable : LockFreeReaderHashtable<MetadataType, TypeCache>
@@ -659,6 +662,16 @@ public bool TryGetCompilerGeneratedCalleesForUserMethod(MethodDesc method, [NotN
659662
MetadataType generatedType = (MetadataType)type.GetTypeDefinition();
660663
Debug.Assert(CompilerGeneratedNames.IsStateMachineOrDisplayClass(generatedType.Name));
661664

665+
// Avoid the heuristics for .NET10+, where DynamicallyAccessedMembers flows to generated code
666+
// because it is annotated with CompilerLoweringPreserveAttribute.
667+
if (_disableGeneratedCodeHeuristics &&
668+
generatedType.Module.Assembly is EcmaAssembly asm && asm.GetTargetFrameworkVersion() >= new Version(10, 0))
669+
{
670+
// Still run the logic for coverage to help us find bugs, but don't use the result.
671+
GetCompilerGeneratedStateForType(generatedType);
672+
return null;
673+
}
674+
662675
var typeCache = GetCompilerGeneratedStateForType(generatedType);
663676
if (typeCache is null)
664677
return null;

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Logger.cs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public class Logger
3737
private readonly bool _treatWarningsAsErrors;
3838
private readonly Dictionary<int, bool> _warningsAsErrors;
3939

40-
public static Logger Null = new Logger(new TextLogWriter(TextWriter.Null), null, false);
40+
public static Logger Null = new Logger(new TextLogWriter(TextWriter.Null), null, false, true);
4141

4242
public bool IsVerbose { get; }
4343

@@ -51,7 +51,8 @@ public Logger(
5151
IEnumerable<string> singleWarnDisabledModules,
5252
IEnumerable<string> suppressedCategories,
5353
bool treatWarningsAsErrors,
54-
IDictionary<int, bool> warningsAsErrors)
54+
IDictionary<int, bool> warningsAsErrors,
55+
bool disableGeneratedCodeHeuristics)
5556
{
5657
_logWriter = writer;
5758
IsVerbose = isVerbose;
@@ -62,22 +63,22 @@ public Logger(
6263
_suppressedCategories = new HashSet<string>(suppressedCategories, StringComparer.Ordinal);
6364
_treatWarningsAsErrors = treatWarningsAsErrors;
6465
_warningsAsErrors = new Dictionary<int, bool>(warningsAsErrors);
65-
_compilerGeneratedState = ilProvider == null ? null : new CompilerGeneratedState(ilProvider, this);
66+
_compilerGeneratedState = ilProvider == null ? null : new CompilerGeneratedState(ilProvider, this, disableGeneratedCodeHeuristics);
6667
_unconditionalSuppressMessageAttributeState = new UnconditionalSuppressMessageAttributeState(_compilerGeneratedState, this);
6768
}
6869

69-
public Logger(TextWriter writer, ILProvider ilProvider, bool isVerbose, IEnumerable<int> suppressedWarnings, bool singleWarn, IEnumerable<string> singleWarnEnabledModules, IEnumerable<string> singleWarnDisabledModules, IEnumerable<string> suppressedCategories, bool treatWarningsAsErrors, IDictionary<int, bool> warningsAsErrors)
70-
: this(new TextLogWriter(writer), ilProvider, isVerbose, suppressedWarnings, singleWarn, singleWarnEnabledModules, singleWarnDisabledModules, suppressedCategories, treatWarningsAsErrors, warningsAsErrors)
70+
public Logger(TextWriter writer, ILProvider ilProvider, bool isVerbose, IEnumerable<int> suppressedWarnings, bool singleWarn, IEnumerable<string> singleWarnEnabledModules, IEnumerable<string> singleWarnDisabledModules, IEnumerable<string> suppressedCategories, bool treatWarningsAsErrors, IDictionary<int, bool> warningsAsErrors, bool disableGeneratedCodeHeuristics)
71+
: this(new TextLogWriter(writer), ilProvider, isVerbose, suppressedWarnings, singleWarn, singleWarnEnabledModules, singleWarnDisabledModules, suppressedCategories, treatWarningsAsErrors, warningsAsErrors, disableGeneratedCodeHeuristics)
7172
{
7273
}
7374

74-
public Logger(ILogWriter writer, ILProvider ilProvider, bool isVerbose)
75-
: this(writer, ilProvider, isVerbose, Array.Empty<int>(), singleWarn: false, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), false, new Dictionary<int, bool>())
75+
public Logger(ILogWriter writer, ILProvider ilProvider, bool isVerbose, bool disableGeneratedCodeHeuristics)
76+
: this(writer, ilProvider, isVerbose, Array.Empty<int>(), singleWarn: false, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), false, new Dictionary<int, bool>(), disableGeneratedCodeHeuristics)
7677
{
7778
}
7879

79-
public Logger(TextWriter writer, ILProvider ilProvider, bool isVerbose)
80-
: this(new TextLogWriter(writer), ilProvider, isVerbose)
80+
public Logger(TextWriter writer, ILProvider ilProvider, bool isVerbose, bool disableGeneratedCodeHeuristics)
81+
: this(new TextLogWriter(writer), ilProvider, isVerbose, disableGeneratedCodeHeuristics)
8182
{
8283
}
8384

src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@
343343

344344
<Compile Include="Compiler\AnalysisBasedInteropStubManager.cs" />
345345
<Compile Include="Compiler\AnalysisBasedMetadataManager.cs" />
346+
<Compile Include="Compiler\AssemblyExtensions.cs" />
346347
<Compile Include="Compiler\BodySubstitution.cs" />
347348
<Compile Include="Compiler\BodySubstitutionParser.cs" />
348349
<Compile Include="Compiler\DependencyAnalysis\AddressTakenMethodNode.cs" />

src/coreclr/tools/aot/ILCompiler.Trimming.Tests/ILCompiler.Trimming.Tests.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@
5050
<RuntimeHostConfigurationOption Include="Mono.Linker.Tests.TargetFramework">
5151
<Value>$(TargetFramework)</Value>
5252
</RuntimeHostConfigurationOption>
53+
<RuntimeHostConfigurationOption Include="Mono.Linker.Tests.TargetFrameworkMoniker">
54+
<Value>$(TargetFrameworkMoniker)</Value>
55+
</RuntimeHostConfigurationOption>
56+
<RuntimeHostConfigurationOption Include="Mono.Linker.Tests.TargetFrameworkMonikerDisplayName">
57+
<Value>$(TargetFrameworkMonikerDisplayName)</Value>
58+
</RuntimeHostConfigurationOption>
5359
<RuntimeHostConfigurationOption Include="Mono.Linker.Tests.LinkerTestDir">
5460
<Value>$(ToolsProjectRoot)illink/test/</Value>
5561
</RuntimeHostConfigurationOption>

src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/AssemblyChecker.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ class LinkedMethodEntity : LinkedEntity
5959
"<Module>.StartupCodeMain(Int32,IntPtr)",
6060
"<Module>.MainMethodWrapper()",
6161
"<Module>.MainMethodWrapper(String[])",
62+
"System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute.__GetFieldHelper(Int32,MethodTable*&)",
63+
"System.Runtime.InteropServices.TypeMapping",
64+
"System.Runtime.InteropServices.TypeMapping.GetOrCreateExternalTypeMapping<TTypeMapGroup>()",
65+
"System.Runtime.InteropServices.TypeMapping.GetOrCreateProxyTypeMapping<TTypeMapGroup>()",
6266

6367
// Ignore compiler generated code which can't be reasonably matched to the source method
6468
"<PrivateImplementationDetails>",
@@ -102,7 +106,7 @@ IEnumerable<string> VerifyImpl()
102106

103107
// TODO - this is mostly attribute verification
104108
// foreach (var originalModule in originalAssembly.Modules)
105-
// VerifyModule(originalModule, linkedAssembly.Modules.FirstOrDefault(m => m.Name == originalModule.Name));
109+
// VerifyModule(originalModule, linkedAssembly.Modules.FirstOrDefault (m => m.Name == originalModule.Name));
106110

107111
// TODO
108112
// VerifyResources(originalAssembly, linkedAssembly);
@@ -291,12 +295,9 @@ static bool ShouldIncludeType(TypeDesc type)
291295
if (metadataType.Namespace.StartsWith("Internal"))
292296
return false;
293297

294-
// Simple way to filter out system assemblies - the best way would be to get a list
295-
// of input/reference assemblies and filter on that, but it's tricky and this should work for basically everything
296-
if (metadataType.Namespace.StartsWith("System"))
298+
if (metadataType.Module.Assembly is EcmaAssembly asm && asm.Assembly.GetName().Name == "System.Private.CoreLib")
297299
return false;
298300

299-
300301
return ShouldIncludeEntityByDisplayName(type);
301302
}
302303

@@ -2059,7 +2060,10 @@ private IEnumerable<string> VerifyKeptAllTypesAndMembersInAssembly(string assemb
20592060
var missingInLinked = originalTypes.Keys.Except(linkedTypes.Keys);
20602061

20612062
if (missingInLinked.Any())
2063+
{
20622064
yield return $"Expected all types to exist in the linked assembly {assemblyName}, but one or more were missing";
2065+
yield break;
2066+
}
20632067

20642068
foreach (var originalKvp in originalTypes)
20652069
{

src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/ILCompilerOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ public class ILCompilerOptions
2020
public bool TreatWarningsAsErrors;
2121
public Dictionary<int, bool> WarningsAsErrors = new Dictionary<int, bool>();
2222
public List<string> SuppressedWarningCategories = new List<string>();
23+
public bool DisableGeneratedCodeHeuristics;
2324
}
2425
}

src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/ResultChecker.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,17 @@ public virtual void Check(TrimmedTestCaseResult testResult)
7878
{
7979
_originalsResolver.Dispose();
8080
}
81+
}
8182

82-
bool HasActiveSkipKeptItemsValidationAttribute(ICustomAttributeProvider provider)
83+
internal static bool HasActiveSkipKeptItemsValidationAttribute(ICustomAttributeProvider provider)
84+
{
85+
if (TryGetCustomAttribute(provider, nameof(SkipKeptItemsValidationAttribute), out var attribute))
8386
{
84-
if (TryGetCustomAttribute(provider, nameof(SkipKeptItemsValidationAttribute), out var attribute))
85-
{
86-
object? by = attribute.GetPropertyValue(nameof(SkipKeptItemsValidationAttribute.By));
87-
return by is null ? true : ((Tool)by).HasFlag(Tool.NativeAot);
88-
}
89-
90-
return false;
87+
object? by = attribute.GetPropertyValue(nameof(SkipKeptItemsValidationAttribute.By));
88+
return by is null ? true : ((Tool)by).HasFlag(Tool.NativeAot);
9189
}
90+
91+
return false;
9292
}
9393

9494
protected virtual AssemblyChecker CreateAssemblyChecker(AssemblyDefinition original, TrimmedTestCaseResult testResult)

0 commit comments

Comments
 (0)