Skip to content

[TrimmableTypeMap] Add AOT-safe array and collection factories using MakeArrayType and MakeGenericType#12030

Open
simonrozsival wants to merge 13 commits into
mainfrom
dev/simonrozsival/android-trimmable-revisit-arrays-generic-collections
Open

[TrimmableTypeMap] Add AOT-safe array and collection factories using MakeArrayType and MakeGenericType#12030
simonrozsival wants to merge 13 commits into
mainfrom
dev/simonrozsival/android-trimmable-revisit-arrays-generic-collections

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Jul 9, 2026

Copy link
Copy Markdown
Member

Summary

This PR adds runtime-only safe factory helpers for the trimmable type map path:

  • SafeArrayFactory centralizes managed array Type lookup and array instance creation.
  • SafeJavaCollectionFactory centralizes generic Java collection wrapper Type lookup and wrapper creation for JavaList<T>, JavaCollection<T>, and JavaDictionary<TKey,TValue>.
  • ValueTypeFactory<T> is the shared primitive/nullable value-type rooting table used by both factories.
  • JNIEnv, JavaConvert, and TrimmableTypeMapTypeManager now call these helpers instead of carrying their own trimmable-specific construction policy inline.

This also adds boxed nullable primitive array support (int?[], etc.) and runtime coverage for nullable array/list/dictionary marshaling.

Why using MakeArrayType() / MakeGenericType() is OK here

These APIs are annotated with RequiresDynamicCode / RequiresUnreferencedCode because the general case is unsafe in NativeAOT/trimming:

  • arbitrary array element types can lack an array EEType/template;
  • arbitrary constructed generic instantiations can lack a runtime template or constructor metadata;
  • value-type generic instantiations are not canonically shared the same way reference-type instantiations are.

This PR does not suppress those warnings broadly. The suppressions are isolated in the safe factories and only after constraining the shapes to cases we know NativeAOT can support.

The runtime source path this is based on is:

  • RuntimeType.NativeAot.cs -> RuntimeTypeInfo.MakeArrayType() / MakeGenericType()
  • ExecutionEnvironmentImplementation.MappingTables.cs
  • TypeLoaderEnvironment.cs
  • TypeBuilder.TryBuildArrayType() / TypeBuilder.TryBuildGenericType()
  • TemplateLocator.cs
  • StandardCanonicalizationAlgorithm.cs

The key NativeAOT behavior is:

  • Reference types canonicalize to __Canon, and reference arrays/generic instantiations can use canonical templates.
  • Value types remain value-specific, so exact value-type arrays and exact value-type generic instantiations must be rooted.
  • Array.CreateInstanceFromArrayType() immediately needs the array TypeHandle and allocates through RuntimeAugments.NewArray(), so it must only see array types that are actually runtime-buildable/rooted.

The factories enforce that model:

  • Reference arrays use elementType.MakeArrayType() and then Array.CreateInstanceFromArrayType() only for NativeAOT-buildable reference-array canonical shapes.
  • First-rank primitive/nullable value arrays are allocated directly through ValueTypeFactory<T>.CreateArray() (new T[length]), which roots the exact T[] vector.
  • Additional array ranks are jagged SZArrays. Once the first vector exists, each outer wrapper has an array reference type as its element, so it follows the reference-array canonical path.
  • Reference Java collection wrappers use typeof(JavaList<>).MakeGenericType(...), etc., relying on canonical generic templates such as JavaList<__Canon>.
  • Primitive/nullable value Java collection wrappers use ValueTypeFactory<T> direct generic references and constructors, rooting exact instantiations such as JavaList<int> and JavaList<int?>.
  • Mixed reference/value dictionaries root the canonical shapes with dedicated type tokens (JavaDictionary<object,T> and JavaDictionary<T,object>).

Unsupported value-type cases are rejected instead of falling through to unrooted MakeGenericType() / MakeArrayType() usage.

Scope

Included:

  • JavaList<T> / IList<T>
  • JavaCollection<T> / ICollection<T>
  • JavaDictionary<TKey,TValue> / IDictionary<TKey,TValue>
  • Java boxed primitive set and nullable forms: bool, sbyte, char, short, int, long, float, double, plus nullable variants
  • reference-type array and wrapper shapes

Out of scope for this PR:

  • JavaSet<T>
  • byte and unsigned aliases
  • removing generated array proxy code
  • removing JavaPeerContainerFactory

Follow-up

This PR intentionally leaves generated array proxy code, JavaPeerContainerFactory, and related obsolete support code in place. A follow-up PR will remove that generator/runtime cleanup once this runtime-only factory path is validated.

Validation

  • dotnet build src/Mono.Android/Mono.Android.csproj -v:minimal /p:Configuration=Debug --no-restore /p:JavaCPath=/Library/Java/JavaVirtualMachines/microsoft-21.jdk/Contents/Home/bin/javac /p:JarPath=/Library/Java/JavaVirtualMachines/microsoft-21.jdk/Contents/Home/bin/jar /p:JavaPath=/Library/Java/JavaVirtualMachines/microsoft-21.jdk/Contents/Home/bin/java
  • dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj -v:minimal --no-restore

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>
simonrozsival and others added 2 commits July 9, 2026 13:04
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>
@simonrozsival simonrozsival changed the title [TrimmableTypeMap] Add AOT-safe array and collection factories [TrimmableTypeMap] Add AOT-safe array and collection factories using MakeArrayType and MakeGenericType Jul 9, 2026
simonrozsival and others added 3 commits July 9, 2026 13:59
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>
@simonrozsival simonrozsival marked this pull request as ready for review July 9, 2026 21:17
Copilot AI review requested due to automatic review settings July 9, 2026 21:17
@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!

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 refactors the trimmable typemap marshaling path to centralize AOT-safe construction of array and generic Java collection wrapper types, and extends runtime/test coverage to support nullable primitive arrays and nullable list/dictionary marshaling.

Changes:

  • Introduces SafeArrayFactory, SafeJavaCollectionFactory, and ValueTypeFactory<T> to constrain and root AOT-safe array/generic instantiations.
  • Updates JNIEnv, JavaConvert, and TrimmableTypeMapTypeManager to use the new factories and adds nullable primitive array support.
  • Adds new tests and updates APK descriptor baselines to reflect output changes.
Show a summary per file
File Description
tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs Adds runtime tests for nullable IList<T> and IDictionary<TKey,TValue> conversions via JavaConvert.FromJniHandle.
tests/Mono.Android-Tests/Mono.Android-Tests/Android.Runtime/JnienvArrayMarshaling.cs Adds test coverage for JNIEnv nullable primitive array round-tripping and item set/get.
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc Updates expected APK file/package sizes after runtime/type-map changes.
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc Updates expected APK file/package sizes after runtime/type-map changes.
src/Mono.Android/Mono.Android.csproj Adds the new factory helper source files to the build.
src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs Extends NativeAOT array-type discovery to use SafeArrayFactory when a generated proxy isn’t present.
src/Mono.Android/Java.Interop/ValueTypeFactory.cs Adds the shared primitive/nullable rooting table for AOT-safe arrays and collection wrappers.
src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs Adds constrained AOT-safe generic wrapper type creation and handle-to-wrapper creation for lists/collections/dictionaries.
src/Mono.Android/Java.Interop/SafeArrayFactory.cs Adds constrained AOT-safe array Type lookup and instance creation (including jagged arrays).
src/Mono.Android/Java.Interop/JavaConvert.cs Switches trimmable-path generic collection conversion to the new factory-based approach.
src/Mono.Android/Android.Runtime/JNIEnv.cs Routes trimmable array allocation through SafeArrayFactory and adds nullable primitive array marshaling support.

Copilot's findings

  • Files reviewed: 11/11 changed files
  • Comments generated: 4

Comment thread src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs Outdated
Comment thread src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs Outdated
Comment thread src/Mono.Android/Java.Interop/SafeArrayFactory.cs Outdated
Comment thread src/Mono.Android/Java.Interop/ValueTypeFactory.cs Outdated
…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>

@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.

🤖 Android PR Review — ⚠️ Needs Changes (minor)

Runtime-only AOT-safe factories for managed arrays and Java collections on the trimmable-type-map path. The core design is well-reasoned: value types are rooted explicitly (typeof(T[]) / new T[] / direct generic constructor references), reference types ride NativeAOT's __Canon canonical templates, and unsupported value-type shapes are rejected (return null) rather than silently reaching an unrooted MakeGenericType/MakeArrayType. The [UnconditionalSuppressMessage] attributes are narrowly scoped with sound justifications after the shapes have been constrained. 👍

I verified the removed CoreCLR Array.CreateInstance catch-all is not a regression: ArrayCreateInstance is only ever reached with nullable primitives (via CreateManagedArrayFromObjectArray) or reference types (IJavaObject/Array converters). Enums are mapped to their underlying primitive by GetConverter, and GetArray<T> uses new T[cnt] directly — so no non-primitive value type flows into SafeArrayFactory.CreateInstance.

Findings (all inline)

Sev Location Issue
⚠️ SafeJavaCollectionFactory.cs:139 Value/value IDictionary goes through the virtual-generic-method path (CreateDictionaryWithKey<TKey>new JavaDictionary<TKey,T>) — the AOT-riskiest construct here — and has no test. Add an IDictionary<int,long> runtime test under the NativeAOT trimmable config.
💡 SafeJavaCollectionFactory.cs:77 TryCreateInstance(Type, object?, out ...) (construct-from-items) has zero callers.
💡 SafeArrayFactory.cs:19 Throwing GetArrayType(Type,int) overload is unused.
💡 JNIEnv.cs:954 Per-element closure/delegate allocation in CopyManagedObjectArray.

Positives

  • Boxed nullable-primitive array / list / dictionary support with round-trip tests (GetArray_NullableInt32, FromJniHandle_IListNullableInt32, FromJniHandle_IDictionaryNullableInt32String).
  • Clear inline rationale for why the trimmable path avoids the reflection fallback and why each rooting trick exists.
  • Correct JNI local/global ref lifetime handling in the new NewObjectArray helpers.

CI

All completed dotnet-android legs on the previous commit's build were green; the build for the latest commit (b959f4a) was still in progress at review time. Not itself a blocker — but the value/value dictionary test above is worth landing before merge, since that's exactly the instantiation most likely to surface only under NativeAOT.

Generated by Android PR Reviewer for #12030 · 358.6 AIC · ⌖ 28.2 AIC · ⊞ 6.8K
Comment /review to run again

Comment thread src/Mono.Android/Java.Interop/SafeArrayFactory.cs
Comment thread src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs
Comment thread src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs
Comment thread src/Mono.Android/Android.Runtime/JNIEnv.cs Outdated
simonrozsival and others added 2 commits July 9, 2026 23:35
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>
@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 10, 2026
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Jul 10, 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 — ⚠️ Minor suggestions

Reviewed the AOT-safe array/collection factory refactor. Overall this is a well-constructed, carefully-reasoned change — the suppression justifications are precise, the value-type-vs-__Canon rooting model is explained clearly, and the new factories cleanly consolidate construction policy that was previously scattered across JNIEnv, JavaConvert, and TrimmableTypeMapTypeManager. Nullable-primitive array/collection marshaling has good round-trip test coverage (GetArray_NullableInt32, FromJniHandle_IListNullableInt32, FromJniHandle_IDictionaryInt32Int64).

Findings

  • ⚠️ Performance (JavaConvert.cs:79) — collection type is resolved by TryGetCollectionType for a boolean gate, then re-resolved inside TryCreateFromJniHandle, causing MakeGenericType() to run twice per uncached FromJniHandle call for reference generic collections. Inline comment posted.
  • 💡 Formatting (JNIEnv.cs:1106) — use nameof (elementType) instead of the string literal. Inline comment posted.

CI

The GitHub combined status shows pending (Azure DevOps dnceng-public validation not yet complete / no required checks reported). Neither finding is merge-blocking, but confirm the pipeline goes green before merge. The primitive/nullable rooting story is only exercised on the reflection-based runtimes by the added tests; NativeAOT rooting for exact value/value instantiations (JavaDictionary<int,long>) relies on generic-virtual rooting and isn't directly asserted under PublishAot in CI — worth keeping in mind for the validation follow-up.

Nice work on the documentation of the AOT reasoning.

Generated by Android PR Reviewer for #12030 · 144.5 AIC · ⌖ 18.8 AIC · ⊞ 6.8K
Comment /review to run again

Comment thread src/Mono.Android/Java.Interop/JavaConvert.cs Outdated
Comment thread src/Mono.Android/Android.Runtime/JNIEnv.cs Outdated

@jonathanpeppers jonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Merge conflicts

simonrozsival and others added 4 commits July 13, 2026 18:15
…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
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Jul 13, 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 — ⚠️ A couple of questions

Thorough, well-documented refactor. Consolidating the trimmable-typemap construction policy into SafeArrayFactory / SafeJavaCollectionFactory / ValueTypeFactory (with the AOT reasoning captured in comments and UnconditionalSuppressMessage justifications) is a clear improvement over the inline suppressions previously scattered across JNIEnv/JavaConvert. Nice touches: boxed-nullable primitive array support, the BuildRuntimeArrayTypes fallback that mirrors the generator's proxy contract, and the regression test (BuildRuntimeArrayTypes_ReferenceLeaf_ReturnsProxyContract) that guards the previously-uncaught "returns only [T[]]" bug fixed in the latest commit.

Findings

  • ⚠️ Behavioral regression for byte collections on the trimmable path — the old ScalarContainerFactories included typeof (byte); the new factories omit it, so IList<byte> etc. now silently resolve to null rather than marshaling or throwing. Flagged inline on JavaConvert.cs. Documented as out-of-scope, but worth an explicit error instead of a silent null.
  • ⚠️ NativeAOT value/value dictionary rootingValueTypeFactory<T>.CreateDictionaryWithKey<TKey> builds value/value JavaDictionary<TKey,T> via generic-virtual dispatch with no dedicated rooting token; the new test only exercises the Mono/CoreCLR path. Flagged inline on ValueTypeFactory.cs — please confirm NativeAOT coverage.

Other observations (non-blocking)

  • The _ = ReferenceKeyDictionaryType; / ReferenceValueDictionaryType discard-to-root pattern works but is subtle; the existing comments help.
  • SafeArrayFactory.TryCreateInstance handles the rank-1 primitive case both via the explicit short-circuit and again inside TryGetVectorType — minor duplication, not a bug.

CI

Checks are currently pending (no failures reported); the ready-to-review label notes any failures are expected flakes. Not treating CI as blocking, but it should be green before merge.

Great work overall — the two ⚠️ items are questions to resolve rather than definite defects.

Generated by Android PR Reviewer for #12030 · 121.6 AIC · ⌖ 13.4 AIC · ⊞ 6.8K
Comment /review to run again

var factoryConverter = TryGetFactoryBasedConverter (target);
if (factoryConverter != null)
return factoryConverter;
if (SafeJavaCollectionFactory.IsSupportedCollectionType (target)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 ⚠️ Patterns — This narrows the trimmable-typemap collection support relative to the old ScalarContainerFactories/TryGetFactoryBasedConverter, which included typeof (byte). SafeJavaCollectionFactory / ValueTypeFactory.PrimitiveTypeFactories deliberately omit byte (and unsigned aliases), so IList<byte>, ICollection<byte>, and IDictionary<byte,...> now return false here and fall through to null (the non-generic IList/IDictionary checks below don't match generic interfaces). On the trimmable path this is a silent behavioral regression for byte collections that previously marshaled. The PR marks byte as out of scope, but the old path supported it — is dropping it at runtime (vs. throwing a clear NotSupportedException) intended? At minimum consider a targeted error so callers don't get a silent null.

{Rule: Preserve existing behavior / avoid silent regressions}

IntPtr handle,
JniHandleOwnership transfer)
{
return new JavaDictionary<TKey, T> (handle, transfer);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 ⚠️ Trimmer/AOTCreateDictionaryWithKey<TKey> is a generic virtual method that constructs new JavaDictionary<TKey,T>() for value TKey/T (the value/value dictionary path). Generic-virtual dispatch instantiated over value types is one of the most fragile constructs under NativeAOT: each exact JavaDictionary<int,long>, JavaDictionary<int?,double>, ... instantiation (up to ~16×16) must be statically rooted, and unlike the reference/mixed cases there's no dedicated rooting token here — the instantiation is only reachable through this virtual call. The added FromJniHandle_IDictionaryInt32Int64 test exercises this on Mono/CoreCLR, where dynamic code masks the problem; it won't catch a missing NativeAOT template. Can you confirm this is covered by an actual NativeAOT run (e.g. the .NativeAOT.apkdesc scenario), or add explicit rooting tokens for these value/value shapes like the mixed-dictionary case does?

{Rule: NativeAOT template rooting for value-type generic instantiations}

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). trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants