[TrimmableTypeMap][NativeAOT] Initialize trimmable typemap runtime#11292
Conversation
There was a problem hiding this comment.
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.Currentis 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. |
|
Additional NativeAOT testing update:
Arm64-only local Debug-pack measurements:
No fatal/crash logcat hits were found for these runs. The NativeAOT trimmable APK contains only |
|
Follow-up on blanket rooting for NativeAOT trimmable typemap:
Validated NativeAOT trimmable arm64 after the change: Result: I also tried disabling
So |
88ecd21 to
aa91d95
Compare
d49d7f6 to
725cbb3
Compare
028c5d0 to
f48da6d
Compare
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>
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>
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>
Remove the diagnostic InclusionDecision/InclusionReason plumbing from the typemap model while keeping the conditional vs. unconditional entry behavior unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep force-unconditional entries as the default generator behavior, with the MSBuild task setting the init-only generator option from its task property. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the CoreCLR and NativeAOT runtime checks separate instead of combining them, even where the current branch bodies are identical. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop the extra generated typemap assembly summary logger and keep the existing per-assembly type count logging. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Let TypeMapAssemblyGenerator carry framework assembly classification as init state so TrimmableTypeMapGenerator can keep using the wrapper instead of building models and invoking the emitter directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reuse the per-assembly TypeMapAssemblyGenerator instance and make the ModelBuilder.Build arguments explicit for readability. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Route NativeAOT JNI runtime setup through a dedicated initialization entry point so common state, trimmable typemap initialization, and native method registration happen in order after the runtime is current. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
NativeAOT roots array typemap dictionaries from the root typemap assembly, which references per-assembly __ArrayMapRank* anchor types. Keep those anchors public so generated metadata is valid across assembly boundaries. Add test coverage for emitted rank anchor visibility. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ILC needs all per-assembly typemap DLLs in UnmanagedEntryPointsAssembly
to generate vtable symbols for __ArrayMapRank{N} types. Only exclude
the root assembly (_Microsoft.Android.TypeMaps.dll) which has no types
requiring vtable generation.
Previously, _Java.Interop.TypeMap.dll and _Mono.Android.TypeMap.dll
were excluded, causing undefined vtable symbol linker errors:
ld: error: undefined symbol: vtable for _Java_Interop_TypeMap___ArrayMapRank1
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When no array maps are needed (maxArrayRank=0, typical for CoreCLR), emit IL that calls the simpler TrimmableTypeMap.Initialize(typeMap, proxyMap) overload instead of the 3-arg overload with IReadOnlyDictionary<string,Type>?[][]. This avoids potential IL signature resolution issues with the new jagged array overload and matches the Initialize signature that existed before the per-assembly array map changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update new EmitInitializeWith*NoArrays methods to use the TrackedInstructionEncoder API (Call/Return) instead of raw OpCode+Token, matching the API change from main. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mono.Android.dll grew due to new JNIEnvInit helper methods, ITypeMap interface, and NativeAOT runtime feature support. System.IO.Hashing.dll is now included (Crc64 package naming). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Without this, ILLink cannot trim NativeAOT-specific branches in JNIEnvInit.CreateTypeManager() and CreateValueManager(), keeping ManagedTypeManager, JavaMarshalValueManager, and other NativeAOT types alive in MonoVM/CoreCLR APKs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
f48da6d to
39128ae
Compare
Explain why NativeAOT host now supplies Java peer marker classes and restore LF line endings in the MonoVM apkdesc baseline so PR diffs show only real size changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop the PR-local ForceUnconditionalEntries configurability and its tests so that workaround removal can happen in a separate PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…52-nativeaot-typemap-init
Keep framework typemap assemblies as ILC references for conditional TypeMap attributes, but do not pass them as unmanaged entrypoint assemblies in the NativeAOT trimmable path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid discovering trimmable typemap assemblies through directory wildcards so stale outputs cannot flow into packaging or NativeAOT ILC inputs after the generated output set changes. Add coverage for the generated list file and for the NativeAOT contract that framework typemap assemblies stay as ILC references without becoming unmanaged entrypoint roots. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop the unused TypeMapEntryAssembly property from the NativeAOT trimmable typemap target so the target only contains consumed inputs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the runtime-required JavaProxyThrowable JCW when filtering framework-generated Java wrappers in the trimmable typemap path, and add regression coverage for the CoreCLRTrimmable packaging case. Refresh the MonoVM ARM64 apkdesc baselines from the failed CI run so apkdiff no longer crashes on stale System.IO.Hashing entries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep framework typemap data available for NativeAOT while avoiding broad framework array roots. Emit framework array entries only for framework types referenced by app or library assemblies, and keep generated array-rank anchors internal to typemap assemblies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Track _AndroidTrimmableTypeMapMaxArrayRank in build.props and include the property cache in _GenerateTrimmableTypeMap inputs so incremental builds regenerate typemap assemblies when the emitted array-map shape changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the framework Java wrappers required by AndroidMessageHandler certificate validation in trimmable builds. These classes already have R8 keep rules, but the trimmable JCW filter stopped generating their Java sources after framework ACWs became conditional. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Generate trimmable typemap JCWs for all discovered emittable peers instead of maintaining a RequiredFrameworkJcwTypes allowlist. Keep filtering non-emittable peer shapes such as DoNotGenerateAcw, interfaces, and invalid JNI path shapes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/review |
|
✅ Android PR Reviewer completed successfully! |
There was a problem hiding this comment.
✅ LGTM — Well-structured NativeAOT trimmable typemap initialization
Summary: This PR cleanly separates NativeAOT runtime initialization from MonoVM/CoreCLR, introduces framework assembly awareness to the typemap scanner/generator, and correctly scopes ILC inputs to avoid rooting the full framework ACW closure. The size improvements are significant (~30% smaller APK for the HelloWorld sample).
Highlights:
- Good refactoring of
JNIEnvInit—InitializeCommonStateeliminates duplicated field assignments across init paths CreateTypeManager/CreateValueManagerfactory methods are now shared between NativeAOT and MonoVM/CoreCLR- The framework array entry filtering (
MarkFrameworkArrayEntryPeers) is a smart approach — only generating array entries for types actually referenced by app assemblies _ReadGeneratedTrimmableTypeMapAssembliestarget with the list file is a clean solution for passing typemap assembly lists across target boundaries- Good test coverage: unit tests for framework ACW conditional entries, array entry skipping, scanner framework marking, and integration tests for ILC response file validation
Issues found: 0 ❌, 1
| Severity | Category | File |
|---|---|---|
| MSBuild targets | Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets — framework ILC assembly items may reference non-existent files |
|
| 💡 | Documentation | JNIEnvInit.cs:91 — stale comment referencing removed method |
| 💡 | Patterns | JavaPeerInfo.cs — mutable set property on otherwise-immutable record |
| 💡 | Code organization | JNIEnvInit.cs:111-115 — redundant runtime guard |
| 💡 | Code duplication | AndroidRuntimeInternal.cs / JNIEnv.cs — identical NativeAOT and CoreCLR branches |
| 💡 | Positive | Clean validation logic and non-required array property conventions |
CI: All public checks pass (license/cla ✅, dotnet-android ✅).
Generated by Android PR Reviewer for issue #11292 · ● 42.6M
Comments that could not be inline-anchored
src/Mono.Android/Android.Runtime/JNIEnvInit.cs:114
🤖 💡 Code organization — The check on line 111 (!IsNativeAotRuntime) already guarantees that NativeAOT is not active. The second guard on lines 113-115 (IsMonoRuntime || IsCoreClrRuntime) is therefore redundant unless you want to also reject a scenario where none of the three runtime flags are set. If that's the intent, a comment would help clarify.
Rule: Comments explain "why", not "what"
src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs:54
🤖 💡 Patterns — GenerateArrayEntries uses { get; set; } on a sealed record, making it the only mutable property. This is intentionally mutated by MarkFrameworkArrayEntryPeers after construction, but it breaks the otherwise-immutable contract of the record. Worth adding a brief comment explaining why mutability is needed here, or alternatively, consider returning a new collection from MarkFrameworkArrayEntryPeers using with { GenerateArrayEntries = ... } and changing this to `in…
src/Mono.Android/Android.Runtime/JNIEnvInit.cs:91
🤖 💡 Documentation — Stale comment references InitializeJniRuntime, which was renamed to InitializeNativeAotRuntime in this PR. Consider updating to match the new method name.
Rule: Remove stale comments (Postmortem #59)
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets:28
🤖 _TrimmableTypeMapFrameworkIlcAssemblies constructs paths like $(_TypeMapOutputDirectory)_%(Filename).TypeMap.dll based on ResolvedFrameworkAssemblies, PrivateSdkAssemblies, and ReferencePath. If a framework assembly listed here doesn't produce a corresponding typemap DLL (e.g., it has no Java peers), the item will reference a non-existent file. Is ILC tolerant of non-existent -r: references, or should these items be filtered with `Condition=" Exists('%(Id…
src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs:93
🤖 💡 MSBuild tasks — ResolvedFrameworkAssemblies is not marked [Required] (correct), but it's initialized to [] which means it's non-nullable. Good — this is consistent with the convention for non-required array properties.
Rule: Positive callout
src/Mono.Android/Android.Runtime/AndroidRuntimeInternal.cs:22
🤖 💡 Code organization — The NativeAOT branch here is identical to the CoreCLR branch (CoreClrUnhandledException). Similarly in JNIEnv.cs line 140. If these runtimes will always share the same handler, consider combining them: RuntimeFeature.IsCoreClrRuntime || RuntimeFeature.IsNativeAotRuntime. This would reduce duplication and make it clear they intentionally share behavior.
Rule: Code duplication
src/Mono.Android/Android.Runtime/JNIEnvInit.cs:141
🤖 💡 Code organization — Nice refactoring: the runtime-identity validation IsMonoRuntime == IsCoreClrRuntime (exactly-one-must-be-true) is cleaner and more correct than the old IsMonoRuntime && IsCoreClrRuntime (only-both-not-both) check. The new check correctly rejects the case where neither is set.
Rule: Positive callout
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.
Key changes:
JniRuntime.Currentis available.--typemap-entry-assemblyfor the root typemap assembly.JnienvInitializeArgswith GC-user-peer class refs and GREF threshold so managed runtime state matches the Mono/CoreCLR init path.RuntimeFeature.IsNativeAotRuntimeswitch for NativeAOT-specific runtime identity.Fixes #11052.
Current state
The repo HelloWorld sample builds, installs, and launches with NativeAOT + trimmable typemap enabled after merging latest
main.Current clean
android-arm64Release output after mirroring legacy framework JCW generation:libHelloWorld.DotNet.so3,638,382bytes1,569,830bytes3,513,624bytes5,129,326bytes2,132,927bytes5,069,400bytesThe ILC response now uses the normal root typemap assembly, keeps framework typemap assemblies as references, and only exports the app/library typemap assemblies plus
Microsoft.Android.Runtime.NativeAOT:The trimmable NativeAOT output remains smaller than the legacy managed-typemap NativeAOT output after framework JCW generation was broadened to mirror the legacy path.
Latest smoke launch on
emulator-5554was successful:No fatal exception/native crash lines were found in logcat for that launch.
Validation
Results:
make preparepassed.make allpassed.Xamarin.Android.Build.Tasks.csprojbuild passed.Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csprojpassed: 493/493 tests.emulator-5554.