Skip to content

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

Open
simonrozsival wants to merge 2 commits into
mainfrom
dev/simonrozsival/trimmable-typemap-missing-stubs
Open

[TrimmableTypeMap] Emit empty stubs for trimmed per-assembly typemaps#12045
simonrozsival wants to merge 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants