Skip to content

[NativeAOT] Don't run the dead post-ILLink pipeline on trimmable NativeAOT#12124

Closed
simonrozsival wants to merge 35 commits into
dev/simonrozsival/trimmable-typemap-default-nativeaot-2from
dev/simonrozsival/nativeaot-drop-dead-afterlink
Closed

[NativeAOT] Don't run the dead post-ILLink pipeline on trimmable NativeAOT#12124
simonrozsival wants to merge 35 commits into
dev/simonrozsival/trimmable-typemap-default-nativeaot-2from
dev/simonrozsival/nativeaot-drop-dead-afterlink

Conversation

@simonrozsival

Copy link
Copy Markdown
Member

Goal

Stop running the post-ILLink AssemblyModifierPipeline on the trimmable NativeAOT path, where it is dead work.

Stacked on #12121 (dev/simonrozsival/trimmable-typemap-default-nativeaot-2). #12121 flips NativeAOT to the trimmable typemap and, to do so, widened the _RunAfterILLinkAdditionalSteps / _AfterILLinkAdditionalSteps conditions to also run for _AndroidRuntime == 'NativeAOT'. This PR shows that clause is unnecessary and reverts it.

Why it's dead work on trimmable NativeAOT

  • Java Callable Wrappers, the typemap, and the manifest are generated in _GenerateTrimmableTypeMap from the pre-link assemblies, before _LinkAssemblies runs.
  • ILC trims and compiles @(IntermediateAssembly) itself — the IlcCompileInputlinked/ override in _AndroidComputeIlcCompileInputs is gated to the non-trimmable path.
  • So AssemblyModifierPipeline only writes rewritten managed assemblies + *.jlo.xml / *.typemap.xml into obj/.../afterlink/, which on this path nothing consumes: they are not shipped (NativeAOT ships the native .so), not fed to ILC (already ran, from @(IntermediateAssembly)), and not read by _GenerateJavaStubs (which copies the generator's Java output).

Validation

Built samples/NativeAOT (arm64, Release) with the afterlink steps enabled vs. disabled:

APK entry enabled vs disabled
classes.dex byte-identical
resources.arsc, AndroidManifest.xml identical
libNativeAOT.so differs only by native-build non-determinism (control: two afterlink-on builds differ by the same amount)

So the produced app is equivalent. This PR restores the original _RunAfterILLinkAdditionalSteps target and its != 'trimmable' conditions (dropping the _GetAfterILLinkAdditionalStepsInputs split that only existed to compute NativeAOT-specific incremental Inputs).

Benefit

  • Reclaims the AssemblyModifierPipeline run (~1.2s on the HelloWorld sample) plus the afterlink assembly copies.
  • Removes the _GenerateJavaStubs → _AfterILLinkAdditionalSteps → _LinkAssemblies → inner ILC build ordering, which currently forces the Java pipeline to wait on the native compile — a prerequisite for later overlapping the two.

Draft until CI confirms the full matrix (device tests + baselines).

simonrozsival and others added 30 commits July 9, 2026 12:53
Centralize safe runtime construction of arrays and generic Java collection wrappers so JavaConvert and JNIEnv no longer carry the trimmable-specific construction policy inline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fix nullable annotations on factory Try methods and keep mixed dictionary creation from bypassing the explicit value-type support map.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Expand suppression justifications and comments with the source-grounded NativeAOT behavior for array and generic type construction.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Consolidate primitive and nullable value-type rooting for safe arrays and Java collection wrappers into ValueTypeFactory<T>.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update the suppression text to say first-rank value vectors bypass CreateInstanceFromArrayType and are allocated through ValueTypeFactory.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…elines

The new nullable/collection marshaling paths made JavaConvert.GetJniHandleConverter AOT-reachable, exposing the legacy TryMakeGenericCollectionTypeFactory helper whose MakeGenericType() calls only suppressed IL2055 (trimming) and not IL3050 (AOT), leaking 6 NativeAOT warnings. Add the matching IL3050 suppression; that branch only runs when !RuntimeFeature.TrimmableTypeMap (Mono/CoreCLR), never under NativeAOT.

Update BuildReleaseArm64 SimpleDotNet CoreCLR/NativeAOT apkdesc baselines to accept the expected size increase from the rooted array/collection typemap entries.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…d API

- Remove the unused SafeJavaCollectionFactory.TryCreateInstance(Type, object?, out IJavaObject?) speculative overload (no call sites).

- Convert the three new factory files to file-scoped namespaces per repo conventions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Check the value-type factory locals directly instead of via intermediate bools so the compiler tracks the non-null flow, clearing CS8602/CS8604 in the dictionary path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… API, avoid per-element closure

- Add FromJniHandle_IDictionaryInt32Int64 to exercise the value/value dictionary branch (JavaDictionary<int,long> via the generic-virtual CreateDictionaryWithKey<TKey>), confirming that value-over-value instantiation is rooted under the trimmable/NativeAOT config.

- Remove the unused throwing SafeArrayFactory.GetArrayType(Type,int) overload (only TryGetArrayType/CreateInstance are called).

- Inline JavaConvert.WithLocalJniHandle in CopyManagedObjectArray to avoid allocating a capturing closure per array element.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ctions

Resolve conflicts from main's IsDynamicCodeSupported [FeatureGuard] refactor and its generated-proxy collection factory, which landed in the same regions this branch rewrites to runtime Safe*Factory construction:

- JNIEnv.ArrayCreateInstance / TrimmableTypeMapTypeManager.GetArrayTypes: keep main's IsDynamicCodeSupported dynamic-code fast path; route the AOT path through SafeArrayFactory (generated proxy first, then runtime construction).
- JavaConvert.GetJniHandleConverter: replace main's TryGetFactoryBasedConverter (generated-proxy path, removed on this branch) with SafeJavaCollectionFactory; keep main's IsDynamicCodeSupported/ManagedTypeMap else-chain and [RequiresDynamicCode] on TryMakeGenericCollectionTypeFactory.
- BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc: kept the branch baseline; MUST be regenerated from a validation build (sizes shift after the merge).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: fddff664-4661-4c06-8922-316731aa2b8d
…roid-trimmable-revisit-arrays-generic-collections
Addresses the two remaining PR review comments:

- Use `nameof (elementType)` instead of the string literal in
  `CreateManagedArrayFromObjectArray`.

- Gate the trimmable generic-collection converter on a new
  `SafeJavaCollectionFactory.IsSupportedCollectionType ()` check that does
  not resolve (or root) the closed collection type. Previously the gate
  called `TryGetCollectionType ()` (running `MakeGenericType ()`) purely as a
  boolean probe, and `TryCreateFromJniHandle ()` then resolved the type again,
  so `MakeGenericType ()` ran twice per uncached `FromJniHandle ()` marshal
  for reference generic collections. The type is now resolved at most once
  (reference collections), or not at all (value collections handled by the
  rooted `ValueTypeFactory` path).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 155743e0-3f7e-489b-b351-80da73edc7d0
The NativeAOT Java-to-managed array reverse lookup
(TrimmableTypeMapTypeManager.GetArrayTypes) fell back to returning only the
`T[]` vector when no generated array proxy was present. That drops the
JavaObjectArray<T>/JavaArray<T> (reference) and JavaArray<T>/JavaPrimitiveArray<T>/
concrete-primitive (e.g. JavaSByteArray) wrapper types that both the generated
proxy and the CoreCLR path return, so marshaling a Java array to those wrapper
types would fail to resolve once array proxies are no longer pre-generated.

Replace the fallback with BuildRuntimeArrayTypes, which reproduces the generated
JavaArrayProxy.GetArrayTypes() contract (see the generator's
ModelBuilder.GetArrayTypeReferences) at runtime and AOT-safely:

- Primitive leaves use PrimitiveArrayInfo's closed `typeof` tokens.
- Reference leaves build [JavaObjectArray<T>, JavaArray<T>, T[]] via canonical
  (__Canon) MakeGenericType/MakeArrayType, the same rooting the trimmable typemap
  already relies on for reference generics.
- Jagged (rank > 1) mirrors ExpandRankOneTypes.

Behavioral note: non-primitive value-type leaves (e.g. Nullable<int>) return only
the exact rooted vector (int?[]); JavaObjectArray<valueType> would require an
unrooted value-type generic instantiation and the generator never emitted one.

Why no test caught this: the existing TryGetArrayProxy_* tests only assert the
*proxy* path (gated on generated proxies), and the marshaling round-trip tests
only need `T[]`, so the no-proxy runtime fallback was never asserted.
BuildRuntimeArrayTypes is a pure function, so the added
TrimmableTypeMapTypeManagerTests exercise it directly on every config.

Also inline JavaConvert.WithLocalJniHandle in
JNIEnv.SetObjectArrayElementFromManagedValue to avoid allocating a capturing
closure per element on the SetArrayItem path (matching CopyManagedObjectArray).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 155743e0-3f7e-489b-b351-80da73edc7d0
Address review: the removed ScalarContainerFactories supported byte-element
collections (IList<byte>, IDictionary<byte,…>), but ValueTypeFactory only mapped
sbyte, so IsSupportedCollectionType returned false for byte and the trimmable
GetJniHandleConverter fell through to a silent null — a regression versus the
reflection paths (which still handle byte via dynamic MakeGenericType).

Add byte and byte? to ValueTypeFactory.PrimitiveTypeFactories. byte marshals to
java.lang.Byte bitwise, identical to sbyte, and JavaConvert already has the byte
scalar converters, so JavaList<byte>/JavaDictionary<byte,…> round-trip correctly
(including values > 127). byte[] arrays are unaffected: they are handled by
JNIEnv's dedicated primitive-array path (new byte[len]), which never reaches
SafeArrayFactory.

Add a FromJniHandle_IListByte round-trip test (with an unsigned-range value).

Also document, in ValueTypeFactory.CreateDictionaryWithKey, why value/value
dictionaries need no dedicated rooting token: every ValueTypeFactory<X> is
statically constructed, CreateDictionary is virtually reachable for all of them
(instantiating the GVM CreateDictionaryWithKey<X> for every key X), so NativeAOT's
generic-virtual-method dependency analysis roots the full JavaDictionary<X,Y>
cross-product over the fixed primitive/nullable set.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 155743e0-3f7e-489b-b351-80da73edc7d0
Add complete nullable byte array marshaling, parse generic collection shapes once per converter, and add focused NativeAOT trimmable coverage for value/value dictionary rooting.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 98b572c2-766d-43a8-8f1f-3c39df991541
Assert complete runtime array type contracts and exercise collection conversion through JniValueManager without reflection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 98b572c2-766d-43a8-8f1f-3c39df991541
Resolve APK size references using the lower values.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: bec709dd-9aac-4675-91e0-b263414a86c3
Fix nullable byte jagged-array JNI signatures, cover the path with runtime tests, and run the full eligible NativeAOT trimmable suite.

Stop consuming generated array proxies and the legacy container-factory accessor while retaining generator compatibility for follow-up cleanup.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: bec709dd-9aac-4675-91e0-b263414a86c3
Use the measurements produced by Azure DevOps build 1508177 after the runtime factory changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: bec709dd-9aac-4675-91e0-b263414a86c3
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0cae6439-30ce-4c1e-b8f6-b64f3d77d4ad
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0e043be9-9070-43bc-aea4-a48438690bdf
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0e043be9-9070-43bc-aea4-a48438690bdf
NativeAOT now defaults to and requires _AndroidTypeMapImplementation=trimmable:

- Microsoft.Android.Sdk.NativeAOT.targets: default managed -> trimmable; always
  run _PreTrimmingFixLegacyDesignerUpdateItems before NativeCompile.
- Xamarin.Android.Common.targets: error if NativeAOT is used with a non-trimmable
  typemap; run the post-ILLink AssemblyModifierPipeline for NativeAOT+trimmable
  (split out _GetAfterILLinkAdditionalStepsInputs); skip the project proguard config
  for NativeAOT+trimmable.
- Microsoft.Android.Sdk.TypeMap.Trimmable.targets: disable ManagedPeerNativeRegistration
  for trimmable; depend on IlcDynamicBuildPropertyDependencies on NativeAOT.
- JNIEnvInit / JreRuntime: NativeAOT now throws if the trimmable type map is not used,
  and the reflection-backed managers are wrapped in IL2026-suppressed helpers so
  Mono.Android and the NativeAOT runtime host build clean under trimming.

Test infrastructure for follow-up NativeAOT triage:
- BaseTest: IgnoreNativeAotLinkedAssemblyChecks / IgnoreOnNativeAot helpers.
- Mono.Android-Tests: add a TrimmableTypeMapUnsupported excluded category.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mmable path

_PreTrimmingFixLegacyDesignerUpdateItems is only defined in the LlvmIr typemap
targets, which are not imported for trimmable builds. Referencing it from
_AndroidRunNativeCompileDependsOn unconditionally broke NativeAOT (now trimmable by
default) with MSB4057. Restore the per-typemap condition so the trimmable path only
depends on NativeCompile.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
NativeAOT trims with ILC and does not produce illink's obj/<config>/<rid>/linked/
directory, so tests that inspect linked assemblies (or assert the obsolete
PreserveAttribute IL6001 warning, or use the unsupported 'Lowercase' package naming
policy) cannot run as-is on NativeAOT. Guard them with the BaseTest helpers:

- LinkerTests: AndroidAddKeepAlives, AndroidUseNegotiateAuthentication,
  PreserveIX509TrustManagerSubclasses, PreserveServices (linked/ inspection),
  WarnWithReferenceToPreserveAttribute (IL6001).
- BuildTest2: NativeAOT (linked/Mono.Android.dll inspection), BuildReleaseArm64.
- IncrementalBuildTest: AppProjectTargetsDoNotBreak, LinkAssembliesNoShrink (linked/),
  ChangePackageNamingPolicy ('Lowercase' policy unsupported on trimmable).

Verified locally: PreserveIX509TrustManagerSubclasses(NativeAOT) now reports Skipped
instead of DirectoryNotFoundException, while the CoreCLR case still passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ault

- XASdkTests: NativeAOT is now warning-clean (the reflection-manager IL3050/IL3053
  warnings are gone), so assert no warnings instead of one.
- ManifestTest.ExportedErrorMessage: the trimmable manifest generator orders merged
  components differently, so assert the coded AMM0000 error for NativeAOT without the
  exact manifest line/column (verified: NativeAOT case passes).
- BuildWithLibraryTests.ProjectDependencies: the trimmable typemap trims Java Callable
  Wrappers for library types that are never instantiated, so the unused LibraryB JCWs
  are absent from classes.dex by design; skip the NativeAOT case (verified locally:
  the scrc64-named JCW .class files are generated but trimmed out of the dex).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
NativeAOT release builds emit a proguard mapping.txt in the output directory
(confirmed in local NativeAOT build outputs), so add it to the expected file list for
the NativeAOT release case of DotNetBuild.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…chable

Drop the hard error that required _AndroidTypeMapImplementation=trimmable on NativeAOT.
For now this PR only flips the NativeAOT default to trimmable while keeping the existing
managed/llvm-ir configurations reachable (the runtime keeps its ManagedTypeManager /
JavaMarshalValueManager fallbacks). Removing the non-trimmable NativeAOT paths and
re-introducing the error will be done in a separate PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This test inspected illink's linked/Mono.Android.dll and the legacy ManagedTypeMapping
class to verify the managed type-map. With the trimmable typemap now the NativeAOT
default, ILC produces neither that linked/ output nor the ManagedTypeMapping type, so the
test no longer applies. Remove it rather than leaving a permanently-ignored test; a
DGML-based type-map check for NativeAOT can be added as a follow-up.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The test asserts the build fails with XA8000 for SkiaSharp's unresolved
@styleable/SKCanvasView, which relies on FixLegacyResourceDesignerStep. That legacy
resource-designer step is intentionally not run on the trimmable typemap path (the
NativeAOT default), so the diagnostic isn't emitted and the NativeAOT case no longer
applies. Skip it on NativeAOT via the IgnoreOnNativeAot helper.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival and others added 5 commits July 15, 2026 17:27
R8 shrinks bound library Java types (JavaSourceJarTest, JavaSourceTestExtension) out
of classes.dex on the trimmable typemap path because the proguard keep config is
incomplete on NativeAOT, so the class-presence assertions fail. Skip the NativeAOT case
via IgnoreOnNativeAot until the underlying proguard-keep bug is fixed.

Tracked by #11774.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The trimmable typemap generates additional Java Callable Wrappers that
trip XA0102 lint warnings. Ignore on NativeAOT until #11774
is resolved.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The trimmable NativeAOT path generates its ProGuard/R8 ACW keep rules
from the ILC DGML into proguard_project_references.cfg
(_ProguardProjectConfiguration) via GenerateNativeAotProguardConfiguration,
and deliberately leaves R8's proguard_project_primary.cfg empty so that
references.cfg is the sole source of ACW keeps.

However _CalculateProguardConfigurationFiles excluded
_ProguardProjectConfiguration for NativeAOT+trimmable, so R8 received no
ACW keep rules and tree-shook every JCW/ACW (e.g. UncaughtExceptionMarshaler)
out of classes.dex. The app then crashed at startup in
JavaInteropRuntime.init with:

  java.lang.ClassNotFoundException: scrc64...UncaughtExceptionMarshaler

Drop the trimmable exclusion so the generated keep rules reach R8.

Verified on an arm64 emulator with CheckJNI enabled: the HelloWorld
NativeAOT (trimmable) sample now retains the ACW JCWs in classes.dex and
launches to MainActivity without crashing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds NativeAotKeepsRuntimeAcwJavaTypesUnderR8, which builds a Release NativeAOT
app with r8 and asserts that classes.dex still contains the runtime
UncaughtExceptionMarshaler ACW kept by the generated NativeAOT ProGuard rules.
If those keep rules are missing, R8 tree-shakes the runtime JCWs and the app
crashes at startup inside JavaInteropRuntime.init.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…veAOT

The trimmable typemap generates Java Callable Wrappers and the typemap
directly from the pre-link assemblies in _GenerateTrimmableTypeMap, and
ILC trims and compiles @(IntermediateAssembly) itself (the IlcCompileInput
override is gated to the non-trimmable path). On the trimmable NativeAOT
path the post-ILLink AssemblyModifierPipeline therefore only rewrites
managed assemblies into afterlink/ that nothing consumes: they are not
shipped (NativeAOT ships the native .so), not fed to ILC, and not read by
_GenerateJavaStubs (which copies the generator's Java output).

Verified: building samples/NativeAOT with the afterlink steps disabled
produces a byte-identical classes.dex and an equivalent libNativeAOT.so
(differing only by native-build non-determinism), so the pipeline is dead
work on this path.

Restore the original _RunAfterILLinkAdditionalSteps target and its
'!= trimmable' conditions, dropping the NativeAOT enablement (and the
_GetAfterILLinkAdditionalStepsInputs split it required). This reclaims the
AssemblyModifierPipeline run and removes the ordering that forced
_GenerateJavaStubs to wait on the inner ILC build.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/trimmable-typemap-default-nativeaot-2 branch from 3d0d593 to 04f2a51 Compare July 15, 2026 21:03
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