[TrimmableTypeMap] Fix String and nullable array proxies#12017
[TrimmableTypeMap] Fix String and nullable array proxies#12017simonrozsival wants to merge 11 commits into
Conversation
…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>
There was a problem hiding this comment.
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.Stringand boxedNullable<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; relaxJavaPeerProxyctor 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
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>
|
/review |
|
✅ Android PR Reviewer completed successfully! |
There was a problem hiding this comment.
🤖 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.BuildManagedTypeKeynow emits the normalized, version-independent generic form (System.Nullable\1[[System.Boolean, System.Runtime]], System.Runtime) that exactly matchesModelBuilder.AssemblyQualify (TypeRefData).GetAssemblyNameForManagedTypeKeycorrectly normalizesNullable(T)(and its element) toSystem.Runtime`, matching the emitter. - Rank matrix ✅ — carrying
Math.Max (maxArrayRank, maxReferenceArrayRank)intoMaxArrayRank/the root generator keeps the__ArrayMapRank{N}anchor matrix rectangular while peers get only reference-rank entries. Covered by the newBuild_ReferenceRankLowerThanPrimitiveRank_*tests. - Boxed mappings ✅ —
sbyte?forjava/lang/Byte(signed) is correct; the 8 boxed types are the right set. - Incremental build ✅ — new property is added to
_PropertyCacheItemsand 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 beforeEmitPrimitiveArrayEntriesis safe: the inner1..maxArrayRankloops 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) { |
There was a problem hiding this comment.
🤖 💡 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}", |
There was a problem hiding this comment.
🤖 💡 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
Summary
Independent trimmable typemap array-proxy fixes extracted from #11822, so the NativeAOT default-flip PR can stay focused.
System.Stringand boxedNullable<T>value types, including Java.InteropGetTypecoverage for boxed nullable arrays._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.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 passedNotes
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.