[NativeAOT] Don't run the dead post-ILLink pipeline on trimmable NativeAOT#12124
Closed
simonrozsival wants to merge 35 commits into
Closed
Conversation
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>
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
force-pushed
the
dev/simonrozsival/trimmable-typemap-default-nativeaot-2
branch
from
July 15, 2026 21:03
3d0d593 to
04f2a51
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Goal
Stop running the post-ILLink
AssemblyModifierPipelineon 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/_AfterILLinkAdditionalStepsconditions to also run for_AndroidRuntime == 'NativeAOT'. This PR shows that clause is unnecessary and reverts it.Why it's dead work on trimmable NativeAOT
_GenerateTrimmableTypeMapfrom the pre-link assemblies, before_LinkAssembliesruns.@(IntermediateAssembly)itself — theIlcCompileInput→linked/override in_AndroidComputeIlcCompileInputsis gated to the non-trimmable path.AssemblyModifierPipelineonly writes rewritten managed assemblies +*.jlo.xml/*.typemap.xmlintoobj/.../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:classes.dexresources.arsc,AndroidManifest.xmllibNativeAOT.soSo the produced app is equivalent. This PR restores the original
_RunAfterILLinkAdditionalStepstarget and its!= 'trimmable'conditions (dropping the_GetAfterILLinkAdditionalStepsInputssplit that only existed to compute NativeAOT-specific incrementalInputs).Benefit
AssemblyModifierPipelinerun (~1.2s on the HelloWorld sample) plus the afterlink assembly copies._GenerateJavaStubs → _AfterILLinkAdditionalSteps → _LinkAssemblies → inner ILC buildordering, 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).