[TrimmableTypeMap] Improve array handling#11238
Draft
simonrozsival wants to merge 2 commits intodev/simonrozsival/trimmable-typemap-javacastfrom
Draft
[TrimmableTypeMap] Improve array handling#11238simonrozsival wants to merge 2 commits intodev/simonrozsival/trimmable-typemap-javacastfrom
simonrozsival wants to merge 2 commits intodev/simonrozsival/trimmable-typemap-javacastfrom
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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).