Skip to content

[TrimmableTypeMap] Add array-typemap scaffolding (blocked on ILLink fix)#11238

Draft
simonrozsival wants to merge 2 commits intodev/simonrozsival/trimmable-typemap-javacastfrom
dev/simonrozsival/trimmable-array-typemap
Draft

[TrimmableTypeMap] Add array-typemap scaffolding (blocked on ILLink fix)#11238
simonrozsival wants to merge 2 commits intodev/simonrozsival/trimmable-typemap-javacastfrom
dev/simonrozsival/trimmable-array-typemap

Conversation

@simonrozsival
Copy link
Copy Markdown
Member

@simonrozsival simonrozsival commented Apr 28, 2026

Status

⚠️ Functionally complete; gated on the dotnet/android SDK picking up dotnet/runtime#126380. That PR (merged 2026-04-20, fixed in .NET 11 preview.5 nightly 11.0.100-preview.5.26228.123) makes ILLink's TypeMapHandler strip TypeSpecification wrappers (array, pointer, byref) before LinkContext.Resolve — closing dotnet/runtime#126177.

This PR currently ships the runtime + generator scaffolding but leaves EmitArrayEntries as a no-op until our SDK bumps to a build with #126380. Validation against a nightly SDK confirms the approach works as designed (see "Validation" below).

Tracking: #11234 (sub-task — arrays only).

What this enables (once flipped)

For every existing per-peer TypeMap<JavaObject>("<jni>", typeof(<Proxy>), typeof(<Target>)), also emit ranks 1–3 array entries:

[assembly: TypeMap<Java.Lang.Object>("[L<jni>;",   typeof(T[]),     typeof(T[]))]
[assembly: TypeMap<Java.Lang.Object>("[[L<jni>;",  typeof(T[][]),   typeof(T[][]))]
[assembly: TypeMap<Java.Lang.Object>("[[[L<jni>;", typeof(T[][][]), typeof(T[][][]))]

Runtime: TrimmableTypeMap.TryGetArrayType(elementType, out arrayType) walks down IsArray/GetElementType() to find the leaf type and array depth, resolves the leaf JNI encoding (primitive static dict OR TryGetJniNameForManagedType), and looks the array up via a new raw ITypeMapWithAliasing.TryGetType. JNIEnv.ArrayCreateInstance then calls Array.CreateInstanceFromArrayType (AOT-safe) — eliminating per-T JavaPeerContainerFactory<T>.CreateArray instantiations.

What's in this PR

File Change
Generator/ModelBuilder.cs EmitArrayEntries — documented no-op pending SDK bump that brings #126380. Body ready to flip on.
Microsoft.Android.Runtime/ITypeMapWithAliasing.cs New bool TryGetType(string jniName, out Type? type) — raw lookup that bypasses JavaPeerProxy filtering.
Microsoft.Android.Runtime/SingleUniverseTypeMap.cs Implements TryGetType.
Microsoft.Android.Runtime/AggregateTypeMap.cs Implements TryGetType (first-wins across universes).
Microsoft.Android.Runtime/TrimmableTypeMap.cs New internal bool TryGetArrayType(Type elementType, out Type? arrayType) implementing the leaf-walk + key-build + lookup flow.
Android.Runtime/JNIEnv.cs ArrayCreateInstance falls back to the legacy per-T factory under trimmable, with a TODO.
Java.Interop/JavaPeerContainerFactory.cs CreateArray/CreateHigherRankArray retained, with a TODO.
tests/.../TypeMapModelBuilderTests.cs Single regression test pinning that EmitArrayEntries produces no entries today.

Validation

Existing dotnet/android lanes (current SDK, scaffolding state)

  • 426 / 426 trimmable typemap unit tests pass.
  • Trimmable + CoreCLR RunTestApp lane on emulator: 917 total, 0 errors, 3 failures — the 3 failures are the pre-existing TryGetJniNameForManagedType_* tests called out as out-of-scope in [TrimmableTypeMap] JavaCast/JavaAs + container support #11225's PR description. No regression.

End-to-end with nightly SDK including #126380

Tested with a minimal repro program against 11.0.100-preview.5.26228.123 (which bundles Microsoft.NET.ILLink.Tasks 11.0.0-preview.5.26228.123):

[assembly: TypeMap<object>("Ljava/lang/Foo;",   typeof(MyFoo),     typeof(MyFoo))]
[assembly: TypeMap<object>("[Ljava/lang/Foo;",  typeof(MyFoo[]),   typeof(MyFoo[]))]
[assembly: TypeMap<object>("[[Ljava/lang/Foo;", typeof(MyFoo[][]), typeof(MyFoo[][]))]
[assembly: TypeMap<object>("Ljava/lang/Bar;",   typeof(MyBar),     typeof(MyBar))]
[assembly: TypeMap<object>("[Ljava/lang/Bar;",  typeof(MyBar[]),   typeof(MyBar[]))]
[assembly: TypeMap<object>("[[Ljava/lang/Bar;", typeof(MyBar[][]), typeof(MyBar[][]))]

new MyFoo();              // MyFoo reachable
var arr = new MyFoo[0];   // MyFoo[] reachable; MyFoo[][] NOT constructed.
                          // MyBar not referenced at all.

PublishTrimmed=true output:

Foo:     MyFoo
Foo[]:   MyFoo[]
Foo[][]: MyFoo[][]      ← kept (conditioned on element type MyFoo, which IS reachable)
Bar:     Not found
Bar[]:   Not found      ← all dropped (element type MyBar is unreachable)
Bar[][]: Not found

So the trimmer:

  • Keeps all 3 array ranks when the element type is reachable. Cost: 3× attribute entries per reachable peer.
  • Drops all 3 array ranks (along with the base peer entry) when the element type is unreachable.

This is exactly the trim-by-peer-reachability behavior we want. The 3× overhead is bounded by reachable peers only.

Path forward

Once dotnet/android's SDK picks up a build with #126380:

  1. Re-enable the body of EmitArrayEntries (3-arg conditional, ranks 1–3, with the existing skip rules for open generics / primitive JNI keywords / alias groups).
  2. Restore the runtime swap in JNIEnv.ArrayCreateInstance (uses TryGetArrayType + Array.CreateInstanceFromArrayType — already wired in this PR).
  3. Delete JavaPeerContainerFactory<T>.CreateArray / CreateHigherRankArray.

The runtime helpers shipped here (TryGetType, TryGetArrayType, TryGetPrimitiveJniEncoding) are the API the follow-up will call directly — already covered by unit tests via Build_DoesNotEmitArrayEntries_PendingILLinkFix.

Out of scope

Generator: for every per-peer TypeMap entry, also emit speculative
`[L<jni>;` / `[[L<jni>;` / `[[[L<jni>;` entries pointing at the
corresponding closed managed array type (`T[]`, `T[][]`, `T[][][]`).
Emitted as 3-arg conditional attributes so the trimmer drops entries
whose array target type is not live in the shipped app. Skips open
generics, primitive JNI keyword keys (`Z`, `B`, …), and alias groups
(deferred until a real-world need).

Runtime: new `ITypeMapWithAliasing.TryGetType` raw lookup that bypasses
`JavaPeerProxy` filtering — array entries point directly at the closed
`Type` and have no proxy. New `TrimmableTypeMap.TryGetArrayType(Type
elementType, out Type? arrayType)` walks down `elementType.IsArray` /
`GetElementType()` to find the leaf type and array depth, resolves the
leaf JNI encoding (primitive static dict OR `TryGetJniNameForManagedType`
wrapped as `L<jni>;`), prepends `[` × (depth+1), and looks up the
closed array `Type` via the raw typemap. Caller then uses AOT-safe
`Array.CreateInstanceFromArrayType`.

`JNIEnv.ArrayCreateInstance` swaps to the new flow on hit; throws
`NotSupportedException` on miss under trimmable. Legacy non-trimmable
fallback unchanged.

Cleanup: deletes `JavaPeerContainerFactory<T>.CreateArray` and the
private `CreateHigherRankArray` helper. The base abstract method goes
too. Other (collection) factory methods stay for the follow-up PR.

Tests: 13 new `ArrayEntries` unit tests in `TypeMapModelBuilderTests`
covering ranks 1-3, conditional emission, open-generic skip, alias-group
skip, and primitive-JNI-keyword skip. Existing count-asserting tests
updated via a `NonArrayEntries` helper.

Tracking: #11234 (sub-task — arrays only; collection factory removal
follows in a separate PR).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival changed the base branch from main to dev/simonrozsival/trimmable-typemap-javacast April 28, 2026 15:19
Reverts the runtime swap and factory-method removal from the previous
commit. Keeps the new `ITypeMapWithAliasing.TryGetType` raw lookup and
`TrimmableTypeMap.TryGetArrayType` helper as scaffolding for the
eventual ILLink-fixed approach.

The speculative `[L<jni>;` / `[[L<jni>;` / `[[[L<jni>;` TypeMap entries
crash ILLink with:

  System.NotSupportedException: TypeDefinition cannot be resolved from
    'Mono.Cecil.ArrayType' type
     at Mono.Linker.LinkContext.Resolve(TypeReference typeReference)
     at Mono.Linker.TypeMapHandler.RecordTypeMapEntry(...)         (3-arg form)
     at Mono.Linker.TypeMapHandler.MarkTypeMapAttribute(...)        (2-arg form)
     at Mono.Linker.TypeMapHandler.ProcessExternalTypeMapGroupSeen(...)

Both 2-arg and 3-arg TypeMap forms are affected — `MarkTypeMapAttribute`
calls `LinkContext.Resolve` on the `TargetType` slot (constructor arg
index 1) for any TypeMap, and Cecil's `ArrayType` is not a
`TypeDefinition`. There is no shape of `TypeMapAttribute` today that
accepts a closed array `Type`.

`EmitArrayEntries` is now a documented no-op.
`JavaPeerContainerFactory<T>.CreateArray` and
`CreateHigherRankArray` are restored.
`JNIEnv.ArrayCreateInstance` falls back to the legacy per-T factory
under trimmable.

Trimmable + CoreCLR lane after revert: 917 total, 0 errors, 3 failures
(pre-existing `TryGetJniNameForManagedType_*`, out of scope — same
baseline as #11225).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival changed the title [TrimmableTypeMap] Replace per-T array factory with TypeMap entries [TrimmableTypeMap] Add array-typemap scaffolding (blocked on ILLink fix) Apr 28, 2026
@simonrozsival simonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Apr 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

copilot `copilot-cli` or other AIs were used to author this trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant