[TrimmableTypeMap] Emit empty stubs for trimmed per-assembly typemaps#12045
Open
simonrozsival wants to merge 2 commits into
Open
[TrimmableTypeMap] Emit empty stubs for trimmed per-assembly typemaps#12045simonrozsival wants to merge 2 commits into
simonrozsival wants to merge 2 commits into
Conversation
The root `_Microsoft.Android.TypeMaps` assembly is generated before trimming
with an `[assembly: TypeMapAssemblyTarget<T>("_X.TypeMap")]` entry for every
per-assembly typemap. ILLink can trim individual `_X.TypeMap` assemblies when
their target Java binding is unused, but those attributes carry an opaque
assembly-name string rather than a metadata reference, so ILLink neither keeps
the target nor prunes the attribute. At startup CoreCLR's `TypeMapping`
enumerates every attribute and calls `Assembly.Load` on the named assembly,
throwing `FileNotFoundException` for the trimmed ones, crashing the app.
Found while running a real .NET MAUI app on CoreCLR with the trimmable
typemap: six unused bindings (GoogleGson, Jsr305Binding, Xamarin.AndroidX.Print,
Xamarin.JavaX.Inject, Xamarin.JSpecify, Xamarin.Kotlin.StdLib) were trimmed
together with their `_X.TypeMap` assemblies, but the dangling
`TypeMapAssemblyTarget` attributes on the root assembly remained.
Fix: after ILLink, emit an empty (entry-free) stub assembly for each
per-assembly typemap that was trimmed away, so `Assembly.Load` succeeds and
contributes no mappings. This leaves the linked root assembly - and the
assembly references ILLink reconciled in it - untouched. Regenerating the root
instead was rejected because it re-introduces unreconciled
System.Runtime.InteropServices facade references that fail to resolve at
runtime.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: fd0c2b70-3ad0-42db-940c-54ae6d5a920b
Contributor
There was a problem hiding this comment.
Pull request overview
This PR prevents CoreCLR + trimmable typemap apps from crashing at startup when ILLink trims away per-assembly _X.TypeMap.dll assemblies that are still referenced (by name string) from the root _Microsoft.Android.TypeMaps assembly. The fix emits empty “stub” typemap assemblies post-ILLink so Assembly.Load succeeds and contributes no mappings.
Changes:
- Add
GenerateMissingTypeMapStubsMSBuild task to emit empty stub assemblies for missing per-assembly typemaps in thelinked/output. - Add
TypeMapAssemblyGenerator.GenerateEmpty(...)helper to generate an entry-free typemap assembly via the existingPEAssemblyBuilder. - Wire stub generation into the CoreCLR trimmable typemap ILLink pipeline (
AfterTargets="ILLink", before publish/R2R inputs are computed).
Show a summary per file
| File | Description |
|---|---|
| src/Xamarin.Android.Build.Tasks/Tasks/GenerateMissingTypeMapStubs.cs | New task that detects missing _*.TypeMap.dll after trimming and writes empty stubs into linked/. |
| src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets | Registers and schedules the new task after ILLink to ensure missing typemap assemblies exist before publish/R2R. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs | Adds GenerateEmpty API to emit a minimal typemap assembly with no entries. |
Copilot's findings
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Write stubs with Files.CopyIfStreamChanged directly on the MemoryStream instead of CopyIfBytesChanged(stream.ToArray()), avoiding an extra byte[] allocation per stub. - Add GenerateMissingTypeMapStubsTests covering (1) a per-assembly typemap present pre-trim but absent from linked/ gets an empty, valid PE stub named after the trimmed assembly while survivors and the root are left untouched, and (2) nothing is emitted when no typemap was trimmed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fd0c2b70-3ad0-42db-940c-54ae6d5a920b
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The root
_Microsoft.Android.TypeMapsassembly is generated before trimming with an[assembly: TypeMapAssemblyTarget<T>("_X.TypeMap")]entry for every per-assembly typemap. ILLink can trim individual_X.TypeMapassemblies when their target Java binding is unused, but those attributes reference the assembly by an opaque name string (not a metadata reference), so ILLink neither keeps the target nor prunes the attribute.At startup, CoreCLR's
TypeMappingenumerates everyTypeMapAssemblyTargetattribute andAssembly.Loads the named assembly, throwing for the trimmed ones:Found while running a real .NET MAUI app on CoreCLR + trimmable typemap (Release,
android-arm64): six unused bindings —GoogleGson,Jsr305Binding,Xamarin.AndroidX.Print,Xamarin.JavaX.Inject,Xamarin.JSpecify,Xamarin.Kotlin.StdLib— were trimmed together with their_X.TypeMapassemblies, but the danglingTypeMapAssemblyTargetattributes on the root remained, so the app crashed on launch.Fix
After ILLink, emit an empty (entry-free) stub assembly for each per-assembly typemap that was trimmed away, so
Assembly.Loadsucceeds and contributes no mappings. Stubs are written into thelinked/output before the typemap DLLs flow into the publish/ReadyToRun pipeline.GenerateMissingTypeMapStubs— compares the pre-trim typemap set against the survivinglinked/set and emits a stub for each missing one.TypeMapAssemblyGenerator.GenerateEmpty(...)— uses the existingPEAssemblyBuilder(System.Reflection.Metadata; no Mono.Cecil).Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets(AfterTargets="ILLink").Why stubs, and not "regenerate the root without the dangling entries"?
Regenerating the root re-introduces references to the
System.Runtime.InteropServicesfacade (which ILLink had reconciled toSystem.Private.CoreLibin the linked root). The shipped, trimmed facade no longer forwardsTypeMapping, so a regenerated root fails at runtime withTypeLoadException: Could not load type 'System.Runtime.InteropServices.TypeMapping'. Emitting stubs leaves ILLink's reconciled root untouched.Testing
TypeMapAssemblyTargets now resolve to empty stubs.Notes / follow-ups
Assembly.Loadcrash occurs). The NativeAOT trimmable path resolves typemaps at ILC time; happy to extend if equivalent handling is wanted there.GenerateMissingTypeMapStubs/GenerateEmptyas a follow-up.