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

🤖 Review — ⚠️ Needs Changes (CI pending)

Solid, well-scoped change that splits the built-in element array rank (_AndroidTrimmableTypeMapMaxArrayRank) from the scanned-peer reference array rank (_AndroidTrimmableTypeMapMaxReferenceArrayRank), and adds explicit System.String and boxed Nullable<T> array proxies. I verified the key correctness path end-to-end:

  • Runtime/generator key symmetry ✅TrimmableTypeMap.BuildManagedTypeKey now emits the normalized, version-independent generic form (System.Nullable\1[[System.Boolean, System.Runtime]], System.Runtime) that exactly matches ModelBuilder.AssemblyQualify (TypeRefData). GetAssemblyNameForManagedTypeKeycorrectly normalizesNullable(T)(and its element) toSystem.Runtime`, matching the emitter.
  • Rank matrix ✅ — carrying Math.Max (maxArrayRank, maxReferenceArrayRank) into MaxArrayRank/the root generator keeps the __ArrayMapRank{N} anchor matrix rectangular while peers get only reference-rank entries. Covered by the new Build_ReferenceRankLowerThanPrimitiveRank_* tests.
  • Boxed mappings ✅sbyte? for java/lang/Byte (signed) is correct; the 8 boxed types are the right set.
  • Incremental build ✅ — new property is added to _PropertyCacheItems and both targets, with a regeneration test.
  • Test coverage ✅ — generator, full-pipeline round-trip, JniTypeManagerTests, and build tests all extended.

Findings

  • 2 × 💡 suggestions (code consolidation + proxy-name convention) posted inline. No bugs or correctness issues found.

Notes

  • CI shows no combined status yet (pending). Per review policy this can't be marked LGTM until the Azure DevOps pipeline is green — please confirm CI passes before merge.
  • The removed maxArrayRank > 0 && guard before EmitPrimitiveArrayEntries is safe: the inner 1..maxArrayRank loops make it a no-op at rank 0.

Nice work — the extensive inline comments explaining the boxed-nullable/String rationale are genuinely helpful for future readers.

Generated by Android PR Reviewer for #12017 · 116.5 AIC · ⌖ 18.9 AIC · ⊞ 6.8K
Comment /review to run again

// signature. Like the primitives and System.String, these are a small fixed set of built-in
// element types (not the scanned-peer explosion), so they go up to maxArrayRank and keep
// multidim boxed arrays resolvable.
foreach (var nullablePrimitive in NullableArrayProxies) {

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 organization — The three emission blocks in EmitPrimitiveArrayEntries (primitive, System.String, and Nullable<T>) repeat the same ~7 lines of boilerplate: build ArrayProxyData, model.ArrayProxyTypes.Add, build proxyReference, add the TypeMapAttributeData entry, and AddArrayProxyAssociations. Consider extracting a small local helper like EmitBuiltinArrayProxy (model, proxy) that takes the constructed ArrayProxyData and performs the add/entry/association steps. It would remove the duplication and ensure the three call sites stay in sync if the entry shape changes.

Rule: Code duplication / consolidation

foreach (var nullablePrimitive in NullableArrayProxies) {
for (int rank = 1; rank <= maxArrayRank; rank++) {
var proxy = new ArrayProxyData {
TypeName = $"Nullable_{nullablePrimitive.Name}_ArrayProxy{rank}",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 💡 Naming — The System.String block above uses the ManagedTypeNameToArrayProxyTypeName ("System.String", rank) helper to derive the proxy type name, while this Nullable<T> block hand-rolls $"Nullable_{nullablePrimitive.Name}_ArrayProxy{rank}" (and the primitive block hand-rolls $"Primitive_{...}_ArrayProxy{rank}"). Consider routing all three through the same helper (or documenting why the nullable names intentionally diverge) so the proxy-type-name convention has a single source of truth.

Rule: Consistency

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