Skip to content
Merged
1 change: 1 addition & 0 deletions src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ Reflection.RunClassConstructor
Reflection.TypeHierarchyLibraryModeSuppressions
Reflection.TypeHierarchyReflectionWarnings
Reflection.TypeMap
Reflection.TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed
Reflection.TypeUsedViaReflection
Reflection.UnsafeAccessor
RequiresCapability.BasicRequires
Expand Down
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
77 changes: 62 additions & 15 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,23 @@ 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 the type map group has been seen, process the attribute immediately.
if (_referencedExternalTypeMaps.Contains(typeMapGroup) || _referencedProxyTypeMaps.Contains(typeMapGroup))
{
_markStep.MarkCustomAttribute(attr.Attribute, new DependencyInfo(DependencyKind.TypeMapEntry, null), new MessageOrigin(attr.Origin));
MarkAssemblyTargetIfReady(typeMapGroup, attr, resolvedTargetAssembly, callingMethod: null);
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));
}


Expand Down Expand Up @@ -297,18 +342,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