[TrimmableTypeMap][NativeAOT] Auto-detect and scan runtime-only assemblies (fixes stripped runtime ACWs)#11982
Conversation
There was a problem hiding this comment.
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
UncaughtExceptionMarshaleris present inclasses.dexfor NativeAOT + trimmable typemap + R8. - Extend
_GenerateTrimmableTypeMapinputs to include an extra framework assembly item list for runtime-only assemblies. - Produce + package a reference assembly for
Microsoft.Android.Runtime.NativeAOTundertools/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
8da90c2 to
b033cb1
Compare
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>
b033cb1 to
8abcbfe
Compare
|
/review |
|
✅ Android PR Reviewer completed successfully! |
There was a problem hiding this comment.
🤖 Code Review Summary
Verdict: 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_ComputeFilesToPublishForRuntimeIdentifiersthat stops afterResolveReferences) is the right layer to solve this at, and generalizes to any runtime-only Java-peer assembly rather than hard-codingUncaughtExceptionMarshaler. - Incrementality is carefully handled. The expensive nested resolve is gated on
Inputs/Outputs, results are cached toruntime-only-assemblies.txt, and an always-run loader feeds the cached list into the generator'sInputsso the generator's own up-to-date check stays stable. TheWriteOnlyWhenDifferent+FileWritesusage is correct. - Re-entrancy is safe. The nested build sets
_OuterIntermediateOutputPath, so_ResolveRuntimeOnlyAssembliesForTrimmableTypeMap/_AddRuntimeOnlyAssembliesToTrimmableTypeMapare correctly skipped inside it (no infinite recursion), mirroring the existing_ResolveAssembliespattern. - Good regression test.
Build_WithTrimmableTypeMap_KeepsNativeAotRuntimeHostAcwsasserts theUncaughtExceptionMarshalerACW survives inclasses.dex, correctly matching the CRC-hashed class-name suffix and gating onIgnoreUnsupportedConfiguration.
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 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 |
There was a problem hiding this comment.
🤖 _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')" |
There was a problem hiding this comment.
🤖 💡 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
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 fromclasses.dex, and the app crashed at startup inJavaInteropRuntime.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
_GenerateTrimmableTypeMapruns 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 singleRuntimeIdentifier.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_ComputeFilesToPublishForRuntimeIdentifiersthat stops afterResolveReferences(noComputeFilesToPublish/ILLink/ILC/AOT) and returns the Android runtime-pack managed assemblies (Microsoft.Android.Runtime.*) that have no@(ReferencePath)counterpart (matched onFilename, soMono.Android/Java.Interoparen't double-scanned; the .NET/BCL runtime pack is excluded)._ResolveRuntimeOnlyAssembliesForTrimmableTypeMap+_AddRuntimeOnlyAssembliesToTrimmableTypeMap(Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets): before_ResolveAssembliesspawns 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 onInputs/Outputsso 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 throughMicrosoft.Android.Sdk.TypeMap.Trimmable.targets).IlcReferencebut not an unmanaged-entrypoint root), matchingMono.Android/Java.Interop, so its JCW native methods register via the runtimeregisterNativespath.Test
Adds
Build_WithTrimmableTypeMap_KeepsNativeAotRuntimeHostAcws, which opts into the trimmable typemap on NativeAOT and assertsclasses.dexretains theUncaughtExceptionMarshalerruntime ACW.Validation (local,
-c Release)Build_WithTrimmableTypeMap_KeepsNativeAotRuntimeHostAcwspasses._Microsoft.Android.Runtime.NativeAOT.TypeMap.dll(with theUncaughtExceptionMarshaler→scrc64…/UncaughtExceptionMarshalerentry + proxy), the JCW.java, and the acw-map entry.IlcReferenceand not anUnmanagedEntryPointsAssembly(correct framework classification)._Mono.Android.TypeMap.dllin the generator output)._ResolveRuntimeOnlyAssembliesForTrimmableTypeMapup-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.