Skip to content

[TrimmableTypeMap][NativeAOT] Auto-detect and scan runtime-only assemblies (fixes stripped runtime ACWs)#11982

Open
simonrozsival wants to merge 1 commit into
mainfrom
dev/simonrozsival/nativeaot-runtime-host-refasm
Open

[TrimmableTypeMap][NativeAOT] Auto-detect and scan runtime-only assemblies (fixes stripped runtime ACWs)#11982
simonrozsival wants to merge 1 commit into
mainfrom
dev/simonrozsival/nativeaot-runtime-host-refasm

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Jul 4, 2026

Copy link
Copy Markdown
Member

Summary

Under the trimmable typemap on NativeAOT, the runtime host assembly Microsoft.Android.Runtime.NativeAOT — and its only Java Callable Wrapper type, UncaughtExceptionMarshaler — was never scanned by the typemap generator. Its JCW, typemap entry, and acw-map entry were all missing, so R8 had nothing to keep, the ACW was dropped from classes.dex, and the app crashed at startup in JavaInteropRuntime.init (setDefaultUncaughtExceptionHandler).

More generally: any runtime-only assembly with Java peers was invisible to the typemap generator. This fixes the whole class of problem, not just UncaughtExceptionMarshaler.

Root cause

_GenerateTrimmableTypeMap runs in the RID-independent outer build over @(ReferencePath) (the compile closure: app + references + ref-pack framework assemblies). Runtime-only assemblies have no ref-pack counterpart and are only pulled from the RID-specific runtime pack in the per-RID inner build, so they never enter the generator's input set. Runtime-pack resolution (ResolveRuntimePackAssets) fundamentally requires a single RuntimeIdentifier.

Fix — auto-detect via the SDK's own resolution

No hard-coded assembly names, no per-assembly reference-assembly plumbing:

  • _ResolveRuntimeOnlyAssembliesForTypeMap (Microsoft.Android.Sdk.AssemblyResolution.targets): a resolve-only sibling of _ComputeFilesToPublishForRuntimeIdentifiers that stops after ResolveReferences (no ComputeFilesToPublish/ILLink/ILC/AOT) and returns the Android runtime-pack managed assemblies (Microsoft.Android.Runtime.*) that have no @(ReferencePath) counterpart (matched on Filename, so Mono.Android/Java.Interop aren't double-scanned; the .NET/BCL runtime pack is excluded).
  • _ResolveRuntimeOnlyAssembliesForTrimmableTypeMap + _AddRuntimeOnlyAssembliesToTrimmableTypeMap (Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets): before _ResolveAssemblies spawns the per-RID ILC builds, run that target once via the MSBuild task for the first RID (managed metadata is RID-independent) and cache the result. The nested resolve is gated on Inputs/Outputs so it does not re-run on incremental no-op builds; an always-run loader reads the cached list into the generator's extra-framework input (_AndroidTrimmableTypeMapExtraFrameworkAssembly, plumbed through Microsoft.Android.Sdk.TypeMap.Trimmable.targets).
  • ILC framework classification generalized: any runtime-pack managed assembly's per-assembly typemap DLL is treated as framework (IlcReference but not an unmanaged-entrypoint root), matching Mono.Android/Java.Interop, so its JCW native methods register via the runtime registerNatives path.

Test

Adds Build_WithTrimmableTypeMap_KeepsNativeAotRuntimeHostAcws, which opts into the trimmable typemap on NativeAOT and asserts classes.dex retains the UncaughtExceptionMarshaler runtime ACW.

Validation (local, -c Release)

  • Build_WithTrimmableTypeMap_KeepsNativeAotRuntimeHostAcws passes.
  • A NativeAOT + trimmable app build now generates _Microsoft.Android.Runtime.NativeAOT.TypeMap.dll (with the UncaughtExceptionMarshalerscrc64…/UncaughtExceptionMarshaler entry + proxy), the JCW .java, and the acw-map entry.
  • The runtime host typemap DLL is an IlcReference and not an UnmanagedEntryPointsAssembly (correct framework classification).
  • No double-scanning of framework assemblies (one _Mono.Android.TypeMap.dll in the generator output).
  • Incremental no-op builds skip the nested resolve (verified via binlog: _ResolveRuntimeOnlyAssembliesForTrimmableTypeMap up-to-date).

Notes

The trimmable typemap is opt-in on main (_AndroidTypeMapImplementation=trimmable); this fix is dormant otherwise. Extracted from the "make trimmable the NativeAOT default" work (#11822) so it can be reviewed and merged independently.

Issue references

Contributes to #10933 and #10793.

Copilot AI review requested due to automatic review settings July 4, 2026 20:36

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 fixes a NativeAOT + trimmable typemap startup crash by ensuring the typemap generator scans the runtime-only Microsoft.Android.Runtime.NativeAOT assembly (via a shipped reference assembly), so its runtime ACW(s) (notably UncaughtExceptionMarshaler) are kept by R8 and appear in classes.dex.

Changes:

  • Add a new build test asserting UncaughtExceptionMarshaler is present in classes.dex for NativeAOT + trimmable typemap + R8.
  • Extend _GenerateTrimmableTypeMap inputs to include an extra framework assembly item list for runtime-only assemblies.
  • Produce + package a reference assembly for Microsoft.Android.Runtime.NativeAOT under tools/typemap-refs, and wire it into the NativeAOT trimmable typemap build.
Show a summary per file
File Description
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs Adds regression test verifying the runtime ACW class is retained in classes.dex.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets Feeds additional runtime-only framework assemblies into typemap generation inputs.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets Injects the NativeAOT runtime host ref assembly into typemap inputs; classifies its per-assembly typemap DLL as framework for ILC.
src/Microsoft.Android.Runtime.NativeAOT/Microsoft.Android.Runtime.NativeAOT.csproj Enables ProduceReferenceAssembly and copies the ref assembly into the SDK pack’s typemap-refs staging area.
build-tools/create-packs/Microsoft.Android.Sdk.proj Packages the NativeAOT runtime host reference assembly under tools/typemap-refs.

Copilot's findings

  • Files reviewed: 5/5 changed files
  • Comments generated: 2

@simonrozsival simonrozsival reopened this Jul 4, 2026
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/nativeaot-runtime-host-refasm branch from 8da90c2 to b033cb1 Compare July 4, 2026 22:06
@simonrozsival simonrozsival changed the title [TrimmableTypeMap][NativeAOT] Scan runtime host ACWs via a ref assembly [TrimmableTypeMap][NativeAOT] Auto-detect runtime-only ACW assemblies Jul 4, 2026
@simonrozsival simonrozsival changed the title [TrimmableTypeMap][NativeAOT] Auto-detect runtime-only ACW assemblies [TrimmableTypeMap][NativeAOT] Auto-detect and scan runtime-only assemblies (fixes stripped runtime ACWs) Jul 4, 2026
Under the trimmable typemap on NativeAOT, the runtime host assembly
Microsoft.Android.Runtime.NativeAOT - and its only Java Callable Wrapper type,
UncaughtExceptionMarshaler - was never scanned by the typemap generator, so its
JCW/typemap/acw-map entries were missing, R8 had nothing to keep, and the app
crashed at startup in JavaInteropRuntime.init (setDefaultUncaughtExceptionHandler).

Root cause: _GenerateTrimmableTypeMap runs in the RID-independent OUTER build over
@(ReferencePath), the compile closure, which omits runtime-only assemblies (no
ref-pack counterpart) that are only pulled from the RID-specific runtime pack in the
per-RID inner build. Runtime-pack resolution (ResolveRuntimePackAssets) requires a
single RuntimeIdentifier.

Fix - discover them automatically from the SDK's own resolution (no hard-coded
assembly names, no per-assembly reference-assembly plumbing):

* _ResolveRuntimeOnlyAssembliesForTypeMap (AssemblyResolution.targets): a
  resolve-only sibling of _ComputeFilesToPublishForRuntimeIdentifiers that stops after
  ResolveReferences (no ComputeFilesToPublish/ILLink/ILC/AOT) and returns the Android
  runtime-pack managed assemblies (Microsoft.Android.Runtime.*) with no @(ReferencePath)
  counterpart (matched on Filename, so Mono.Android/Java.Interop are not double-scanned;
  the .NET/BCL runtime pack is excluded).

* _ResolveRuntimeOnlyAssembliesForTrimmableTypeMap + _AddRuntimeOnlyAssembliesToTrimmableTypeMap
  (NativeAOT.targets): before _ResolveAssemblies spawns the per-RID ILC builds, run that
  target once via the MSBuild task for the first RID (managed metadata is RID-independent)
  and cache the result. The nested resolve is gated on Inputs/Outputs so it does not
  re-run on incremental no-op builds; an always-run loader reads the cached list into the
  generator's extra-framework input.

ILC framework classification is generalized: any runtime-pack managed assembly's
per-assembly typemap DLL is treated as framework (IlcReference but not an
unmanaged-entrypoint root), matching Mono.Android/Java.Interop.

Adds Build_WithTrimmableTypeMap_KeepsNativeAotRuntimeHostAcws, which opts into the
trimmable typemap on NativeAOT and asserts classes.dex retains the
UncaughtExceptionMarshaler runtime ACW.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/nativeaot-runtime-host-refasm branch from b033cb1 to 8abcbfe Compare July 7, 2026 22:43
@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 8, 2026
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Android PR Reviewer completed successfully!

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 Code Review Summary

Verdict: ⚠️ Minor changes suggested — the MSBuild logic is well-designed and correct, and CI is fully green (42/42 dotnet-android checks passing on Azure DevOps). The two items below are non-blocking.

What this PR does well

  • Solid root-cause fix. Runtime-only assemblies (the NativeAOT runtime host Microsoft.Android.Runtime.NativeAOT) were never in @(ReferencePath) for the RID-independent outer-build typemap generator. The auto-detect approach (a resolve-only sibling of _ComputeFilesToPublishForRuntimeIdentifiers that stops after ResolveReferences) is the right layer to solve this at, and generalizes to any runtime-only Java-peer assembly rather than hard-coding UncaughtExceptionMarshaler.
  • Incrementality is carefully handled. The expensive nested resolve is gated on Inputs/Outputs, results are cached to runtime-only-assemblies.txt, and an always-run loader feeds the cached list into the generator's Inputs so the generator's own up-to-date check stays stable. The WriteOnlyWhenDifferent + FileWrites usage is correct.
  • Re-entrancy is safe. The nested build sets _OuterIntermediateOutputPath, so _ResolveRuntimeOnlyAssembliesForTrimmableTypeMap/_AddRuntimeOnlyAssembliesToTrimmableTypeMap are correctly skipped inside it (no infinite recursion), mirroring the existing _ResolveAssemblies pattern.
  • Good regression test. Build_WithTrimmableTypeMap_KeepsNativeAotRuntimeHostAcws asserts the UncaughtExceptionMarshaler ACW survives in classes.dex, correctly matching the CRC-hashed class-name suffix and gating on IgnoreUnsupportedConfiguration.

Findings (2 inline comments)

Severity Category Location
⚠️ Documentation TrimmableTypeMapBuildTests.cs:96 — stale "reference assembly is shipped" comment contradicts the actual resolve-and-cache implementation.
💡 MSBuild targets ...TypeMap.Trimmable.NativeAOT.targets:37 — ILC framework classification includes the BCL runtime pack, unlike the resolve target which filters it out; harmless but asymmetric.

Neither blocks merge on correctness; the ⚠️ is a doc-accuracy fix worth making before merge. Since the trimmable typemap is opt-in on main, the risk surface is limited to _AndroidTypeMapImplementation=trimmable builds.

Reviewed against the dotnet/android MSBuild, testing, and repo-convention guidelines.

Generated by Android PR Reviewer for #11982 · 206.2 AIC · ⌖ 22.1 AIC · ⊞ 6.8K
Comment /review to run again

// generator never scanned it. Its only Java Callable Wrapper type, UncaughtExceptionMarshaler,
// therefore had no JCW and no typemap entry -> the runtime ACW is absent from classes.dex and the
// app crashes at startup in JavaInteropRuntime.init (setDefaultUncaughtExceptionHandler). A
// reference assembly for the host is now shipped in the SDK pack and fed to the generator. The JCW

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 ⚠️ Documentation — This comment claims a "reference assembly for the host is now shipped in the SDK pack and fed to the generator," but that is not what the PR does. The final implementation auto-detects runtime-only assemblies at build time via the nested _ResolveRuntimeOnlyAssembliesForTypeMap resolve pass — the PR description itself states "No hard-coded assembly names, no per-assembly reference-assembly plumbing." This stale wording describes the abandoned ref-assembly approach (cf. the ...-refasm branch name) and will mislead anyone debugging this area later. Please reword it to describe the resolve-and-cache mechanism that the test actually exercises.

Rule: Remove stale comments (Postmortem #59)

(ILC reads its conditional TypeMap attributes) but is removed from the unmanaged-entrypoint
roots, matching Mono.Android/Java.Interop. Their JCW native methods register via the runtime
registerNatives path. -->
<_TrimmableTypeMapFrameworkIlcAssemblyNames Include="@(RuntimePackAsset->'_%(Filename).TypeMap')"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 💡 MSBuild targets — This includes every runtime-pack managed DLL, including the .NET/BCL runtime pack (Microsoft.NETCore.App.Runtime.*), whereas the sibling _ResolveRuntimeOnlyAssembliesForTypeMap deliberately filters to Microsoft.Android.Runtime and excludes the BCL. It is harmless today — these names only feed the Remove on _TrimmableTypeMapUnmanagedEntryPointAssemblyNames, and BCL assemblies have no generated _*.TypeMap DLL to remove — but the asymmetry between the two targets is a maintenance trap and inflates the item group with hundreds of no-op BCL names. Consider matching the resolve target's StartsWith('Microsoft.Android.Runtime') filter here, or add a note explaining why the broader set is intentional.

Rule: Consistency with sibling targets

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.

2 participants