Skip to content

[TrimmableTypeMap] Fix String and nullable array proxies#12017

Open
simonrozsival wants to merge 11 commits into
mainfrom
dev/simonrozsival/trimmable-typemap-array-nullable
Open

[TrimmableTypeMap] Fix String and nullable array proxies#12017
simonrozsival wants to merge 11 commits into
mainfrom
dev/simonrozsival/trimmable-typemap-array-nullable

Conversation

@simonrozsival

Copy link
Copy Markdown
Member

Summary

Independent trimmable typemap array-proxy fixes extracted from #11822, so the NativeAOT default-flip PR can stay focused.

  • Emit built-in array proxy entries for System.String and boxed Nullable<T> value types, including Java.Interop GetType coverage for boxed nullable arrays.
  • Split array proxy rank defaults: primitive arrays keep _AndroidTrimmableTypeMapMaxArrayRank (3 under AOT), while reference-like arrays use _AndroidTrimmableTypeMapMaxReferenceArrayRank (1 under AOT) to avoid generating/compiling large rank-2/3 reference-array closures by default.
  • Fix nullable value type proxy codegen and make JavaPeerProxy's base constructor accessible to generated typemap proxy assemblies.

Testing

  • dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj → 613 passed

Notes

Extracted from #11822. This does not change the NativeAOT default; it only fixes the opt-in trimmable typemap array behavior.

Issue references

Contributes to #10933 and #10793.

simonrozsival and others added 8 commits July 8, 2026 09:50
…pping

GetTypes ("[Ljava/lang/String;") dropped the System.String array family
(System.String[], JavaObjectArray<string>, JavaArray<string>) on NativeAOT.

The runtime enumerates array element types via GetTypesForSimpleReference,
which injects System.String for java/lang/String through
GetBuiltInTypeForSimpleReference. But System.String is neither a scanned Java
peer (EmitArrayEntriesForPeer) nor a primitive (EmitPrimitiveArrayEntries), so
no array proxy was generated for it. On NativeAOT the array path relies on the
pre-generated proxy map (TryGetArrayProxy), so the System.String element type
yielded nothing. (CoreCLR fabricates the types dynamically, so it was fine.)

Emit a System.String array proxy alongside the primitives in the
_Java.Interop.TypeMap assembly, using the reference-array family (Primitive is
null). The proxy map key "System.String, System.Runtime" matches
TrimmableTypeMap.TryGetManagedTypeKey (typeof (string)).

Fixes the System.String portion of Java.InteropTests.JniTypeManagerTests.GetType
on the trimmable/NativeAOT path. Boxed-nullable element types (bool?/int?/...
via java/lang/Boolean etc.) have the same shape and will be covered/verified by
follow-up test coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
JniTypeManagerTests.GetType covered primitive keyword arrays ([Z..[D) and the
System.String reference array ([Ljava/lang/String;), but not the boxed nullable
counterparts. Add assertions for [Ljava/lang/Boolean;, [Ljava/lang/Byte;,
[Ljava/lang/Character;, [Ljava/lang/Short;, [Ljava/lang/Integer;,
[Ljava/lang/Long;, [Ljava/lang/Float;, [Ljava/lang/Double;.

Each boxed reference maps to Nullable<T> (JniBuiltinSimpleReferenceToType /
GetBuiltInTypeForSimpleReference), and the non-keyword array path yields
JavaObjectArray<T?> and T?[] (no JavaArray<T>/JavaPrimitiveArray<T>). The
expected types match the default ReflectionJniTypeManager behavior.

These pass on the default typemap. On the trimmable typemap they exercise the
same array-proxy gap just fixed for System.String; if the boxed element types
lack pre-generated array proxies on NativeAOT they will fail, driving the
follow-up implementation fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GetTypes ("[Ljava/lang/Boolean;") and the other boxed value-type array
signatures dropped the Nullable<T> array family (bool?[], JavaObjectArray<bool?>)
on NativeAOT. Like System.String, Nullable<T> is a built-in reference mapping
injected at runtime by GetBuiltInTypeForSimpleReference, so it is neither a
scanned Java peer nor a primitive and had no pre-generated array proxy.

Unlike String, the proxy map key is the tricky part: at runtime
TryGetManagedTypeKey (typeof (bool?)) keyed on Type.FullName, whose type
argument carries the full versioned assembly-qualified name
(System.Nullable`1[[System.Boolean, System.Private.CoreLib, Version=...]]),
which the generator can't stably reproduce.

Normalize the generic key on both sides:
  * Runtime (BuildManagedTypeKey): build closed-generic keys from the open
    definition's FullName plus each type argument's normalized key (simple
    assembly name, no Version/Culture/PublicKeyToken), and treat Nullable<T> as
    a System.Runtime type. typeof (bool?) -> "System.Nullable`1[[System.Boolean,
    System.Runtime]], System.Runtime".
  * Generator (ModelBuilder): emit reference-array proxies for the eight
    Nullable<primitive> types that have a boxed java/lang mapping, keyed with
    the same normalized string.

Normalizing (rather than emitting version-qualified duplicates) keeps a single
canonical proxy per logical type.

Fixes the boxed-nullable portion of JniTypeManagerTests.GetType on trimmable
NativeAOT (added in the preceding test-coverage commit).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The trimmable typemap generates array-proxy types (JavaObjectArray<T>,
T[], nested closures) up to a configurable rank so that array types can
be resolved without Array.CreateInstance under NativeAOT. Emitting these
uniformly at rank 3 caused a NativeAOT ILC type explosion: on an AppCompat
app ILC compiled/preinitialized ~129,320 types and IlcCompile took 333s,
which pushed the heaviest CI apps past the 30-min DefaultBuildTimeOut.

Reference-type array proxies (scanned Java peers, System.String, boxed
Nullable<T>) are now capped at a separate, lower rank
(_AndroidTrimmableTypeMapMaxReferenceArrayRank, default 1 for NativeAOT)
while primitive arrays keep the higher rank (default 3). Jagged reference
arrays (Foo[][]) are rare, and each nesting level multiplies the
JavaObjectArray<T> closure ILC must process, so this trims the bulk of the
cost while preserving the common Foo[] and int[][][] cases.

The anchor matrix (model.MaxArrayRank) stays uniform at max(primitive,
reference) across all typemap assemblies so RootTypeMapAssemblyGenerator's
rectangular [assembly][rank] sentinel layout is unchanged; only the emitted
entries per rank differ.

Measured on the same AppCompat NativeAOT app:
  ILC types    129,320 -> 22,673   (5.7x fewer)
  IlcCompile   333s -> 91s         (3.65x faster)
  TypeMap DLLs 19.7MB -> 9.7MB     (~2x smaller)

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

The split-rank change capped all reference-type array proxies at rank 1,
which regressed multidimensional string arrays: GetTypes("[[Ljava/lang/String;")
no longer yielded System.String[][] / JavaObjectArray<JavaObjectArray<string>>.
That expectation is baseline Java.Interop behavior (JniTypeManagerTests.GetType,
since #517) that the managed/llvm-ir/reflection type maps all satisfy, so the
trimmable type map must satisfy it too now that it is the default.

System.String is a single, fixed built-in element type (injected at runtime by
GetBuiltInTypeForSimpleReference), not part of the scanned-peer population that
drives the NativeAOT ILC type explosion. Emit its array proxies up to the
primitive maxArrayRank instead of maxReferenceArrayRank, exactly like the
keyword primitives. The cost is negligible: on the AppCompat NativeAOT app ILC
types went from 22,673 -> 22,717 (+44) and IlcCompile stayed ~60-90s (vs rank-3's
129,320 types / 333s).

Scanned Java peers stay capped at maxReferenceArrayRank (1); boxed Nullable<T>
built-ins likewise stay at rank 1 (no baseline coverage requires multidim boxed
arrays).

Also drop TryGetArrayProxy_ObjectLeaf_ReturnsAllRankTypes: its additionalRank:2
assertions expected Object[][] etc., which is exactly the reference-rank-2 case
that split-rank intentionally no longer generates.

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

Follow-up to promoting System.String: the boxed Nullable<T> value types
(java/lang/Boolean, java/lang/Integer, ...) are built-in element mappings
just like System.String and the keyword primitives — a small, fixed set that
is not part of the scanned-peer population driving the NativeAOT ILC type
explosion. Emit their array proxies up to maxArrayRank instead of
maxReferenceArrayRank so all built-in element types are treated uniformly and
multidimensional boxed arrays (int?[][], "[[Ljava/lang/Integer;") resolve.

Only scanned Java peers remain capped at maxReferenceArrayRank (1).

Cost stays negligible on the AppCompat NativeAOT app: ILC types 22,717 ->
23,101 (+384) and IlcCompile ~56s (vs rank-3's 129,320 types / 333s).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Generated typemap proxy types can derive directly from the non-generic JavaPeerProxy base when they need to represent interfaces or open generic definitions. Make the base constructor protected so those generated proxies can call it from typemap assemblies.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 8, 2026 07:51

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 improves the trimmable typemap’s array-proxy support by adding missing built-in proxies (notably System.String and boxed Nullable<T>), normalizing runtime array-proxy keys for closed generics, and introducing separate rank caps for reference vs primitive array proxies to reduce NativeAOT build-time/size costs.

Changes:

  • Generate array proxy entries/associations for System.String and boxed Nullable<T> (including updated unit tests validating metadata emission and signature decoding).
  • Add a separate reference-array rank property (MaxReferenceArrayRank / _AndroidTrimmableTypeMapMaxReferenceArrayRank) and plumb it through the MSBuild task + generator.
  • Normalize runtime managed-type keys for closed generics (e.g., Nullable<bool>) so runtime lookups match generator-emitted keys; relax JavaPeerProxy ctor visibility for generated proxy assemblies.
Show a summary per file
File Description
Configuration.OperatingSystem.props Adds a host-specific props file (appears to be a local artifact).
tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs Removes a reference-array proxy runtime test (coverage gap).
tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs Expands generator tests for built-in String/Nullable proxies and split-rank behavior.
src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs Adds MaxReferenceArrayRank task input and passes it to the generator.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets Introduces default _AndroidTrimmableTypeMapMaxReferenceArrayRank and passes it to the task.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets Plumbs MaxReferenceArrayRank for the post-trim generator pass.
src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs Normalizes managed-type keys for closed generics to match generated typemap keys.
src/Mono.Android/Java.Interop/JavaPeerProxy.cs Makes the base constructor protected so generated typemap proxy assemblies can derive from it.
src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs Adds maxReferenceArrayRank parameter and uses max rank for the root anchor matrix.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs Passes split-rank settings into ModelBuilder.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs Emits built-in proxies for System.String and boxed Nullable<T>; caps scanned-peer arrays by reference rank.
external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.cs Adds signature-to-type coverage for boxed Nullable<T> reference arrays.

Copilot's findings

  • Files reviewed: 12/12 changed files
  • Comments generated: 6

Comment thread Configuration.OperatingSystem.props Outdated
Comment thread src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs
Comment thread src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs Outdated
simonrozsival and others added 2 commits July 8, 2026 09:58
Restore object-leaf array proxy coverage for rank 1 and split the rank-2 reference-array assertions into an ignored test until higher-rank reference array proxy generation is revisited.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the accidental host-specific Configuration.OperatingSystem.props file from the PR. Clarify that MaxReferenceArrayRank applies only to scanned Java peer arrays, while built-in primitives, System.String, and boxed Nullable<T> use MaxArrayRank. Drop the unused maxReferenceArrayRank parameter from EmitPrimitiveArrayEntries.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add _AndroidTrimmableTypeMapMaxReferenceArrayRank to the build property cache so changing the reference-array rank invalidates _GenerateTrimmableTypeMap just like the primitive/built-in rank does. Add a regression test for the property-cache invalidation and clarify the TypeMapAssemblyGenerator parameter docs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants