Skip to content

[TrimmableTypeMap][NativeAOT] Initialize trimmable typemap runtime#11292

Draft
simonrozsival wants to merge 7 commits intodev/simonrozsival/trimmable-array-typemapfrom
dev/simonrozsival/11052-nativeaot-typemap-init
Draft

[TrimmableTypeMap][NativeAOT] Initialize trimmable typemap runtime#11292
simonrozsival wants to merge 7 commits intodev/simonrozsival/trimmable-array-typemapfrom
dev/simonrozsival/11052-nativeaot-typemap-init

Conversation

@simonrozsival
Copy link
Copy Markdown
Member

@simonrozsival simonrozsival commented May 5, 2026

Summary

Fix NativeAOT startup with the trimmable typemap implementation enabled, and make the generated trimmable typemap usable for NativeAOT without rooting the full Mono.Android framework ACW closure.

This PR is currently stacked on the trimmable array typemap work in #11238 so the NativeAOT path can use the current array-entry implementation and so the PR shows the real size impact of the combined work.

Key changes:

  • Initialize generated trimmable typemap data before creating the NativeAOT Java.Interop runtime.
  • Register trimmable typemap native methods after JniRuntime.Current is available.
  • Add generated typemap assemblies to ILC inputs before ILC input computation and emit --typemap-entry-assembly for the root typemap assembly.
  • Populate NativeAOT JnienvInitializeArgs with GC-user-peer class refs and GREF threshold so managed runtime state matches the Mono/CoreCLR init path.
  • Add an explicit RuntimeFeature.IsNativeAotRuntime switch for NativeAOT-specific runtime identity.
  • Keep scanning framework assemblies for typemap entries, but treat framework ACW implementors as conditional trim-target entries instead of unconditional user ACW roots.
  • Compile the shared __ArrayMapRankN anchors into Mono.Android so NativeAOT can resolve the generated array typemap groups.

Fixes #11052.

Current state

The repo HelloWorld sample now builds, installs, and launches with NativeAOT + trimmable typemap enabled.

Current clean android-arm64 Release output:

Artifact Size
signed APK 3,601,583 bytes
signed AAB 1,543,717 bytes
libHelloWorld.DotNet.so 3,481,288 bytes

This is about 1.4 MiB smaller than the earlier NativeAOT managed/legacy typemap result measured in this investigation, while startup remains in the same range. The latest smoke launch on emulator-5554 was successful:

Status: ok
LaunchState: COLD
TotalTime: 254
WaitTime: 258

The size is not the final expected floor. There is still obvious room to improve as we continue identifying and reducing NativeAOT codegen and metadata roots; this PR gets the NativeAOT trimmable typemap path into a working, measurable state so those follow-up reductions can be targeted with real data.

Validation

MSBUILDDISABLENODEREUSE=1 ./dotnet-local.sh build \
  src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj \
  -c Debug -nr:false --nologo

MSBUILDDISABLENODEREUSE=1 ./dotnet-local.sh test \
  tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj \
  -c Debug -nr:false --nologo

MSBUILDDISABLENODEREUSE=1 ./dotnet-local.sh test \
  src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj \
  -c Debug -nr:false --nologo \
  --filter FullyQualifiedName~GenerateTrimmableTypeMapTests

Results:

  • Xamarin.Android.Build.Tasks.csproj build passed.
  • Microsoft.Android.Sdk.TrimmableTypeMap.Tests: 457/457 passed.
  • focused GenerateTrimmableTypeMapTests: 9/9 passed.
  • clean HelloWorld NativeAOT trimmable build passed with no __ArrayMapRank ILC failure.
  • HelloWorld installed and launched successfully on emulator-5554; logcat showed libHelloWorld.DotNet.so loaded and no fatal exception/native crash.

Copilot AI review requested due to automatic review settings May 5, 2026 17:03
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 wires the trimmable typemap path into NativeAOT startup and build-time ILC inputs so NativeAOT apps can initialize the managed/runtime typemap state before Java interop begins.

Changes:

  • Adds NativeAOT-specific build plumbing for trimmable typemap assemblies and runtime feature switches.
  • Updates NativeAOT host/runtime initialization so typemap data is initialized earlier and native registration happens after JniRuntime.Current is available.
  • Aligns NativeAOT runtime state with the Mono/CoreCLR init path for GREF/GC-user-peer setup and runtime identification.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets Adds typemap assemblies to NativeAOT ILC inputs and sets the typemap entry assembly.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk.NativeAOT.targets Adds a NativeAOT runtime feature switch to host configuration.
src/native/nativeaot/host/host.cc Populates NativeAOT init args with GREF threshold and GC-user-peer class refs.
src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs Introduces IsNativeAotRuntime runtime identity switch.
src/Mono.Android/Android.Runtime/JNIEnvInit.cs Splits NativeAOT typemap initialization/registration into explicit helper steps and applies more init state from args.
src/Mono.Android/Android.Runtime/JNIEnv.cs Treats NativeAOT like CoreCLR for uncaught exception propagation.
src/Mono.Android/Android.Runtime/AndroidRuntimeInternal.cs Treats NativeAOT like CoreCLR for unhandled-exception callback selection.
src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs Reorders NativeAOT startup to initialize typemap data before runtime creation and register natives afterward.

<ItemGroup>
<IlcReference Include="@(_GeneratedTypeMapAssemblies)" />
<UnmanagedEntryPointsAssembly Include="@(_GeneratedTypeMapAssemblies)" />
<_TrimmableTypeMapIlcAssemblies Include="$(_TypeMapOutputDirectory)*.dll" />
Comment on lines 12 to +16
<Target Name="_AddTrimmableTypeMapAssembliesToIlc"
AfterTargets="_GenerateJavaStubs"
Condition=" '@(_GeneratedTypeMapAssemblies->Count())' != '0' ">
BeforeTargets="_AndroidComputeIlcCompileInputs"
DependsOnTargets="_GenerateTrimmableTypeMap">
<PropertyGroup>
<TypeMapEntryAssembly>$(_TypeMapAssemblyName)</TypeMapEntryAssembly>
@simonrozsival
Copy link
Copy Markdown
Member Author

Additional NativeAOT testing update:

  • Made ForceUnconditionalEntries configurable and disabled it for NativeAOT trimmable typemap builds only.
  • Rebuilt HelloWorld as single-RID android-arm64 across the measured configs using -p:RuntimeIdentifier=android-arm64.

Arm64-only local Debug-pack measurements:

Variant APK size TotalTime avg / median Runs
Mono 8.47 MiB 242.8 / 210 ms 414, 211, 198, 210, 181
CoreCLR llvm-ir typemap 14.64 MiB 259.6 / 235 ms 381, 210, 235, 218, 254
CoreCLR trimmable typemap 24.70 MiB 290.0 / 264 ms 404, 304, 229, 249, 264
NativeAOT original managed typemap 4.86 MiB 169.8 / 163 ms 230, 165, 152, 139, 163
NativeAOT trimmable typemap, ForceUnconditionalEntries=false 11.84 MiB 186.6 / 170 ms 262, 164, 171, 170, 166

No fatal/crash logcat hits were found for these runs. The NativeAOT trimmable APK contains only lib/arm64-v8a/libHelloWorld.DotNet.so after switching to RuntimeIdentifier=android-arm64.

@simonrozsival
Copy link
Copy Markdown
Member Author

Follow-up on blanket rooting for NativeAOT trimmable typemap:

  • The current NativeAOT trimmable ILC response has no --root: entries and no root descriptors.
  • I found one real blanket root item: we were passing every generated typemap assembly to UnmanagedEntryPointsAssembly, which emitted:
    • _HelloLibrary.DotNet.TypeMap
    • _HelloWorld.DotNet.TypeMap
    • _Java.Interop.TypeMap
    • _Microsoft.Android.TypeMaps
    • _Mono.Android.TypeMap
  • I narrowed this so only app/library typemap assemblies are exported for JNI UCO wrappers. Framework/root typemap assemblies stay as IlcReference inputs, but are no longer unmanaged-entrypoint roots.

Validated NativeAOT trimmable arm64 after the change:

--scanreflection
--generateunmanagedentrypoints:System.Private.CoreLib,HIDDEN
--generateunmanagedentrypoints:_HelloLibrary.DotNet.TypeMap
--generateunmanagedentrypoints:_HelloWorld.DotNet.TypeMap
--generateunmanagedentrypoints:Microsoft.Android.Runtime.NativeAOT

Result:

APK: 13,124,718 bytes / 12.52 MiB
libHelloWorld.DotNet.so: 13,000,904 bytes / 12.40 MiB
TotalTime runs: 266, 166, 165, 168, 169 ms
TotalTime avg/median: 186.8 / 168.0 ms
No fatal/crash logcat hits

I also tried disabling IlcScanReflection for this path. That produced much smaller APKs, but it is not currently runnable:

  • without scanreflection: APK 3.22 MiB, crash in TypeMapLazyDictionary.CreateExternalTypeMap(RuntimeType) for Java.Lang.Object
  • with narrow rd.xml / dynamic dependencies: APK ~4.0-4.3 MiB, same crash
  • with IlcGenerateCompleteTypeMetadata=true: APK 13.76 MiB, same crash

So --scanreflection appears to be required today for ILC to emit the NativeAOT type-map blob consumed by TypeMapLazyDictionary, even though --typemap-entry-assembly:_Microsoft.Android.TypeMaps is present. I did not commit the broken no-scan experiment.

Wire the trimmable typemap into NativeAOT startup and ILC inputs so generated typemap assemblies are included in the NativeAOT closure and runtime state is initialized before managed peer creation.

Also add an explicit NativeAOT runtime feature switch for runtime code paths that should not be treated as MonoVM.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival marked this pull request as draft May 5, 2026 21:41
Make ForceUnconditionalEntries configurable and disable it for NativeAOT trimmable typemap testing so framework bindings can be conditionally rooted.

Keep the existing workaround enabled by default for other trimmable typemap configurations.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival changed the title [NativeAOT] Initialize trimmable typemap runtime [TrimmableTypeMap][NativeAOT] Initialize trimmable typemap runtime May 5, 2026
simonrozsival and others added 5 commits May 5, 2026 23:46
Keep generated framework typemap assemblies as ILC references for type-map metadata, but do not pass them as UnmanagedEntryPointsAssembly inputs. This avoids treating framework typemap assemblies as unmanaged-entrypoint roots while preserving app typemap exports.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Clarify the ForceUnconditionalEntries plumbing and make the NativeAOT unmanaged-entrypoint assembly filtering easier to read without changing behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Check each NativeAOT GC peer FindClass result before continuing so a failed lookup does not leave a pending JNI exception while another JNI lookup runs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
NativeAOT trimmable typemap generation scans framework assemblies so runtime-needed framework peers remain available, but framework ACW implementors should not be blanket unconditional roots. Classify framework inputs before model generation and emit framework ACWs as conditional trim-target entries.

Add summary logging and regression coverage for duplicate MSBuild input items where only one copy carries framework metadata.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Include ArrayMapAnchors.cs in Mono.Android so generated per-rank array TypeMap entries can resolve their shared Microsoft.Android.Runtime.__ArrayMapRankN anchor types at NativeAOT compile time.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/11052-nativeaot-typemap-init branch from 88ecd21 to aa91d95 Compare May 5, 2026 22:02
@simonrozsival simonrozsival changed the base branch from main to dev/simonrozsival/trimmable-array-typemap May 5, 2026 22:10
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.

[TrimmableTypeMap] NativeAOT end-to-end validation

2 participants