-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Update TypeMap attribute handling #120477
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Mark TypeMapAssemblyTargetAttributes when a typemapuniverse is marked - Recurse through TypeMapAssemblyTarget assemblies to find all TypeMapAttributes - Refactor TypeMapHandler construction and initialization - Use TypeReferenceEqualityComparer for Dictionaries - Add more test coverage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR updates TypeMap attribute handling in the IL linker to fix issues with assembly preservation and attribute marking. The changes improve how TypeMapAttributes are handled across assemblies and ensure assemblies containing only TypeMap attributes are properly preserved.
Key changes:
- Enhanced TypeMapHandler to recursively process TypeMapAssemblyTarget assemblies
- Improved attribute marking logic to mark assemblies when TypeMap attributes need to be preserved
- Added comprehensive test coverage for cross-assembly TypeMap scenarios
Reviewed Changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
File | Description |
---|---|
src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs | Extended test case with cross-assembly TypeMap scenarios and dependency validation |
src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapSecondOrderReference.cs | New test dependency assembly with TypeMap attributes for second-order reference testing |
src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapReferencedAssembly.cs | New test dependency assembly with TypeMap attributes for primary reference testing |
src/tools/illink/src/linker/Linker/TypeReferenceEqualityComparer.cs | Added obsolete marker for Default property to prevent misuse |
src/tools/illink/src/linker/Linker/TypeMapHandler.cs | Major refactor of TypeMapHandler with improved initialization, recursive assembly processing, and proper assembly marking |
src/tools/illink/src/linker/Linker.Steps/SweepStep.cs | Updated IsMarkedAssembly to check both assembly and main module marking |
src/tools/illink/src/linker/Linker.Steps/MarkStep.cs | Simplified TypeMapHandler initialization and made MarkAssembly method public |
src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs
Outdated
Show resolved
Hide resolved
Tagging subscribers to this area: @dotnet/illink |
@jkoritzinsky does this need to be backported to .NET 10? As in, I assume this bug would break CsWinRT 3.0 too? 🤔 |
…ap.cs Co-authored-by: Copilot <[email protected]>
bool IsMarkedAssembly(AssemblyDefinition assembly) | ||
{ | ||
return Annotations.IsMarked(assembly.MainModule); | ||
return Annotations.IsMarked(assembly) || Annotations.IsMarked(assembly.MainModule); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An assembly that only had MarkAssembly
called would be not be kept if the MainModule wasn't marked. If it makes more sense to mark the MainModule in MarkAssembly rather than have this check, I'm happy to change.
internal sealed class TypeReferenceEqualityComparer : EqualityComparer<TypeReference> | ||
{ | ||
[Obsolete("Default will point to default object equality comparer. Use the constructor to create an instance with a resolver.")] | ||
public static new object Default => throw new InvalidOperationException(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accidentally used TypeReferenceEqualityComparer.Default
, which went to ObjectEqualityComparer.Default
and required more debugging time than it should have. Adding this property to avoid doing that again.
public void Resolve(LinkContext context, TypeMapHandler manager) | ||
{ | ||
foreach (AssemblyNameReference assemblyName in assemblies) | ||
HashSet<AssemblyDefinition> visited = new(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: check _assembly
here and return before allocating anything if it's null
var nextAssemblyName = AssemblyNameReference.Parse(str); | ||
if (context.TryResolve(nextAssemblyName) is AssemblyDefinition nextAssembly) | ||
{ | ||
if (!visited.Contains(nextAssembly) && !toVisit.Contains(nextAssembly)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add items to visited on enqueue above, instead of dequeue, then we can avoid the linear scan through toVisit here.
MarkTypeMapAttribute(entry, new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod)); | ||
foreach (var entry in assemblyTargets) | ||
{ | ||
var info = new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: TypeMapEntry
doesn't seem like it quite fits the usage - maybe add another DependencyKind?
{ | ||
var info = new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod); | ||
_markStep.MarkCustomAttribute(entry.Attribute, info, new MessageOrigin(entry.Origin)); | ||
_markStep.MarkAssembly(entry.Origin, info, new MessageOrigin(entry.Origin)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this use MarkTypeMapAttribute
instead? Same below.
if (seenTypeGroups.Contains(group) && attr.DependencySourceRequiresTarget(_context, dependencyTypeDef)) | ||
{ | ||
MarkTypeMapAttribute(attr, new DependencyInfo(DependencyKind.TypeMapEntry, trimTarget)); | ||
MarkTypeMapAttribute(attr, new DependencyInfo(DependencyKind.TypeMapEntry, dependencySource)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will go to the else
case even if the dependency source doesn't require the target - should the seenTypeGroups
and the else
below be inside the DependencySourceRequiresTarget
check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good catch. We should split first on DependencySourceRequiresTarget()
, then seenTypeGroups.Contains()
to match the other processing flow (pending dependency source -> pending typeMapGroup -> mark).
{ | ||
"TypeMapAttribute`1" => | ||
sourceType is null || context.Annotations.IsRelevantToVariantCasting(sourceType) | ||
|| context.Annotations.IsInstantiated(sourceType), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think for the external type map, the types aren't required to be instantiated or marked relevant to variant casting. I think the _typeMapHandler.ProcessType(type)
in MarkInstruction
is intended to keep type map entries in more cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I meant to leave a comment that it also should require the target when the source is a target of an IsInst instruction. "Target of IsInst" just doesn't have an associated annotation and I'm a little hesitant to add another cache for this. This should handle all "annotatable" ways that the target type is required at initialization. I think as long as this is only called during initialization and we keep the ProcessType() call for IsInst instructions we should be okay. I can add a comment to make that clear or refactor it to make it only visible to RecordTypeMapEntry.
{ | ||
[Kept] | ||
[ExpectedWarning("IL2057", "Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String)'")] | ||
[ExpectBodyModified] // Bug |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the bug?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for noting this, I forgot to add a comment. It looks like the UnusedTypeChecks optimization is messing with the TypeMap attributes. The if (obj is UsedTrimTarget)
checks are being modified. I'll a better example and a github issue for this.
Fixes #120394