diff --git a/src/tools/illink/src/linker/Linker.Steps/MarkStep.cs b/src/tools/illink/src/linker/Linker.Steps/MarkStep.cs index 1cccdecc476ae9..611af2c59cdf87 100644 --- a/src/tools/illink/src/linker/Linker.Steps/MarkStep.cs +++ b/src/tools/illink/src/linker/Linker.Steps/MarkStep.cs @@ -1488,6 +1488,9 @@ public virtual void MarkAssembly(AssemblyDefinition assembly, DependencyInfo rea if (CheckProcessed(assembly)) return; + // Flush any TypeMapAssemblyTarget attributes that were waiting for this assembly to be marked. + _typeMapHandler.TriggerPendingAssemblyTargets(assembly); + var assemblyOrigin = new MessageOrigin(assembly); EmbeddedXmlInfo.ProcessDescriptors(assembly, Context); diff --git a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs index 2b1d2e4a55e8c4..013e7708d097f9 100644 --- a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs +++ b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs @@ -28,7 +28,16 @@ sealed class TypeMapHandler // [type map group: custom attributes] Dictionary> _pendingExternalTypeMapEntries = null!; Dictionary> _pendingProxyTypeMapEntries = null!; - Dictionary> _pendingAssemblyTargets = null!; + + // [type map group: (custom attribute, resolved target assembly)] + // The resolved target assembly is null if it could not be found. In that case, the attribute will not be marked. + Dictionary> _pendingAssemblyTargets = null!; + + // [target assembly: (type map group, custom attribute, calling method)] + // When a type map group is seen, assembly targets whose referenced assembly has not yet been marked are moved here. + // When the referenced assembly is eventually marked (due to a TypeMap entry being marked), all pending entries for + // it are also marked. + Dictionary> _pendingAssemblyTargetsByAssembly = null!; HashSet _referencedExternalTypeMaps = null!; HashSet _referencedProxyTypeMaps = null!; @@ -52,6 +61,7 @@ public void Initialize(LinkContext context, MarkStep markStep, AssemblyDefinitio _pendingExternalTypeMapEntries = new(typeReferenceEqualityComparer); _pendingProxyTypeMapEntries = new(typeReferenceEqualityComparer); _pendingAssemblyTargets = new(typeReferenceEqualityComparer); + _pendingAssemblyTargetsByAssembly = []; _referencedExternalTypeMaps = new(typeReferenceEqualityComparer); _referencedProxyTypeMaps = new(typeReferenceEqualityComparer); var typeMapResolver = new TypeMapResolver(entryPointAssembly); @@ -70,12 +80,11 @@ public void ProcessExternalTypeMapGroupSeen(MethodDefinition callingMethod, Type MarkTypeMapAttribute(entry, new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod)); } } - if (_pendingAssemblyTargets.Remove(typeMapGroup, out List? assemblyTargets)) + if (_pendingAssemblyTargets.Remove(typeMapGroup, out List<(CustomAttributeWithOrigin Attr, AssemblyDefinition? TargetAssembly)>? assemblyTargets)) { - foreach (var entry in assemblyTargets) + foreach (var (entry, targetAssembly) in assemblyTargets) { - var info = new DependencyInfo(DependencyKind.TypeMapAssemblyTarget, callingMethod); - MarkTypeMapAttribute(entry, info); + MarkAssemblyTargetIfReady(typeMapGroup, entry, targetAssembly, callingMethod); } } } @@ -91,12 +100,11 @@ public void ProcessProxyTypeMapGroupSeen(MethodDefinition callingMethod, TypeRef MarkTypeMapAttribute(entry, new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod)); } } - if (_pendingAssemblyTargets.Remove(typeMapGroup, out List? assemblyTargets)) + if (_pendingAssemblyTargets.Remove(typeMapGroup, out List<(CustomAttributeWithOrigin Attr, AssemblyDefinition? TargetAssembly)>? assemblyTargets)) { - foreach (var entry in assemblyTargets) + foreach (var (entry, targetAssembly) in assemblyTargets) { - var info = new DependencyInfo(DependencyKind.TypeMapAssemblyTarget, callingMethod); - MarkTypeMapAttribute(entry, info); + MarkAssemblyTargetIfReady(typeMapGroup, entry, targetAssembly, callingMethod); } } } @@ -112,6 +120,41 @@ void MarkTypeMapAttribute(CustomAttributeWithOrigin entry, DependencyInfo info) _markStep.MarkRequirementsForInstantiatedTypes(targetTypeDef); } + void MarkAssemblyTargetIfReady(TypeReference typeMapGroup, CustomAttributeWithOrigin entry, AssemblyDefinition? targetAssembly, MethodDefinition? callingMethod) + { + // If the target assembly could not be resolved (it is not present in the linker input), + // the attribute cannot safely be kept: at runtime Assembly.Load would throw + // FileNotFoundException. Drop it silently; the assembly simply does not participate + // in the type map. + if (targetAssembly is null) + return; + + if (_context.Annotations.IsMarked(targetAssembly)) + { + // Target assembly is already marked. Ideally we would only keep the attribute when the + // assembly has at least one surviving TypeMap/TypeMapAssociation entry, but checking that + // here would add complexity for a narrow case. We accept this slight over-approximation. + MarkTypeMapAttribute(entry, new DependencyInfo(DependencyKind.TypeMapAssemblyTarget, callingMethod)); + } + else + { + // Target assembly is not yet marked. Defer: mark when (if) the assembly eventually gets marked. + _pendingAssemblyTargetsByAssembly.AddToList(targetAssembly, (typeMapGroup, entry, callingMethod)); + } + } + + // Called from MarkStep.MarkAssembly whenever an assembly is first marked (for any reason). + // This ensures that pending TypeMapAssemblyTarget attributes are flushed regardless of + // the order in which assemblies are visited. + internal void TriggerPendingAssemblyTargets(AssemblyDefinition newlyMarkedAssembly) + { + if (!_pendingAssemblyTargetsByAssembly.Remove(newlyMarkedAssembly, out List<(TypeReference Group, CustomAttributeWithOrigin Attr, MethodDefinition? CallingMethod)>? waiting)) + return; + + foreach (var (_, attr, callingMethod) in waiting) + MarkTypeMapAttribute(attr, new DependencyInfo(DependencyKind.TypeMapAssemblyTarget, callingMethod)); + } + public void ProcessType(TypeDefinition definition) { EnsureInitialized(); @@ -192,21 +235,16 @@ static TypeReference UnwrapToResolvableType(TypeReference type) return type; } - private void AddAssemblyTarget(TypeReference typeMapGroup, CustomAttributeWithOrigin attr) + private void AddAssemblyTarget(TypeReference typeMapGroup, CustomAttributeWithOrigin attr, AssemblyDefinition? resolvedTargetAssembly) { // Validate attribute if (attr.Attribute.ConstructorArguments is not ([{ Value: string }])) return; - // If the type map group has been seen, mark the attribute immediately - if (_referencedExternalTypeMaps.Contains(typeMapGroup) || _referencedProxyTypeMaps.Contains(typeMapGroup)) - { - _markStep.MarkCustomAttribute(attr.Attribute, new DependencyInfo(DependencyKind.TypeMapEntry, null), new MessageOrigin(attr.Origin)); - return; - } - - // Otherwise, it's pending until the type map group is seen - _pendingAssemblyTargets.AddToList(typeMapGroup, attr); + // Otherwise, it's pending until the type map group is seen. + // Note: resolvedTargetAssembly may be null if the assembly could not be resolved (e.g., it doesn't + // exist in the input). In that case the attribute will be dropped (not marked) when the group is seen. + _pendingAssemblyTargets.AddToList(typeMapGroup, (attr, resolvedTargetAssembly)); } @@ -297,18 +335,20 @@ public void Resolve(LinkContext context, TypeMapHandler manager) } else if (attr.AttributeType.Name is "TypeMapAssemblyTargetAttribute`1") { - manager.AddAssemblyTarget(typeMapGroup, (attr, assembly)); + AssemblyDefinition? resolvedTargetAssembly = null; if (attr.ConstructorArguments[0].Value is string str) { var nextAssemblyName = AssemblyNameReference.Parse(str); if (context.TryResolve(nextAssemblyName) is AssemblyDefinition nextAssembly) { + resolvedTargetAssembly = nextAssembly; if (seen.Add(nextAssembly)) { toVisit.Enqueue(nextAssembly); } } } + manager.AddAssemblyTarget(typeMapGroup, (attr, assembly), resolvedTargetAssembly); } } } diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapAllConditionalEntriesDep.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapAllConditionalEntriesDep.cs new file mode 100644 index 00000000000000..6f7d7659a1d8f9 --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapAllConditionalEntriesDep.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +// All TypeMap entries in this assembly are conditional (3-argument form). +// The trim target (AllConditionalTrimTarget) is never referenced by the test assembly, +// so ILLink drops the entry, which leaves this assembly with no surviving TypeMap entries. +// The test verifies that the TypeMapAssemblyTarget attribute pointing here is also removed. +using System.Runtime.InteropServices; +using Mono.Linker.Tests.Cases.Reflection.Dependencies; + +[assembly: TypeMap("ConditionalEntry", typeof(AllConditionalTarget), typeof(AllConditionalTrimTarget))] + +namespace Mono.Linker.Tests.Cases.Reflection.Dependencies +{ + public class AllConditionalTarget; + public class AllConditionalTrimTarget; +} diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapAllConditionalGroupDep.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapAllConditionalGroupDep.cs new file mode 100644 index 00000000000000..c8c6959e795d5f --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapAllConditionalGroupDep.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Mono.Linker.Tests.Cases.Reflection.Dependencies +{ + // Group marker type used by TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed. + // Lives in its own assembly so the test assembly's TypeMapAssemblyTarget + // generic argument does not create a compile-time reference to the conditional.dll dependency. + public class AllConditionalGroupType; +} diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed.cs new file mode 100644 index 00000000000000..20e7e710ea112c --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; +using Mono.Linker.Tests.Cases.Expectations.Assertions; +using Mono.Linker.Tests.Cases.Expectations.Metadata; +using Mono.Linker.Tests.Cases.Reflection.Dependencies; + +// The test assembly references "conditional.dll" only by the assembly name string. +// All TypeMap entries in conditional.dll are conditional on AllConditionalTrimTarget, +// which is never marked. So conditional.dll ends up with no surviving TypeMap entries. +// The fix should cause this TypeMapAssemblyTarget attribute to be removed from the linked output. +[assembly: TypeMapAssemblyTarget("conditional")] + +namespace Mono.Linker.Tests.Cases.Reflection +{ + // Compile the group-type assembly first so both the test assembly and conditional.dll can reference it. + // Compile conditional.dll second with addAsReference:false so the test assembly has no compile-time + // dependency on it (only the string reference in TypeMapAssemblyTarget). + [SetupCompileBefore("allconditionalgroup.dll", new[] { "Dependencies/TypeMapAllConditionalGroupDep.cs" })] + [SetupCompileBefore("conditional.dll", new[] { "Dependencies/TypeMapAllConditionalEntriesDep.cs" }, + references: new[] { "allconditionalgroup.dll" }, addAsReference: false)] + [SetupLinkerAction("link", "System.Private.CoreLib")] // Needed to apply embedded XML (RemoveAttributeInstances) + [SetupLinkerArgument("--ignore-link-attributes", "false")] + [RemovedAssembly("conditional.dll")] + [RemovedAttributeInAssembly("test", typeof(TypeMapAssemblyTargetAttribute))] + [Kept] + class TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed + { + [Kept] + static void Main() + { + // Use the group so the trimmer processes the TypeMapAssemblyTarget attribute. + _ = TypeMapping.GetOrCreateExternalTypeMapping(); + } + } +}