Skip to content
3 changes: 3 additions & 0 deletions src/tools/illink/src/linker/Linker.Steps/MarkStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
80 changes: 60 additions & 20 deletions src/tools/illink/src/linker/Linker/TypeMapHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,16 @@ sealed class TypeMapHandler
// [type map group: custom attributes]
Dictionary<TypeReference, List<CustomAttributeWithOrigin>> _pendingExternalTypeMapEntries = null!;
Dictionary<TypeReference, List<CustomAttributeWithOrigin>> _pendingProxyTypeMapEntries = null!;
Dictionary<TypeReference, List<CustomAttributeWithOrigin>> _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<TypeReference, List<(CustomAttributeWithOrigin Attr, AssemblyDefinition? TargetAssembly)>> _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<AssemblyDefinition, List<(TypeReference Group, CustomAttributeWithOrigin Attr, MethodDefinition? CallingMethod)>> _pendingAssemblyTargetsByAssembly = null!;

HashSet<TypeReference> _referencedExternalTypeMaps = null!;
HashSet<TypeReference> _referencedProxyTypeMaps = null!;
Expand All @@ -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);
Expand All @@ -70,12 +80,11 @@ public void ProcessExternalTypeMapGroupSeen(MethodDefinition callingMethod, Type
MarkTypeMapAttribute(entry, new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod));
}
}
if (_pendingAssemblyTargets.Remove(typeMapGroup, out List<CustomAttributeWithOrigin>? 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);
}
}
}
Expand All @@ -91,12 +100,11 @@ public void ProcessProxyTypeMapGroupSeen(MethodDefinition callingMethod, TypeRef
MarkTypeMapAttribute(entry, new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod));
}
}
if (_pendingAssemblyTargets.Remove(typeMapGroup, out List<CustomAttributeWithOrigin>? 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);
}
}
}
Expand All @@ -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))
Comment thread
jtschuster marked this conversation as resolved.
{
// 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();
Expand Down Expand Up @@ -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;
}
Comment on lines -201 to -206

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should keep this check, but call MarkAssemblyTargetIfReady instead of directly calling MarkCustomAttribute.


// 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));
}


Expand Down Expand Up @@ -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);
Comment thread
jtschuster marked this conversation as resolved.
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AllConditionalGroupType>("ConditionalEntry", typeof(AllConditionalTarget), typeof(AllConditionalTrimTarget))]

namespace Mono.Linker.Tests.Cases.Reflection.Dependencies
{
public class AllConditionalTarget;
public class AllConditionalTrimTarget;
}
Original file line number Diff line number Diff line change
@@ -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<AllConditionalGroupType>
// generic argument does not create a compile-time reference to the conditional.dll dependency.
public class AllConditionalGroupType;
}
Original file line number Diff line number Diff line change
@@ -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<AllConditionalGroupType>("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<AllConditionalGroupType>))]
[Kept]
class TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed
Comment thread
Copilot marked this conversation as resolved.
{
[Kept]
static void Main()
{
// Use the group so the trimmer processes the TypeMapAssemblyTarget attribute.
_ = TypeMapping.GetOrCreateExternalTypeMapping<AllConditionalGroupType>();
}
}
}
Loading