Skip to content

[Java.Interop] Map nullable byte jagged arrays to java.lang.Byte#12113

Draft
simonrozsival wants to merge 24 commits into
mainfrom
dev/simonrozsival/fix-legacy-nullable-byte-arrays
Draft

[Java.Interop] Map nullable byte jagged arrays to java.lang.Byte#12113
simonrozsival wants to merge 24 commits into
mainfrom
dev/simonrozsival/fix-legacy-nullable-byte-arrays

Conversation

@simonrozsival

Copy link
Copy Markdown
Member

Summary

  • treat byte? as a managed-to-JNI signature alias of sbyte? for java/lang/Byte in the reflection type manager built-ins
  • preserve Java-to-managed canonicalization of java/lang/Byte as sbyte?
  • cover rank-2 and rank-3 nullable byte jagged arrays in legacy and trimmable type-manager/device tests

Experiment evidence

Before this change, legacy reverse signature lookup had no built-in entry for System.Byte?. Looking up byte?[][] or byte?[][][] therefore failed, and JNIEnv.NewArray() fell back to java.lang.Object[][] / java.lang.Object[][][]. The element values still round-tripped. The trimmable manager already returned the intended [[Ljava/lang/Byte; and [[[Ljava/lang/Byte; signatures.

Behavior change

Legacy byte? reverse lookup now shares the existing nullable signed-byte JNI signature, producing exact java.lang.Byte array classes at ranks 2 and 3. Primitive byte/sbyte behavior, Java-to-managed sbyte? canonicalization, and collection roots are unchanged.

Tests

  • dotnet test tests/Java.Interop-Tests/Java.Interop-Tests.csproj --filter FullyQualifiedName~JniTypeManagerTests (6 passed; supplied installed JDK paths)
  • added device coverage for exact rank-2/rank-3 JNI class names and managed value round-trips; device tests were not run locally

simonrozsival and others added 24 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
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
Base automatically changed from dev/simonrozsival/android-trimmable-revisit-arrays-generic-collections to main July 15, 2026 20:11
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