ILLink: Don't preserve forwarder assemblies referenced by link-action root assemblies#126519
ILLink: Don't preserve forwarder assemblies referenced by link-action root assemblies#126519
Conversation
…semblies When an assembly is rooted with `-a` (AllMembers) and has link action, MarkStep.MarkEntireAssembly previously called TypeReferenceMarker.MarkTypeReferences unconditionally. This walked all type references in the assembly and called MarkForwardedScope for each, marking exported type entries in forwarder assemblies and causing those assemblies to be preserved. This is unnecessary for link-action assemblies because SweepStep rewrites type references to point directly to implementation assemblies (via AssemblyReferencesCorrector), making the forwarder assemblies unnecessary. The fix splits MarkEntireAssembly into two concerns: - MarkExportedTypes: always marks the assembly's own ExportedType entries and their forwarded scopes (needed even for link assemblies that are themselves forwarders) - TypeReferenceMarker.MarkTypeReferences: now only called for copy/save assemblies where type references won't be rewritten Also adds a regression test: UsedForwarderIsRemovedWhenReferencedByRootedAssembly. Fixes #126518 Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/620011f6-5d77-4684-9948-36a463ece85e Co-authored-by: sbomer <787361+sbomer@users.noreply.github.com>
Move the CreateTypeReferenceForExportedTypeTarget helper out of TypeReferenceMarker and into MarkStep, used by MarkExportedTypes. TypeReferenceMarker.ProcessExportedType is now a no-op since MarkExportedTypes handles all exported type marking (always called before TypeReferenceMarker). This makes TypeReferenceMarker focused solely on marking forwarded scopes of type references. Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/3f923948-9778-4cb0-90d8-1fdfae2875d2 Co-authored-by: sbomer <787361+sbomer@users.noreply.github.com>
MarkEntireAssembly now only marks all types, custom attributes, and exported types — no action check needed. TypeReferenceMarker.MarkTypeReferences is called directly in the copy/save branch of MarkAssembly, where it conceptually belongs. The link-action root path calls MarkEntireAssembly without the type reference scope walk, since SweepStep rewrites those references anyway. Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d97f6dea-e5d1-499b-b24e-384fc89ddca7 Co-authored-by: sbomer <787361+sbomer@users.noreply.github.com>
…ndling MarkExportedTypes now only marks the export entry itself (MarkExportedType) for all root assemblies. MarkForwardedScope for exported types is moved back into TypeReferenceMarker.ProcessExportedType, which already only runs for copy/save assemblies. This keeps the two concerns separate: marking the forwarder entry vs. preserving the assembly it points to. Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/a0ee3bc2-d2ba-46c6-9177-76b764d96d85 Co-authored-by: sbomer <787361+sbomer@users.noreply.github.com>
Undo the static refactoring of CreateTypeReferenceForExportedTypeTarget, keeping it as an instance method on TypeReferenceMarker that captures assembly.MainModule from the enclosing class.
|
Note This review was generated by Copilot. ILLink Type-Forwarder Marking Review —
|
… is handled Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/6d212a0c-aaf9-43f6-9464-dc0e37b7b97f Co-authored-by: sbomer <787361+sbomer@users.noreply.github.com>
Done in cbbf72e. Added |
There was a problem hiding this comment.
Pull request overview
Adjusts ILLink’s marking behavior so that rooting a link-action assembly no longer unintentionally preserves referenced type-forwarder assemblies, relying instead on the sweep step’s reference rewriting. This aligns TypeReferenceMarker usage back with its historical intent (copy/save assemblies) while still ensuring exported type entries are recorded for rooted assemblies.
Changes:
- Moved
TypeReferenceMarker.MarkTypeReferencesout ofMarkEntireAssemblyand into the copy/save path inMarkAssembly. - Added
MarkExportedTypesso rooted assemblies always record their ownExportedTypeentries without preserving forwarder assemblies. - Added two
TypeForwardingtest cases to validate forwarder removal/rewrite for rootedlinkassemblies, including a rooted forwarder scenario.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/tools/illink/src/linker/Linker.Steps/MarkStep.cs |
Splits exported-type entry marking from type-reference scope marking so forwarders aren’t preserved for rooted link assemblies. |
src/tools/illink/test/Mono.Linker.Tests.Cases/TypeForwarding/UsedForwarderIsRemovedWhenReferencedByRootedAssembly.cs |
Regression coverage: rooted consumer (-a test, link) shouldn’t keep Forwarder.dll, and reference should be rewritten away. |
src/tools/illink/test/Mono.Linker.Tests.Cases/TypeForwarding/RootedForwarderWithExportedTypesIsHandled.cs |
Coverage for a rooted forwarder (-a Forwarder, link): exported type entries preserved, impl kept if used, and consumer reference rewritten. |
...ink/test/Mono.Linker.Tests.Cases/TypeForwarding/RootedForwarderWithExportedTypesIsHandled.cs
Outdated
Show resolved
Hide resolved
|
@copilot apply changes based on the comments in this thread |
…o MarkExportedTypes Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/0b1ef5fb-462b-4dc2-b733-efa071753b64 Co-authored-by: sbomer <787361+sbomer@users.noreply.github.com>
Done in c53905a:
|
Description
MarkStep.MarkEntireAssemblyunconditionally calledTypeReferenceMarker.MarkTypeReferences, which walks all type references and callsMarkForwardedScopeon each — causing forwarder assemblies to be preserved even for root assemblies withlinkaction. Forlink-action assemblies this is unnecessary:SweepStep.AssemblyReferencesCorrectorrewrites type references to point directly to implementation assemblies, making forwarders irrelevant at runtime.Historically,
TypeReferenceMarkerwas only ever intended for copy/save assemblies. It started running for link-action root assemblies as an unintended side effect of #96363 reusingMarkEntireAssemblyfor the new root-with-link-action path.Changes:
MarkStep.cs:MarkExportedTypes(assembly, origin): marks the assembly's ownExportedTypeentries (viaMarkExportedType) for all root assemblies — this records the export entry itself without preserving the forwarder assembly it points to; reuses the caller'sMessageOriginto avoid per-entry allocationsTypeReferenceMarker.MarkTypeReferences: moved out ofMarkEntireAssemblyand called directly in the copy/save branch ofMarkAssembly—MarkEntireAssemblyis now action-agnostic (marks all types, custom attributes, and exported type entries only)TypeReferenceMarker.ProcessExportedType: callsMarkForwardedScopefor each exported type — runs only for copy/save assemblies, so forwarder assemblies pointed to by exports are only preserved when their references won't be rewrittenCreateTypeReferenceForExportedTypeTarget: private instance helper insideTypeReferenceMarker, placed afterProcessExtra, that correctly handles nested exported types viaDeclaringTypeand is used byProcessExportedTypeUsedForwarderIsRemovedWhenReferencedByRootedAssembly.cs— Regression test: verifiesForwarder.dllis removed when the consuming assembly is rooted with-a test(link action)RootedForwarderWithExportedTypesIsHandled.cs— Test: verifies that a root linked assembly that is itself a type forwarder (rooted with-a Forwarder) has its exported type entries correctly preserved byMarkExportedTypes, the implementation assembly is kept, and the consuming assembly's reference to the forwarder is rewritten by the sweepThe key separation of concerns:
MarkExportedType) — always done for all root assembliesMarkForwardedScope) — done only for copy/save assemblies viaTypeReferenceMarkerCustomer Impact
Projects using
-ato root an assembly withlinkaction unnecessarily retain forwarder assemblies, increasing trimmed output size.Regression
No — this is a pre-existing behavioral issue, not a regression from a recent release.
Testing
Added two new ILLink test cases:
UsedForwarderIsRemovedWhenReferencedByRootedAssembly: assertsForwarder.dllis removed and the assembly reference is rewritten when the consuming assembly is rooted with-a test(link action)RootedForwarderWithExportedTypesIsHandled: asserts that when a forwarder assembly is itself rooted with-a Forwarder(link action), its exported type entries are preserved byMarkExportedTypes, the implementation is kept because the forwarded type is used, and the consuming assembly's reference to the forwarder is rewritten away by the sweepAll 44
TypeForwardingtests and 49LinkXmltests continue to pass.Risk
Low.
TypeReferenceMarker.MarkTypeReferencesis now called only in the copy/save branch ofMarkAssembly(its historically intended location), andMarkEntireAssemblyis kept focused on marking all types and their members. Exported type entry marking (MarkExportedType) always runs for all root assemblies and reuses the caller'sMessageOriginfor consistency, while forwarder assembly preservation (MarkForwardedScopefor exported types) is scoped to copy/save assemblies viaTypeReferenceMarker. TheCreateTypeReferenceForExportedTypeTargethelper is encapsulated as an instance method insideTypeReferenceMarker(afterProcessExtra) since it is only used there. Existing tests cover copy/save, link, and rooted-forwarder scenarios.Package authoring no longer needed in .NET 9
IMPORTANT: Starting with .NET 9, you no longer need to edit a NuGet package's csproj to enable building and bump the version.
Keep in mind that we still need package authoring in .NET 8 and older versions.
Original prompt
[Chronological Review] 1. User asked which files are uncommitted in /home/sven/src/runtime 2. Agent ran `git status --short` and reported the results 3. User asked to review the changes around type forwarder behavior 4. Agent loaded the code-review skill, gathered diffs for all three changed files (MarkStep.cs modified, old test deleted, new test created) 5. Agent spawned a background explore task to gather deep context: MarkEntireAssembly method, TypeReferenceMarker class, MarkingHelpers methods, ProcessReferencesStep.IsFullyPreservedAction, test file listing, related tests, DependencyKind.ExportedType usage, git log 6. Agent spawned another sync explore task to understand type reference rewriting in SweepStep/ProcessReferencesStep 7. Agent delivered a comprehensive code review with holistic assessment and 4 detailed findings (2 warnings, 2 approvals) 8. User noted they filed issue https://github.com//issues/126518 9. Agent read the issue via GitHub MCP server 10. User asked to commit the current fix 11. Agent checked branch (main) and staged/committed the 3 files with a detailed commit message referencing #126518[Intent Mapping]
[Technical Inventory]
main[Code Archaeology]
[Progress Assessment]
[Context Validation]
[Recent Commands Analysis]
1. Conversation Overview: - Primary Objectives: User asked to (1) check uncommitted files, (2) "review the changes around type forwarder behavior please", (3) note a filed GitHub issue, and (4) "Commit the current fix." - Session Context: Working in dotnet/runtime repo (/home/sven/src/runtime) on the `main` branch. The changes fix an ILLink bug where rooted assemblies with link action incorrectly preserved forwarder assemblies referenced through type references. The full workflow was: status check → deep code review → issue acknowledgment → commit. - User Intent Evolution: Straightforward progression from exploration → review → commit. User is "sbomer" (GitHub member, ILLink area).- Technical Foundation:
- Repository: dotnet/runtime at
- Component: ILLink (IL Linker) — .NET's trimming/linking tool in
- Core Concept: Type forwarders are assemblies containing
- Assembly Actions:
- Bug:
- GitHub Issue: #126518 — "ILLink: root assembly preserves referenced forwarders in other assemblies", filed b...
/home/sven/src/runtime, branchmainsrc/tools/illink/ExportedTypeentries that redirect type lookups to implementation assemblies. The linker can remove them when it rewrites type references to point directly at implementation assemblies.link(rewrite/trim),copy(preserve as-is),save(preserve with minor changes).ProcessReferencesStep.IsFullyPreservedActionreturns true only for Copy/Save.MarkEntireAssemblyunconditionally calledTypeReferenceMarker.MarkTypeReferences, which marked forwarder scopes for ALL rooted assemblies. For link-action assemblies, this was unnecessary becauseSweepStep.AssemblyReferencesCorrectorrewrites type references to point directly to implementation assemblies.Created from Copilot CLI via the copilot delegate command.