[TrimmableTypeMap] Improve array handling#11238
Conversation
Improve CreatePeer under the trimmable typemap to match legacy JavaCast/JavaAs contracts: - Bad-cast disambiguation: distinguish incompatible Java types (return null → InvalidCastException) from missing typemap entries (ArgumentException) and generator gaps (NotSupportedException). - Closed-generic activation: when the proxy targets an open generic (e.g. JavaList<>), activate the closed targetType (e.g. JavaList<int>) via reflection using the (IntPtr, JniHandleOwnership) ctor. The [DynamicallyAccessedMembers(Constructors)] annotation on targetType guarantees the trimmer preserves the ctor metadata. - Type resolution: map IJavaPeerable/object/Exception to concrete peer types before proxy lookup, mirroring the legacy GetPeerType behavior. - TargetTypeMatches: restructure so open-generic proxies match only closed instantiations of their definition. - FindClass safety: catch ClassNotFoundException in TryGetProxyFromTargetType for types not present in the APK. - Don't synthesize activation from inherited ctors: match legacy GetConstructor() which doesn't find inherited constructors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ranks - Resolve activation ctor on invoker types for interface peers, so the generator picks the correct ctor signature (XA vs JI style). - Skip JNI keyword types (Z, B, C, S, I, J, F, D) in the scanner. These single-letter JNI names collided with primitive type handling in JniRuntime.JniTypeManager. - Skip all ArrayRank>0 types (JavaBooleanArray, JavaArray<>, etc.) — they were incorrectly added as aliases for java/lang/Object, causing alias resolution to select the open-generic JavaArray<> proxy. - Don't synthesize activation from inherited ctors in the generator, matching legacy Type.GetConstructor() behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add trimmable-typemap variant of GetThis.java that uses mono.android.Runtime.register instead of ManagedPeer.registerNativeMembers. Java.Interop-Tests.targets swaps variants based on $(_AndroidTypeMapImplementation). Re-enabled tests: - JavaCast_BadInterfaceCast (bad-cast disambiguation) - JavaCast_BaseToGenericWrapper (closed-generic activation) - JavaCast_CheckForManagedSubclasses (bad-cast disambiguation) - JavaCast_InvalidTypeCastThrows (bad-cast disambiguation) - JavaAs_Exceptions (inherited ctor fix) - DisposeAccessesThis (trimmable GetThis.java) - CreateGenericValue (ArrayRank scanner fix) Updated remaining exclusion comments with accurate root-cause descriptions. Removed resolved TODO comments from test files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
For every non-aliased, non-generic peer (excluding JNI primitive-keyword
keys), the generator can now emit three speculative `TypeMap<T>` entries
keyed by the **element JNI name** and anchored to per-typemap-assembly
`__ArrayMapRank{1,2,3}` sentinel TypeDefs:
[assembly: TypeMap<__ArrayMapRank1>("java/lang/String", typeof(string[]), typeof(string[]))]
[assembly: TypeMap<__ArrayMapRank2>("java/lang/String", typeof(string[][]), typeof(string[][]))]
[assembly: TypeMap<__ArrayMapRank3>("java/lang/String", typeof(string[][][]), typeof(string[][][]))]
The trim target is the closed array type itself, so ILC's per-shape
conditional drops entries when the array shape is never constructed —
validated on disjoint-set NativeAOT tests for both reference and value
type element peers.
* `Generator/Model/TypeMapAssemblyData.cs`:
* `TypeMapAssemblyData.RankSentinels` (nullable `RankSentinelNames`)
— when set, the emitter generates the three sentinel TypeDefs.
* `TypeMapAttributeData.AnchorRank` (nullable int) — when set,
overrides the model-level default anchor with the rank-{value}
sentinel from the same assembly.
* `Generator/TypeMapAssemblyEmitter.cs`:
* `EmitRankSentinels` mirrors the existing `__TypeMapAnchor` pattern.
* Per-anchor `TypeMap<TGroup>` 3-arg ctor refs are now built and
cached lazily by anchor handle (`GetOrAddTypeMapAttr3ArgCtorRef`).
* `EmitTypeMapAttribute` resolves rank-anchored entries to the local
sentinel handle.
* `Generator/ModelBuilder.cs`:
* `Build` takes a new `bool emitArrayEntries` (default false).
When true, sets `RankSentinels` and routes each peer through
`EmitArrayEntries`, which produces the per-rank trio.
* Skip rules: open-generic peers, JNI primitive-keyword keys, alias
groups (would produce duplicate keys; deferred).
* `tests/.../TypeMapModelBuilderTests.cs`: 14 new tests covering the
default-off behavior, default sentinel names, per-rank-trio emission,
element-only key, closed-array trim target, conditional-only entries,
open-generic / alias / primitive-keyword skip rules, multiple-peer
isolation, and PE blob round-trip (sentinels emitted, attribute
blobs survive).
Tracking: #11234 Phase 2 (arrays-only, container types stay untouched).
Runtime wiring + MSBuild flag follow in subsequent commits.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`JNIEnv.ArrayCreateInstance` now branches on `RuntimeFeature.IsDynamicCodeSupported`: * CoreCLR / Mono (true) — `Array.CreateInstance(elementType, length)`. No typemap roundtrip; supports unlimited array rank. * NativeAOT (false) — typemap lookup → AOT-safe `Array.CreateInstanceFromArrayType`. Capped at the emitted ranks (1–3); miss throws `NotSupportedException` with diagnostic. The runtime fork lets us avoid emitting (and paying for) speculative array TypeMap entries on CoreCLR-only builds, where the runtime type loader can construct any `T[]` dynamically anyway. * `ITypeMapWithAliasing.TryGetArrayType(string jniElementTypeName, int rank, out Type? arrayType)` — new abstraction for the per-rank array dictionary lookup. `SingleUniverseTypeMap` carries three nullable `IReadOnlyDictionary<string, Type>?` fields (rank 1, 2, 3) populated at `TrimmableTypeMap.Initialize` time; `AggregateTypeMap` does first-wins iteration. * `TrimmableTypeMap.TryGetArrayType(Type elementType, out Type?)` — walks down `elementType.IsArray` / `GetElementType()` to find the leaf type and array depth, resolves the leaf JNI element name (primitive static dict OR `TryGetJniNameForManagedType` wrapped), and delegates the (jni, rank+1) lookup to the interface. * `TrimmableTypeMap.Initialize` gains 5-arg overloads (single + aggregate) accepting the per-rank dicts. Existing 2-arg overloads stay as wrappers passing null per-rank dicts so older generated assemblies keep working. * `RootTypeMapAssemblyGenerator`: the generated `TypeMapLoader.Initialize` IL now branches on the new `emitArrayEntries` flag. When true, it collects per-assembly `__ArrayMapRank{1,2,3}` sentinels via `TypeMapping.GetOrCreateExternalTypeMapping<__ArrayMapRank{N}>()` and passes the resulting dicts to the 5-arg `TrimmableTypeMap.Initialize`. Aggregate (Debug) path is fully implemented; merged-universe (Release) path throws at generation time with a clear message — wiring the shared-universe array sentinels is a small follow-up. * `GenerateTrimmableTypeMap` MSBuild task: new `EmitArrayEntries` property forwarded through `TrimmableTypeMapGenerator.Execute` and `RootTypeMapAssemblyGenerator.Generate`. SDK target sets it to `$(PublishAot)`. * `JavaPeerContainerFactory<T>.CreateArray` and `CreateHigherRankArray` deleted. Container methods (`CreateList`, `CreateCollection`, `CreateDictionary*`) stay untouched — those are tracked separately in #11234. Validation: * 445 / 445 generator unit tests pass. * Trimmable + CoreCLR `RunTestApp` lane on emulator: **917 total / 0 errors / 3 failures** (pre-existing `TryGetJniNameForManagedType_*`, called out as out-of-scope in #11225). No regression. * The NativeAOT branch path is gated on dotnet/runtime#126380 (ships in .NET 11 nightly preview.5+); validated with the playground repro separately. Tracking: #11234 Phase 2 (arrays only). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
5b32352 to
956a999
Compare
* JNIEnv.ArrayCreateInstance:
* Drop the IL3050 #pragma — IsDynamicCodeSupported acts as a
[FeatureGuard] so the trimmer dead-codes the dynamic branch under
PublishAot without needing a manual suppression.
* Drop the 'Mono' mention from the comment — the trimmable typemap
is a CoreCLR-only feature.
* SingleUniverseTypeMap._arrayMapsByRank:
* Switched from a 1-indexed array of fixed length 4 (with [0] unused)
to a 0-indexed variable-length array (length determined by what the
generator emitted). TryGetArrayType uses (rank - 1) for indexing
and bounds-checks to the actual length.
* Variable rank count throughout:
* RankSentinelNames: replaced the fixed (Rank1, Rank2, Rank3) record
with a name list + Count, generated via CreateDefault(maxRank).
* TypeMapAssemblyEmitter: _rankAnchorHandles becomes a 0-indexed
variable-length array sized to model.RankSentinels.Count.
* ModelBuilder.Build: replaced 'bool emitArrayEntries' with
'int maxArrayRank' (0 = disabled). EmitArrayEntries takes the max
and loops 1..maxArrayRank instead of using a const.
* RootTypeMapAssemblyGenerator: 'int maxArrayRank' parameter; the
generated TypeMapLoader.Initialize now builds a single jagged
'IReadOnlyDictionary<string, Type>?[][]' (per-universe per-rank)
instead of fixed-rank-count locals. Rank loop is unrolled.
* GenerateTrimmableTypeMap MSBuild task: 'int MaxArrayRank' property.
* New $(_AndroidTrimmableTypeMapMaxArrayRank) MSBuild property —
defaults to 3 under PublishAot=true, 0 otherwise. Users can override
to support unlimited rank under NativeAOT (limited to what they're
willing to pay in attribute metadata).
* TrimmableTypeMap.Initialize:
* Single overload: '(typeMap, proxyMap, IReadOnlyDictionary<string,
Type>?[]? arrayMapsByRank)' — 0-indexed by (rank - 1).
* Aggregate overload: '(typeMaps[], proxyMaps[],
IReadOnlyDictionary<string, Type>?[]?[]? perUniverseArrayMaps)' —
jagged, indexed first by universe, then by (rank - 1).
* Existing 2-arg overloads retained as wrappers passing null.
* Tests: updated BuildModelWithArrays to take maxArrayRank (default 3).
Replaced 'RankSentinels.Rank{1,2,3}' assertions with Names[i] indexing.
Added Build_EmitArrayEntries_HonoursMaxArrayRank test that verifies
emission with maxArrayRank=1 and =5.
Validation:
* 446 / 446 generator unit tests pass (445 baseline + 1 new for the
MaxArrayRank parametrization).
* Trimmable + CoreCLR RunTestApp lane on emulator: 917 / 0 errors / 3
pre-existing failures. No regression.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tightens XML doc comments and inline notes added in this PR — keeps the intent but drops walls of explanation that were repeating what the code already says. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces the NotSupportedException stub in RootTypeMapAssemblyGenerator
with a fully functional shared-universe + array entries IL emit path,
covering the Release build configuration when array typemap entries
are enabled (`_AndroidTrimmableTypeMapMaxArrayRank > 0`).
Each per-assembly typemap DLL still owns its own `__ArrayMapRank{N}`
sentinel TypeDefs, so each rank's entries are split across N per-asm
dicts. The new `CompositeStringTypeReadOnlyDictionary` wraps an array
of source dicts and routes `TryGetValue` first-hit, letting the existing
single-universe-with-arrays `TrimmableTypeMap.Initialize` overload
consume them as a single per-rank dict each.
Generated IL (shared + arrays):
var arrayMapsByRank = new IReadOnlyDictionary<string, Type>?[maxRank];
for each rank r:
var sources = new IReadOnlyDictionary<string, Type>?[asmCount];
sources[i] = TypeMapping.GetOrCreateExternalTypeMapping<PerAsm[i].__ArrayMapRank_r>();
arrayMapsByRank[r-1] = new CompositeStringTypeReadOnlyDictionary(sources);
TrimmableTypeMap.Initialize(
TypeMapping.GetOrCreateExternalTypeMapping<Java.Lang.Object>(),
TypeMapping.GetOrCreateProxyTypeMapping<Java.Lang.Object>(),
arrayMapsByRank);
Also extends `IgnoresAccessChecksTo` to cover per-asm DLLs in shared
mode when arrays are enabled (root needs to reach their internal sealed
`__ArrayMapRank{N}` types).
Adds 4 generator unit tests; all 450 tests pass. Validated default
Release CoreCLR trimmable lane: 917/0/3 (matches baseline).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Removes obvious redundant inline comments and trims a couple of doc summaries; behavior unchanged. 450/450 unit tests still pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds an IsSZArray guard inside the leaf walk so multi-dimensional arrays (e.g. byte[,], byte[,][]) bail out cleanly rather than walking through GetElementType() as if they were jagged szarray chains. JNI only supports single-dim zero-based arrays so any multi-dim element type reaching this method indicates a caller bug — return false rather than silently producing a misleading rank-N lookup. In practice this is unreachable from MCW-generated marshaling (JNI arrays are always szarrays) so behavior is unchanged for real callers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces TypeMapAssemblyData.RankSentinels (a RankSentinelNames wrapper
class storing a list of derived-from-rank strings) with a plain int
MaxArrayRank field. The sentinel names are always `__ArrayMapRank{N}`
by convention — both the per-asm emitter and the runtime/root loader
hard-code that pattern — so the wrapper added no value beyond rank
storage.
Removes ~35 LOC and a public-ish helper class. 450/450 unit tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Folds the multi-source first-hit logic into SingleUniverseTypeMap by making its rank dict storage jagged (`[rank-1][source]`). The composite class was just a thin wrapper around a list of dicts that did first-hit TryGetValue — exactly what the type map's own array lookup needs to do. Native jagged storage avoids the wrapper allocation, removes one type, and eliminates a member-ref + newobj from the generated IL per rank. Initialize signature changes: the single-universe-with-arrays overload now takes `IReadOnlyDictionary<string,Type>?[]?[]?` (jagged) instead of `IReadOnlyDictionary<string,Type>?[]?`. The aggregate path wraps each universe's 1-source rank dicts into 1-element-inner-jagged at construction time so SingleUniverseTypeMap's storage shape is uniform. Mono.Android Release builds clean. 449/449 unit tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Defines `Microsoft.Android.Runtime.__ArrayMapRank1` ... `__ArrayMapRank8` as internal sealed types in Mono.Android. Per-asm typemap DLLs no longer emit their own rank sentinel TypeDefs — they reference the shared anchors instead. Per rank, every per-asm DLL contributes to the SAME TypeMap group, so `TypeMapping.GetOrCreateExternalTypeMapping<__ArrayMapRankN>()` returns ONE merged dict spanning all per-asm DLLs. Knock-on simplifications: - `SingleUniverseTypeMap` and `AggregateTypeMap` no longer hold rank dicts; the rank lookup table lives directly on `TrimmableTypeMap` (singleton) since it's logically global, not per-universe. - `ITypeMapWithAliasing.TryGetArrayType` removed — no longer needed. - `Initialize(typeMaps[], proxyMaps[], perUniverseArrayMaps[][])` collapses to `Initialize(typeMaps[], proxyMaps[], arrayMapsByRank[])` (1D, same shape as the single-universe overload). Both modes pass the SAME array-shape — the only thing that varies is whether the main typemap is single (shared) or N-element (aggregate). - Root assembly IL: shared and aggregate paths share an `EmitBuildArrayMapsByRank` helper that emits `maxArrayRank` calls to `GetOrCreateExternalTypeMapping<Mono.Android.__ArrayMapRankN>()` directly. Shared mode no longer needs IgnoresAccessChecksTo on per-asm DLLs. - The supported maximum is `MaxSupportedArrayRank = 8`. The generator throws `ArgumentOutOfRangeException` if `_AndroidTrimmableTypeMapMaxArrayRank` exceeds it (with a message pointing at the Mono.Android types to add for more). Net -82 LOC. Mono.Android Release builds clean. 449/449 unit tests pass. Device test (Release CoreCLR trimmable): 917 / 0 errors / 3 pre-existing failures — matches baseline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drops the unreachable elementType null check (param is non-nullable under #nullable enable), collapses the redundant rank/index arithmetic (rankIndex = elementDepth = rank - 1), and folds the JNI name resolution into a single ternary. Behavior unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Collapses 4 IL emit paths × 4 MemberRef helpers in the root assembly generator down to 2 of each. Both paths always emit the 3-arg `Initialize` (with arrayMapsByRank); when maxArrayRank=0 the new `EmitArrayMapsByRankOrNull` helper just emits `ldnull`. Also drops the matching no-array `Initialize` C# overloads from `TrimmableTypeMap` since nothing calls them — the generated root assembly always picks the 3-arg shape now. The internal callers can pass `arrayMapsByRank: null`. Net -157 LOC. 449/449 unit tests pass; Mono.Android Release builds clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1. EmitTypeMapLoader: only build the Initialize MemberRef for the path actually taken (was building both shared and aggregate variants unconditionally — added a stray MemberRef row to every assembly). 2. ModelBuilder.Brackets: replace LINQ Enumerable.Repeat fallback for rank>=4 with a straightforward StringBuilder loop. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drops a redundant `if (maxArrayRank > 0)` guard (the default is 0 already and the parameter is validated non-negative above) and inlines `moduleName`. Behavior unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The == default check on _rankAnchorHandles[i] was redundant — since EmitRankSentinels populates all slots up to MaxArrayRank, the length check is sufficient. The error message also referenced 'TypeDef' when we now emit TypeRefs (shared anchors live in Mono.Android). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/review |
|
✅ Android PR Reviewer completed successfully! |
There was a problem hiding this comment.
✅ LGTM — Clean design, minor doc/style nits
Well-architected PR. The runtime fork (CoreCLR → Array.CreateInstance directly, NativeAOT → per-rank typemap + Array.CreateInstanceFromArrayType) is sound and avoids unnecessary typemap lookups on CoreCLR while remaining AOT-safe on NativeAOT.
What looks good:
- IL emission for both aggregate (Debug) and single-universe (Release) paths is correct — stack discipline, local variable encoding, and member reference blobs all check out.
- Thorough edge-case handling in
EmitArrayEntries: primitives skipped (dedicated JNI handlers exist), open generics rejected (typeof(T<>[])invalid), alias groups deferred. TryGetArrayType's unwrap loop correctly handles nested szarrays and rejects multi-dim arrays.- 14+ new generator unit tests cover the key scenarios including PE round-trip verification.
- Sentinel types in
ArrayMapAnchors.csmirror the established__TypeMapAnchorpattern nicely. MaxSupportedArrayRank = 8matches the 8 anchor types, with the MSBuild default of 3 for NativeAOT being practical for typical apps.
Minor nits (3 inline 💡):
| Sev | Category | File |
|---|---|---|
| 💡 | Formatting | Array.Empty<T>() → [] (2 instances) |
| 💡 | Documentation | EmitArrayEntries doc comment mentions [L<jni>;-shaped keys but keys are bare element names |
| 💡 | Documentation | Stale 2-arg Initialize references in comments |
No correctness, safety, or performance issues found.
Generated by Android PR Reviewer for issue #11238 · ● 9.8M
|
|
||
| // Per-rank array sentinel TypeDefs, 0-indexed by (rank - 1). Empty when array entries | ||
| // aren't emitted. | ||
| EntityHandle [] _rankAnchorHandles = Array.Empty<EntityHandle> (); |
There was a problem hiding this comment.
🤖 💡 Formatting — Repo convention prefers [] over Array.Empty<T>().
| EntityHandle [] _rankAnchorHandles = Array.Empty<EntityHandle> (); | |
| EntityHandle [] _rankAnchorHandles = []; |
Same applies to TrimmableTypeMap.cs line 41:
_arrayMapsByRank = arrayMapsByRank ?? [];Rule: Use [] not Array.Empty<T>()
| /// <param name="maxArrayRank"> | ||
| /// Maximum array rank for which per-assembly typemaps emitted <c>__ArrayMapRank{N}</c> | ||
| /// sentinels. Must match the value passed to the per-assembly generators. 0 means | ||
| /// no array sentinels were emitted; the loader uses the 2-arg <c>Initialize</c>. |
There was a problem hiding this comment.
🤖 💡 Documentation — The xml-doc says "the loader uses the 2-arg Initialize", but the old 2-arg overloads were replaced by the new 3-arg overloads that always take arrayMapsByRank (passing null when 0). Consider updating:
/// no array sentinels were emitted; the loader passes <c>null</c> for <c>arrayMapsByRank</c>.
Also, the class-level pseudo-C# example at lines 36–49 still shows the old 2-arg TrimmableTypeMap.Initialize(typeMaps, proxyMaps) call — updating that would keep it consistent with the new 3-arg signature.
Rule: Stale comments (Postmortem #59)
| /// Emits per-rank <c>[L<jni>;</c>-shaped TypeMap entries for one peer, anchored to | ||
| /// the per-assembly <c>__ArrayMapRank{N}</c> sentinels. Skips open generics, primitive | ||
| /// JNI keyword keys (handled by the legacy primitive-array path), and alias groups. |
There was a problem hiding this comment.
🤖 💡 Documentation — The doc comment says "[L<jni>;-shaped TypeMap entries", but the design choice here is specifically to use the bare element JNI name as the key (no [L...; wrapping). The test Build_EmitArrayEntries_KeyIsElementJniName documents this explicitly. Consider rewording to avoid confusion:
| /// Emits per-rank <c>[L<jni>;</c>-shaped TypeMap entries for one peer, anchored to | |
| /// the per-assembly <c>__ArrayMapRank{N}</c> sentinels. Skips open generics, primitive | |
| /// JNI keyword keys (handled by the legacy primitive-array path), and alias groups. | |
| /// Emits per-rank array TypeMap entries for one peer, anchored to | |
| /// the per-assembly <c>__ArrayMapRank{N}</c> sentinels. Keys are bare element JNI names | |
| /// (rank is encoded by the sentinel anchor, not by JNI array prefixes). Skips open generics, | |
| /// primitive JNI keyword keys (handled by the legacy primitive-array path), and alias groups. |
Rule: Comments explain "why", not "what" — and shouldn't contradict the implementation
There was a problem hiding this comment.
Pull request overview
Refactors trimmable typemap array creation to avoid per-T factory IL bloat by routing array construction through JNIEnv.ArrayCreateInstance, using Array.CreateInstance when dynamic code is supported and a per-rank array typemap under NativeAOT.
Changes:
- Add generator support for per-rank array
TypeMapentries (with rank anchors) and plumbMaxArrayRankthrough the MSBuild task + targets. - Update the runtime typemap initialization and lookup to support array-type resolution under NativeAOT, and update
JNIEnv.ArrayCreateInstanceto use the new runtime fork. - Remove
JavaPeerContainerFactory<T>array creation APIs and update tests to cover the new generator/root-assembly behavior.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs | Adds unit tests validating array-entry model emission (ranks, keys, skip rules, blob round-trip). |
| tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs | Adds tests for merged-universe root assembly generation when arrays are enabled. |
| src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs | Adds MaxArrayRank task input and forwards it into typemap generation. |
| src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets | Introduces $(_AndroidTrimmableTypeMapMaxArrayRank) defaults and passes it to the task. |
| src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs | Extends initialization to accept per-rank array dictionaries and adds TryGetArrayType lookup used by NativeAOT. |
| src/Mono.Android/Microsoft.Android.Runtime/ArrayMapAnchors.cs | Adds shared __ArrayMapRank{N} anchor types in Mono.Android for rank grouping. |
| src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs | Removes array creation APIs from the container factory surface (arrays move to JNIEnv). |
| src/Mono.Android/Android.Runtime/JNIEnv.cs | Implements the runtime fork for array creation (dynamic-code vs NativeAOT typemap). |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs | Adds MaxSupportedArrayRank validation and threads maxArrayRank through generation. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs | Passes maxArrayRank into ModelBuilder.Build. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs | Emits rank anchor refs and supports rank-anchored 3-arg TypeMap<TGroup> constructor emission. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs | Emits root-loader initialization that passes array rank dictionaries (or null) into TrimmableTypeMap.Initialize. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs | Emits per-rank array entries into the typemap model when enabled. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs | Adds MaxArrayRank and per-entry AnchorRank metadata for array mapping. |
| public static void Initialize ( | ||
| IReadOnlyDictionary<string, Type> typeMap, | ||
| IReadOnlyDictionary<Type, Type> proxyMap, | ||
| IReadOnlyDictionary<string, Type>?[]? arrayMapsByRank) | ||
| { | ||
| ArgumentNullException.ThrowIfNull (typeMap); | ||
| ArgumentNullException.ThrowIfNull (proxyMap); | ||
| InitializeCore (new SingleUniverseTypeMap (typeMap, proxyMap)); | ||
| InitializeCore (new SingleUniverseTypeMap (typeMap, proxyMap), arrayMapsByRank); | ||
| } |
There was a problem hiding this comment.
TrimmableTypeMap.Initialize previously had 2-arg overloads; this change replaces them with 3-arg overloads only. That’s a breaking API change and can also break older generated _Microsoft.Android.TypeMaps assemblies at runtime (MissingMethodException) if they still call the 2-arg overloads. Consider re-introducing the 2-arg overloads as wrappers that forward null for arrayMapsByRank to preserve backward compatibility.
| if ((uint)rankIndex >= (uint)_arrayMapsByRank.Length || _arrayMapsByRank [rankIndex] is not { } dict) { | ||
| return false; | ||
| } | ||
|
|
||
| string? leafJniName = leaf.IsPrimitive | ||
| ? TryGetPrimitiveJniName (leaf, out var p) ? p : null | ||
| : TryGetJniNameForManagedType (leaf, out var jni) ? jni : null; | ||
|
|
||
| return leafJniName is not null && dict.TryGetValue (leafJniName, out arrayType); |
There was a problem hiding this comment.
TryGetArrayType currently relies on _arrayMapsByRank lookups using primitive JNI keywords (Z/B/C/...). However the generator intentionally skips emitting array entries for primitive JNI keyword keys, so NativeAOT + trimmable typemap will fail to create jagged primitive arrays (e.g. byte[][] where elementType is typeof(byte[])), leading to JNIEnv.ArrayCreateInstance throwing NotSupportedException. Please add an AOT-safe fallback for primitive-leaf arrays (e.g. a precomputed (primitive, rank)->Type table) or ensure primitive-key array entries are present in the rank maps.
| if ((uint)rankIndex >= (uint)_arrayMapsByRank.Length || _arrayMapsByRank [rankIndex] is not { } dict) { | |
| return false; | |
| } | |
| string? leafJniName = leaf.IsPrimitive | |
| ? TryGetPrimitiveJniName (leaf, out var p) ? p : null | |
| : TryGetJniNameForManagedType (leaf, out var jni) ? jni : null; | |
| return leafJniName is not null && dict.TryGetValue (leafJniName, out arrayType); | |
| bool isPrimitiveLeaf = leaf.IsPrimitive; | |
| if ((uint)rankIndex < (uint)_arrayMapsByRank.Length && _arrayMapsByRank [rankIndex] is { } dict) { | |
| string? leafJniName = isPrimitiveLeaf | |
| ? TryGetPrimitiveJniName (leaf, out var p) ? p : null | |
| : TryGetJniNameForManagedType (leaf, out var jni) ? jni : null; | |
| if (leafJniName is not null && dict.TryGetValue (leafJniName, out arrayType)) { | |
| return true; | |
| } | |
| } | |
| // Primitive leaf arrays intentionally do not have primitive-keyed rank-map entries, | |
| // so construct the next szarray type directly as an AOT-safe fallback. | |
| if (isPrimitiveLeaf) { | |
| arrayType = elementType.MakeArrayType (); | |
| return true; | |
| } | |
| return false; |
| // dictionary at runtime via TypeMapping.GetOrCreateExternalTypeMapping<__ArrayMapRankN>(). | ||
| // | ||
| // To support a higher MaxArrayRank, add additional types here and bump | ||
| // TrimmableTypeMap.MaxSupportedArrayRank. |
There was a problem hiding this comment.
The comment references TrimmableTypeMap.MaxSupportedArrayRank, but the max-rank constant introduced in this PR lives in TrimmableTypeMapGenerator.MaxSupportedArrayRank. Please update the comment to point at the correct symbol/location so future rank bumps don’t miss updating the generator limit.
| // TrimmableTypeMap.MaxSupportedArrayRank. | |
| // TrimmableTypeMapGenerator.MaxSupportedArrayRank. |
| <!-- Max array rank for __ArrayMapRank{N} sentinel emission. Defaults to 3 under AOT (CoreCLR | ||
| reaches Array.CreateInstance directly and skips the typemap roundtrip). --> |
There was a problem hiding this comment.
The comment says "Defaults to 3 under AOT (CoreCLR reaches Array.CreateInstance directly and skips the typemap roundtrip)", but $(PublishAot)==true is exactly the scenario where RuntimeFeature.IsDynamicCodeSupported is false and JNIEnv.ArrayCreateInstance must use the typemap/Array.CreateInstanceFromArrayType path. Please reword this comment to reflect the actual behavior (e.g., default 3 for NativeAOT; 0 for non-AOT where dynamic code can use Array.CreateInstance).
| <!-- Max array rank for __ArrayMapRank{N} sentinel emission. Defaults to 3 under AOT (CoreCLR | |
| reaches Array.CreateInstance directly and skips the typemap roundtrip). --> | |
| <!-- Max array rank for __ArrayMapRank{N} sentinel emission. Defaults to 3 for NativeAOT | |
| (PublishAot=true, dynamic code is unavailable, so array creation uses the typemap path); | |
| defaults to 0 otherwise, where dynamic code can use Array.CreateInstance directly. --> |
| /// <param name="maxArrayRank"> | ||
| /// Maximum array rank for which per-assembly typemaps emitted <c>__ArrayMapRank{N}</c> | ||
| /// sentinels. Must match the value passed to the per-assembly generators. 0 means | ||
| /// no array sentinels were emitted; the loader uses the 2-arg <c>Initialize</c>. |
There was a problem hiding this comment.
The maxArrayRank doc says the loader uses the 2-arg Initialize overload when maxArrayRank is 0, but the generator now emits calls to the 3-arg Initialize(..., arrayMapsByRank) and passes null when maxArrayRank == 0. Please update this doc comment to match the emitted IL.
| /// no array sentinels were emitted; the loader uses the 2-arg <c>Initialize</c>. | |
| /// no array sentinels were emitted; the loader still calls the 3-arg <c>Initialize</c> | |
| /// overload and passes <see langword="null"/> for the array-maps argument. |
| [Fact] | ||
| public void Generate_MergedMode_WithArrays_ProducesValidPEAssembly () | ||
| { | ||
| using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], | ||
| useSharedTypemapUniverse: true, maxArrayRank: 3); | ||
| using var pe = new PEReader (stream); | ||
| Assert.True (pe.HasMetadata); | ||
| } |
There was a problem hiding this comment.
PR description states that merged-universe (Release) array emission currently throws at generation time and needs a follow-up, but these new merged-mode array tests indicate the merged-universe path is implemented. Please update the PR description (or the code/tests) so they agree about whether merged-mode array emission is supported in this PR.
|
|
||
| throw new NotSupportedException ( | ||
| $"No TrimmableTypeMap array entry for element type '{elementType}'. " + | ||
| $"Add an [assembly: TypeMap] entry for the closed array type or report an issue."); |
There was a problem hiding this comment.
The NotSupportedException guidance is misleading: TryGetArrayType looks up entries in the per-rank __ArrayMapRank{N} TypeMap groups keyed by the element JNI name, so adding a generic [assembly: TypeMap] entry for the closed array type may not fix the issue unless it’s anchored to the correct rank group. Consider updating the message to mention the required __ArrayMapRank{rank} group / MaxArrayRank setting (or point users at the MSBuild property that controls emission) so the suggested remediation is accurate.
| $"Add an [assembly: TypeMap] entry for the closed array type or report an issue."); | |
| $"Array lookups use the element type within the per-rank '__ArrayMapRank1' typemap group; ensure the mapping is emitted for rank 1 (for example by increasing MaxArrayRank) or report an issue."); |
Summary
Replaces the per-
TJavaPeerContainerFactory<T>.CreateArrayfactory with a runtime fork that uses the .NET runtime type loader on CoreCLR/Mono and a per-rank trimmable typemap on NativeAOT. Eliminates the per-peer array-creation IL bloat thatJavaPeerContainerFactory<T>was contributing — without the trim regressions we evaluated through several alternative designs (see Considered alternatives at the end).Tracking: #11234 Phase 2 (arrays-only — container types
JavaList<T>/JavaCollection<T>/JavaDictionary<K, V>stay untouched and have their own follow-up PR).Runtime fork
JNIEnv.ArrayCreateInstancebranches onRuntimeFeature.IsDynamicCodeSupported:Why fork? CoreCLR's runtime type loader can construct any
T[]from its element-type metadata at runtime, so going through the typemap is unnecessary work and trim warnings. NativeAOT cannot (value-type arrays each need their own codegen, and our marshaling call sites are opaque to the trimmer), so the typemap is the only AOT-safe route.Generator changes
For every non-aliased, non-generic peer (excluding JNI primitive-keyword keys), the generator emits three speculative
TypeMapentries keyed by the element JNI name and anchored to per-typemap-assembly rank sentinel TypeDefs:~/Projects/playground/TestTypeMapArrayswith disjoint type sets — works for both ref- and value-type elements)."[" + "L" + jni + ";"runtime concat.__ArrayMapRank{N}sentinels mirror the existing__TypeMapAnchorpattern (per-assembly, like generic anchor types).$(PublishAot) == true. Saves attribute metadata on CoreCLR-only builds where the runtime fork bypasses the typemap entirely.typeof(T<>[])invalid), JNI primitive-keyword keys (handled by the legacy primitive-array path), alias groups (would produce duplicate keys; deferred until alias-aware design).Runtime API
ITypeMapWithAliasinggains:SingleUniverseTypeMapcarries three nullableIReadOnlyDictionary<string, Type>?fields (rank 1, 2, 3);AggregateTypeMapdoes first-wins iteration.TrimmableTypeMap.Initializegains 5-arg overloads (single + aggregate) accepting the per-rank dicts; the existing 2-arg overloads stay as wrappers passingnullper-rank dicts.TrimmableTypeMap.TryGetArrayType(Type elementType, out Type? arrayType)walks downelementType.IsArray/GetElementType()to find the leaf type and the element array depth, resolves the leaf JNI element name (primitive static dict ORTryGetJniNameForManagedTypewrapped), and delegates the(elementJni, rank=elementDepth+1)lookup to_typeMap.TryGetArrayType.Generator IL emission
RootTypeMapAssemblyGenerator.EmitTypeMapLoaderbranches onemitArrayEntries. When true, the generatedTypeMapLoader.InitializeIL collects per-rank dicts from each per-assembly typemap (5IReadOnlyDictionary<...>[]arrays total — typeMaps, proxyMaps, arrayMapsRank1/2/3) and passes them to the 5-argTrimmableTypeMap.Initialize. The aggregate (Debug) path is fully implemented; the merged-universe (Release) path throws at generation time with a clear message — wiring the shared-universe array sentinels is a small follow-up.What's deleted
JavaPeerContainerFactory<T>.CreateArrayandCreateHigherRankArray.JavaPeerContainerFactory.Other factory methods (
CreateList,CreateCollection,CreateDictionary*) stay — those will be addressed in the follow-up Phase 2-collections PR.Files changed
Generator/Model/TypeMapAssemblyData.csTypeMapAttributeData.AnchorRank(1-based per-rank anchor) andTypeMapAssemblyData.RankSentinels(sentinel TypeDef names).Generator/TypeMapAssemblyEmitter.csEmitRankSentinelsmirrorsEmitAnchorType; per-anchorTypeMap<TGroup>3-arg ctor caching viaGetOrAddTypeMapAttr3ArgCtorRef.Generator/ModelBuilder.csbool emitArrayEntriesparameter onBuild; per-rank trio emitted via newEmitArrayEntries(skip rules: generics, primitive keywords, aliases).Generator/RootTypeMapAssemblyGenerator.csbool emitArrayEntriesparameter; aggregate path emits per-rank dict arrays inTypeMapLoader.Initialize; new 5-argInitializemember refs.TrimmableTypeMapGenerator.csExecuteandGenerateTypeMapAssembliespropagateemitArrayEntries.Generator/TypeMapAssemblyGenerator.csGeneratepropagatesemitArrayEntries.Tasks/GenerateTrimmableTypeMap.csEmitArrayEntriesMSBuild task property.targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targetsEmitArrayEntries="$(PublishAot)"on the task invocation.Microsoft.Android.Runtime/ITypeMapWithAliasing.csTryGetArrayTypeinterface method.Microsoft.Android.Runtime/SingleUniverseTypeMap.csTryGetArrayTypeimpl.Microsoft.Android.Runtime/AggregateTypeMap.csTryGetArrayTypeimpl.Microsoft.Android.Runtime/TrimmableTypeMap.csInitializeoverloads +TryGetArrayType(Type, out Type?)+TryGetPrimitiveJniNamehelper.Android.Runtime/JNIEnv.csArrayCreateInstanceruntime fork.Java.Interop/JavaPeerContainerFactory.csCreateArray/CreateHigherRankArraydeleted; class doc updated.tests/.../TypeMapModelBuilderTests.csValidation
RunTestApplane on emulator (-p:_AndroidTypeMapImplementation=trimmable -p:UseMonoRuntime=false): 917 total, 0 errors, 3 failures (the 3 are pre-existingTryGetJniNameForManagedType_*from [TrimmableTypeMap] JavaCast/JavaAs + container support #11225, called out as out-of-scope there). No regression.~/Projects/playground/TestTypeMapArraysagainst .NET 11 nightly11.0.100-preview.5.26228.123(which bundles [ILLink/ILCompiler] Fix crash when array types are used as TypeMap trim targets runtime#126380'sTypeMapHandler.UnwrapToResolvableTypefix). The dotnet/android end-to-end NativeAOT lane needs the SDK to ship a build with #126380 — currently in our SDK we have11.0.0-preview.4.26215.121which crashes ILLink before reaching the new code.SDK gating
The NativeAOT array-emission path requires dotnet/runtime#126380 (
TypeMapHandler.UnwrapToResolvableType, merged 2026-04-20, ships in .NET 11 nightly preview.5+). Until dotnet/android picks up an SDK with that fix:Array.CreateInstancedirectly; the typemap isn't consulted._AndroidTypeMapImplementation=trimmable— would crash in ILLink'sTypeMapHandler.RecordTypeMapEntryon the[assembly: TypeMap<__ArrayMapRank{N}>(...)]array entries. Not in any current dotnet/android CI lane (the typical NativeAOT lanes use Mono runtime or don't enable trimmable typemap), so the PR can land without blocking on the SDK bump. Once the SDK bumps, the path becomes live with no further changes.Considered alternatives
Documented in detail in #11234. Summary:
TypeMapAssociation<G>(typeof(T), typeof(T[]))): cleanerType → Typelookup, butProxyTypeMapNode.GetConditionalStaticDependenciesconditions onMaximallyConstructableType(key)(the element), soT[]survives wheneverTis reachable — losing per-shape trimming. Validated on disjoint-set NativeAOT tests."[L<jni>;","[[L<jni>;"): same trim behavior as per-rank groups but pays astring.Concatat everyJNIEnv.ArrayCreateInstancecall. Per-rank groups eliminate that cost.Out of scope
JavaList<T>/JavaCollection<T>/JavaDictionary<K, V>) and the correspondingJavaPeerContainerFactory<T>.Create*methods.JavaPeerProxy<T>.GetContainerFactory()virtual / override (deletion follows in the Phase 3 PR).TrimmableTypeMap.GetContainerFactory(Type).[DAM(Constructors)]annotations ([TrimmableTypeMap] Address all trimming and AOT warnings #10794).