Skip to content

[TrimmableTypeMap] Pre-generate Mono.Android/Java.Interop typemaps at SDK build time#12127

Draft
simonrozsival wants to merge 11 commits into
mainfrom
dev/simonrozsival/pregenerate-mono-android-typemap
Draft

[TrimmableTypeMap] Pre-generate Mono.Android/Java.Interop typemaps at SDK build time#12127
simonrozsival wants to merge 11 commits into
mainfrom
dev/simonrozsival/pregenerate-mono-android-typemap

Conversation

@simonrozsival

Copy link
Copy Markdown
Member

Goal

Pre-generate the trimmable type map for Mono.Android (and Java.Interop) at SDK build time (issue #10792), so app builds no longer rescan Mono.Android's ~8000 types (~5s of a ~6s generation step) on every build, and startup avoids re-initializing that map.

Approach

The framework typemaps are generated once during the SDK/pack build and shipped in Microsoft.Android.Sdk under data/prebuilt-typemap:

  • _Mono.Android.TypeMap.dll + _Java.Interop.TypeMap.dll — always anchored on the Java.Lang.Object universe, so a single artifact serves both app-build universe modes (single/shared in Release, aggregate/per-assembly in Debug).
  • mono.android-typemaps.jar — the pre-compiled Java Callable Wrappers.
  • mono.android-acw-map.txt — the ACW map, merged into the app's acw-map.txt so R8 keep rules retain the pre-built JCWs.

The app build indexes Mono.Android + Java.Interop for resolution but does not rescan them (AssemblyInput.ScanForPeers=false); the generated root assembly references the pre-built per-assembly typemaps via [assembly: TypeMapAssemblyTarget<Java.Lang.Object>].

Verification

  • 620 Microsoft.Android.Sdk.TrimmableTypeMap.Tests pass.
  • On a physical arm64 device (API 36):
    • Single/shared universe (Release NativeAOT, trimmable) — launches to MainActivity, no crash.
    • Aggregate/per-assembly universe (CoreCLR Debug, trimmable) — Displayed …/MainActivity, no crash.
    • classes.dex retains the pre-built JCWs (~255 KB) via the merged ACW keep rules.

Known limitation

Debug NativeAOT (aggregate universe + array maps, maxArrayRank>0) is not yet wired for the pre-generated framework universe; it falls back to normal scanning (builds correctly, without the speedup). Completing the arrays-aggregate + shared-framework Initialize emit is a follow-up.


Draft — WIP prototype for review.

simonrozsival and others added 10 commits July 15, 2026 20:23
…root

Toward pre-generating Mono.Android's typemap at SDK build time (issue #10792):
add a generateRootAssembly flag to TrimmableTypeMapGenerator.Execute (and a
GenerateRootAssembly property on the GenerateTrimmableTypeMap task, default true).

When false, the per-assembly typemap DLLs (e.g. _Mono.Android.TypeMap) and JCW
Java sources are emitted, but the root _Microsoft.Android.TypeMaps assembly
(TypeMapAssemblyTarget<T> attributes + TypeMapLoader.Initialize) is not: that
root is emitted by the app build, which will reference the pre-generated
per-assembly typemap alongside the app's own.

Adds a unit test asserting the per-assembly typemap is produced and the root is
omitted when generateRootAssembly is false.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
…nder Java.Lang.Object

Special-case pre-generated framework typemaps (e.g. _Mono.Android.TypeMap, built at
SDK build time per issue #10792) in the root typemap assembly. Such typemaps always
use Java.Lang.Object as their universe anchor, so a single artifact serves both
app-build universe modes:

- RootTypeMapAssemblyGenerator.Generate gains sharedFrameworkTypeMapNames. They are
  emitted as [assembly: TypeMapAssemblyTarget<Java.Lang.Object>("name")] regardless of
  useSharedTypemapUniverse.
  * Merged (Release): they join the single Java.Lang.Object universe, so
    GetOrCreateExternalTypeMapping<Java.Lang.Object>() merges them with the app's
    entries automatically — no Initialize change.
  * Aggregate (Debug): TypeMapLoader.Initialize adds the Java.Lang.Object universe as
    element [0] alongside the app's per-assembly __TypeMapAnchor universes.
- Threaded through TrimmableTypeMapGenerator.Execute/GenerateTypeMapAssemblies.
- Aggregate + array maps (maxArrayRank > 0) with a shared framework universe throws
  NotSupportedException for now (tracked follow-up).

Adds unit tests asserting the framework typemap is referenced under Java.Lang.Object
(scope Mono.Android) in both modes, the app typemap keeps its own anchor in aggregate
mode, and both produce valid PE. 619 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
Milestone: generate _Mono.Android.TypeMap.dll (issue #10792) once during the SDK/pack
build instead of rescanning Mono.Android (~8000 types) on every app build.

- GenerateTrimmableTypeMap task: add ForceSharedTypemapUniverse. When true the typemap
  always uses the Java.Lang.Object universe anchor regardless of Debug, so the single
  pre-generated artifact is consumable by both app-build universe modes.
- build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets: new
  _GeneratePreBuiltMonoAndroidTypeMap target scans the built Mono.Android.dll (framework
  assemblies supplied for reference only) with ForceSharedTypemapUniverse=true and
  GenerateRootAssembly=false, emitting _Mono.Android.TypeMap.dll + JCW Java sources; and
  _AddPreBuiltMonoAndroidTypeMapToPackage ships the DLL in the SDK pack under
  data/prebuilt-typemap. Imported by Microsoft.Android.Sdk.proj.

Verified locally: the target produces _Mono.Android.TypeMap.dll (5.4 MB) with no
__TypeMapAnchor (i.e. anchored on Java.Lang.Object), 320 JCW .java files, and no root
_Microsoft.Android.TypeMaps assembly.

JCW jar compilation + app-build consumption follow in later milestones.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
…ack layout

ConfigureLocalWorkload (used by 'make all' and -t:InstallMaui) does not run the NuGet
GetFilesToPackage flow, so the FilesToPackage hook alone did not place the pre-generated
typemap into the local pack. Write _Mono.Android.TypeMap.dll directly into the pack layout
at $(MicrosoftAndroidSdkPackDir)data\prebuilt-typemap\ and run the generation
BeforeTargets=ConfigureLocalWorkload as well as _GenerateXASdkContent, so the artifact is
present for both local workload testing and the NuGet pack.

Verified: after ConfigureLocalWorkload, _Mono.Android.TypeMap.dll (5.4 MB) is present under
bin/.../packs/Microsoft.Android.Sdk.Darwin/<ver>/data/prebuilt-typemap/.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
…pemaps.jar

Compile the pre-generated Java Callable Wrappers (issue #10792) into a jar shipped in the
SDK pack under data/prebuilt-typemap, so the app build can dex them directly instead of
re-generating and re-compiling the ~320 Mono.Android JCWs.

- _CompilePreBuiltMonoAndroidTypeMapJcwJar javac-compiles the generated JCW .java into
  mono.android-typemaps.jar. Classpath mirrors GenerateJavaCallableWrappers:
  android.jar + mono.android.jar + java_runtime.jar.
- Ship the jar alongside _Mono.Android.TypeMap.dll (FilesToPackage + local pack layout).

Verified: mono.android-typemaps.jar (403 entries) is produced under
bin/.../packs/Microsoft.Android.Sdk.Darwin/<ver>/data/prebuilt-typemap/.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
…map refs in the task

Adds the generator/scanner/task plumbing the app build needs to consume a pre-generated
Mono.Android typemap (issue #10792):

- AssemblyInput gains ScanForPeers. When false the assembly is indexed for base-type
  resolution but not scanned for peer emission; JavaPeerScanner skips ScanAssembly for it.
- GenerateTrimmableTypeMap task gains PreGeneratedTypeMapAssemblies: those assemblies are
  marked ScanForPeers=false, and their _<Name>.TypeMap names are passed to the generator as
  sharedFrameworkTypeMapNames so the generated root references them under Java.Lang.Object.

Adds a unit test asserting a reference-only assembly emits no peers/typemap. 620 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
Wire the app build to use the SDK-shipped Mono.Android typemap (issue #10792) instead of
rescanning Mono.Android's ~8000 types on every build:

- Microsoft.Android.Sdk.TypeMap.Trimmable.targets: when the pre-generated typemap exists in
  the pack (data/prebuilt-typemap, gated by $(_AndroidUsePreGeneratedMonoAndroidTypeMap)),
  pass Mono.Android to GenerateTrimmableTypeMap as a PreGeneratedTypeMapAssembly (indexed for
  resolution, not rescanned); add the pre-built _Mono.Android.TypeMap.dll to the generated
  typemap set so it is linked/published and referenced by the root under Java.Lang.Object; and
  add mono.android-typemaps.jar to @(AndroidJavaLibrary) for dexing.
- create-packs targets: emit generated JCW .java / .class to obj intermediate so only the
  _Mono.Android.TypeMap.dll and jar are packaged.

Verified (Release NativeAOT, _AndroidTypeMapImplementation=trimmable): the app build no longer
generates _Mono.Android.TypeMap (only _Java.Interop/_Microsoft.Android.Runtime.NativeAOT/_NativeAOT
+ root), and the root references the pre-built _Mono.Android.TypeMap.

KNOWN GAP (next milestone): R8 shrinks the pre-built Mono.Android JCWs from classes.dex because
their ACW entries are no longer in the app acw-map (Mono.Android excluded from scan), so no keep
rules are generated. The pre-generated typemap's ACW keep rules must be shipped and fed to R8
before on-device runs will work.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
…r R8 keep rules

With Mono.Android excluded from the app scan, its ACW entries were absent from the app
acw-map, so R8 generated no keep rules and shrank the pre-built Mono.Android JCWs out of
classes.dex. Ship the pre-generated typemap's ACW map and merge it back in:

- create-packs: GenerateTrimmableTypeMap now also writes mono.android-acw-map.txt, shipped in
  the pack under data/prebuilt-typemap.
- App build: when the pre-generated typemap is used, append mono.android-acw-map.txt to the
  app's acw-map.txt after generation, so the proguard/R8 keep-rule generators (CoreCLR and
  NativeAOT) emit -keep rules for the pre-built Mono.Android JCWs.

Verified (Release NativeAOT, trimmable): classes.dex returns to ~255 KB (was ~13 KB when the
JCWs were shrunk), i.e. the pre-built Mono.Android JCWs are retained.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
… fix array rank

On-device (Release NativeAOT, trimmable) the app crashed at startup with BadImageFormatException
in TypeMapLazyDictionary.CreateExternalTypeMap. Two causes, both fixed:

1. Duplicate java/lang/Object in the merged Java.Lang.Object universe: the pre-built Mono.Android
   typemap mapped it to Java.Lang.Object while the app-scanned _Java.Interop.TypeMap mapped it to
   Java.Interop.JavaObject. MergeCrossAssemblyAliases coordinates these only within a single scan.
   Fix: pre-generate Mono.Android AND Java.Interop together (aliases coordinated) and exclude both
   from the app scan; ship/reference all pre-built *.TypeMap.dll.
2. Array-rank mismatch: the pre-built typemap used MaxArrayRank=0 while the app root/runtime used
   the AOT default (3), so array entries landed in the main universe. Fix: generate the pre-built
   typemap with MaxArrayRank=3, and have the root reference the framework typemaps' __ArrayMapRank{N}
   universes too.

Verified on a physical arm64 device (API 36): the Release/single-universe trimmable NativeAOT app
launches to MainActivity with no BadImageFormatException / crash. 620 unit tests still pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
…egate + array maps)

The aggregate-universe + array-maps (maxArrayRank > 0) combination — i.e. Debug NativeAOT — is
not yet wired for pre-generated framework typemaps and previously threw NotSupportedException,
which would break those builds. Gracefully disable the pre-generation for that combination so the
build falls back to scanning Mono.Android normally (correct, just without the speedup) instead of
failing.

Verified: Debug NativeAOT trimmable now builds (Mono.Android scanned in-app), while Release
NativeAOT (single universe) and CoreCLR Debug (aggregate universe) continue to use the
pre-generated typemap and are device-verified.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc

@simonrozsival simonrozsival left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Needs Changes (self-review via android-reviewer)

Note: posted as a COMMENT review since GitHub disallows REQUEST_CHANGES on one's own PR.

Solid, well-staged implementation of #10792; the design (pre-generate Mono.Android + Java.Interop together under the Java.Lang.Object universe so one artifact serves both universe modes) is sound and device-verified in both modes (Release NativeAOT single-universe; CoreCLR Debug aggregate). A few correctness/robustness items before merge, plus the documented Debug-NativeAOT limitation.

Counts: ❌ 0 · ⚠️ 2 · 💡 2

Positives:

  • Clear milestone-by-milestone commits with informative messages.
  • Good unit coverage for the new generator paths (reference-only assemblies, generateRootAssembly, framework-universe target attributes).
  • Graceful fallback for the unsupported Debug-NativeAOT combo avoids a hard build regression.

Before merge:

  1. ⚠️ Fix incremental Outputs for the pre-generation target (now produces two typemaps).
  2. ⚠️ Document/justify the hardcoded MaxArrayRank="3" coupling.
  3. 💡 File a tracking issue for the arrays-aggregate NotSupportedException path.
  4. 💡 Reconsider mutating the app acw-map.txt in place.

CI hasn't run on this branch yet — not mergeable until the dotnet-android pipeline is green.

Comment thread build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets Outdated
Comment thread build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets Outdated
- Track both pre-built typemaps in _GeneratePreBuiltMonoAndroidTypeMap
  Outputs (_Mono.Android.TypeMap.dll + _Java.Interop.TypeMap.dll) so the
  incremental state is not broken if the Java.Interop typemap is deleted.
- Replace the hardcoded MaxArrayRank="3" with a documented
  _PreBuiltTypeMapMaxArrayRank property explaining it must match the app
  build's effective rank (BadImageFormatException otherwise).
- Reference the tracking issue (#12128) from the arrays-aggregate
  NotSupportedException and the Debug-NativeAOT fallback comment.
- Stop mutating the app's acw-map.txt in place. Instead build a separate
  deterministic acw-map.prebuilt-merged.txt (app map + pre-built framework
  map, Overwrite=true) and feed that to the NativeAOT R8 keep-rule
  generator, so the app's acw-map.txt stays byte-stable and its own
  incremental checks keep working (no double-append).

Verified: 620 TrimmableTypeMap unit tests pass; Release NativeAOT
(trimmable, R8) sample keeps the pre-built JCWs (classes.dex ~255KB, 310
mono.android keep rules) and launches on device without crashing, while
acw-map.txt is no longer mutated.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
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.

1 participant