Skip to content

[TrimmableTypeMap] Emit empty stubs for trimmed per-assembly typemaps#12045

Merged
jonathanpeppers merged 2 commits into
mainfrom
dev/simonrozsival/trimmable-typemap-missing-stubs
Jul 13, 2026
Merged

[TrimmableTypeMap] Emit empty stubs for trimmed per-assembly typemaps#12045
jonathanpeppers merged 2 commits into
mainfrom
dev/simonrozsival/trimmable-typemap-missing-stubs

Conversation

@simonrozsival

Copy link
Copy Markdown
Member

Problem

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 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 TypeMapping enumerates every TypeMapAssemblyTarget attribute and Assembly.Loads the named assembly, throwing for the trimmed ones:

System.IO.FileNotFoundException: Could not load file or assembly '_GoogleGson.TypeMap, Culture=neutral, PublicKeyToken=null'.
   at System.Runtime.InteropServices.TypeMapLazyDictionary.ProcessAttributes(...)
   at System.Runtime.InteropServices.TypeMapping.GetOrCreateExternalTypeMapping[TTypeMapGroup]()
   at Microsoft.Android.Runtime.TypeMapLoader.Initialize()
   at Android.Runtime.JNIEnvInit.InitializeTrimmableTypeMapData()

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.TypeMap assemblies, but the dangling TypeMapAssemblyTarget attributes 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.Load succeeds and contributes no mappings. Stubs are written into the linked/ output before the typemap DLLs flow into the publish/ReadyToRun pipeline.

  • New task GenerateMissingTypeMapStubs — compares the pre-trim typemap set against the surviving linked/ set and emits a stub for each missing one.
  • New public helper TypeMapAssemblyGenerator.GenerateEmpty(...) — uses the existing PEAssemblyBuilder (System.Reflection.Metadata; no Mono.Cecil).
  • Wired into 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.InteropServices facade (which ILLink had reconciled to System.Private.CoreLib in the linked root). The shipped, trimmed facade no longer forwards TypeMapping, so a regenerated root fails at runtime with TypeLoadException: Could not load type 'System.Runtime.InteropServices.TypeMapping'. Emitting stubs leaves ILLink's reconciled root untouched.

Testing

  • Existing trimmable-typemap unit tests: 616 passed.
  • Manual end-to-end: the MAUI app (CoreCLR, trimmable, Release) now builds, installs, and launches to its first page with no crash — previously it crashed at startup. The six dangling TypeMapAssemblyTargets now resolve to empty stubs.
  • APK size impact is negligible (6 stubs ≈ 12 KB uncompressed).

Notes / follow-ups

  • Scope: wired into the CoreCLR trimmable path (where the runtime Assembly.Load crash occurs). The NativeAOT trimmable path resolves typemaps at ILC time; happy to extend if equivalent handling is wanted there.
  • Can add unit / build-integration tests for GenerateMissingTypeMapStubs / GenerateEmpty as a follow-up.

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
Copilot AI review requested due to automatic review settings July 12, 2026 20:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 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 GenerateMissingTypeMapStubs MSBuild task to emit empty stub assemblies for missing per-assembly typemaps in the linked/ output.
  • Add TypeMapAssemblyGenerator.GenerateEmpty(...) helper to generate an entry-free typemap assembly via the existing PEAssemblyBuilder.
  • 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

Comment thread src/Xamarin.Android.Build.Tasks/Tasks/GenerateMissingTypeMapStubs.cs Outdated
- 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
@simonrozsival simonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jul 13, 2026
@jonathanpeppers jonathanpeppers merged commit 33a77b4 into main Jul 13, 2026
42 checks passed
@jonathanpeppers jonathanpeppers deleted the dev/simonrozsival/trimmable-typemap-missing-stubs branch July 13, 2026 13:04
simonrozsival added a commit that referenced this pull request Jul 13, 2026
…#12045)

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.

- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants