Skip to content

[TrimmableTypeMap] Remove generated array proxy codegen and JavaPeerContainerFactory#12126

Merged
jonathanpeppers merged 9 commits into
dotnet:mainfrom
simonrozsival:android-trimmable-remove-unnecessary-array-codegen
Jul 16, 2026
Merged

[TrimmableTypeMap] Remove generated array proxy codegen and JavaPeerContainerFactory#12126
jonathanpeppers merged 9 commits into
dotnet:mainfrom
simonrozsival:android-trimmable-remove-unnecessary-array-codegen

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Jul 15, 2026

Copy link
Copy Markdown
Member

Summary

Follow-up to #12030, which added the runtime-only AOT-safe factories (SafeArrayFactory, SafeJavaCollectionFactory, ValueTypeFactory) that fully replace the old generated array-proxy code path and JavaPeerContainerFactory. That PR explicitly deferred removing the now-dead generator/runtime machinery to a follow-up — this is that follow-up. It also drops other machinery that is now unused: the generic JavaPeerProxy<T> base and the DynamicallyAccessedMembers annotation it carried.

Background — why this existed, and why we're removing it

The trimmable typemap originally avoided reflection entirely on the marshaling path. To create T[], JavaList<T>, JavaCollection<T>, and JavaDictionary<K,V> in an AOT-safe way (no MakeGenericType/MakeArrayType/Activator), the generator emitted, per binding type:

This construction was AOT-safe but expensive in app size: it forced a unique JavaPeerContainerFactory<T> and the cascading new T[] / new JavaList<T> / new JavaCollection<T> / new JavaDictionary<T,*> instantiations for every peer type — even ones never used as a collection/array element. For a typical .NET MAUI app (~2000 binding types) that's ~2000 factory instantiations plus their generic cascades — a substantial NativeAOT size hit. See #11234 for the full analysis.

#12030 switched this to reflection (Type.MakeArrayType() / Type.MakeGenericType() + Array.CreateInstanceFromArrayType) done in a trimming- and AOT-safe way: the calls are constrained to shapes NativeAOT can build (reference types canonicalize to __Canon; exact value-type arrays/instantiations are rooted via ValueTypeFactory), with the RequiresDynamicCode/RequiresUnreferencedCode suppressions isolated to those safe factories. The reflection path costs a small amount of startup work but removes the per-type generated bloat entirely — and it simplifies the generator and runtime a great deal.

This PR deletes the machinery that #12030 made redundant.

Changes

Runtime (src/Mono.Android)

  • Remove the JavaArrayProxy attribute base class and JavaPeerProxy.GetContainerFactory().
  • Delete JavaPeerContainerFactory.cs (and its Mono.Android.csproj entry).
  • Remove TrimmableTypeMap.TryGetArrayProxy, the array-proxy cache/sentinel, MissingJavaArrayProxy, dead managed-type-key helpers, and the per-rank array-map Initialize overloads (keep the 2-arg single and []-aggregate overloads that the generator actually emits).
  • Drop ITypeMap.TryGetArrayProxyType and the per-rank array-map constructors/field from AggregateTypeMap and SingleUniverseTypeMap.
  • Remove the generic JavaPeerProxy<T> base entirely. It only existed to auto-populate TargetType from typeof(T) and to root T's constructors (via [DynamicallyAccessedMembers]) for the removed GetContainerFactory(). Peers are always activated through the generated CreateInstance / InvokerType, never by reflecting over T, so the annotation and the generic base are no longer needed.
  • Update PublicAPI.Unshipped.txt for all API levels.

Generator (Microsoft.Android.Sdk.TrimmableTypeMap) + MSBuild

  • Remove ArrayProxyData/PrimitiveArrayProxyData, MaxArrayRank, AnchorRank, EmitArrayProxyType, __ArrayMapRank emission, and all maxArrayRank plumbing through the generator, the GenerateTrimmableTypeMap task, and the .targets.
  • Remove now-dead scanner array-entry infrastructure (GenerateArrayEntries, ReferencedTypeNamesByAssembly).
  • Every proxy (concrete class, interface, open generic definition) is now emitted deriving from the single JavaPeerProxy base with the (string, Type) ctor; removed the generic type reference and the base-type selection branch, the now-dead JavaPeerProxyData.IsInterface model property, and the per-anchor TypeMap ctor-ref caches left over from the rank-anchor mechanism.

Tests / docs

  • Remove the legacy TryGetArrayProxy_* device tests and the array-proxy generator/model/scanner unit tests and build-test expectations.
  • Update base-type / base-ctor-chain assertions and comments; remove the generic-proxy device test.

Related work

Validation

  • Microsoft.Android.Sdk.TrimmableTypeMap builds; 581 generator unit tests pass.
  • ✅ Repo-wide sweep confirms zero remaining references to any removed symbol.
  • ⏳ Full Mono.Android build + device tests to be validated in CI (requires a prepared local SDK not available in the authoring environment).

simonrozsival and others added 8 commits July 15, 2026 22:32
…ontainerFactory

PR #12030 added runtime-only AOT-safe factories (SafeArrayFactory,
SafeJavaCollectionFactory, ValueTypeFactory) that fully replace the old
generated array-proxy code path and JavaPeerContainerFactory. That PR
explicitly deferred removing the now-dead generator/runtime machinery to
a follow-up; this is that follow-up.

Runtime (src/Mono.Android):
- Remove the JavaArrayProxy attribute base class and JavaPeerProxy.GetContainerFactory().
- Delete JavaPeerContainerFactory.cs (and its Mono.Android.csproj entry).
- Remove TrimmableTypeMap.TryGetArrayProxy, the array-proxy cache/sentinel,
  MissingJavaArrayProxy, dead managed-type-key helpers, and the per-rank
  array-map Initialize overloads (keep the 2-arg single and []-aggregate ones).
- Drop ITypeMap.TryGetArrayProxyType and the per-rank array-map
  constructors/field from AggregateTypeMap and SingleUniverseTypeMap.
- Update PublicAPI.Unshipped.txt for all API levels.

Generator (Microsoft.Android.Sdk.TrimmableTypeMap) + MSBuild:
- Remove ArrayProxyData/PrimitiveArrayProxyData, MaxArrayRank, AnchorRank,
  EmitArrayProxyType, __ArrayMapRank emission, and all maxArrayRank plumbing
  through the generator, the GenerateTrimmableTypeMap task, and the .targets.
- Remove now-dead scanner array-entry infrastructure (GenerateArrayEntries,
  ReferencedTypeNamesByAssembly).

Tests:
- Remove the legacy TryGetArrayProxy_* device tests and the array-proxy
  generator/model/scanner unit tests and build-test expectations.

Validated: Microsoft.Android.Sdk.TrimmableTypeMap builds and its 581 unit
tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1240235b-7dc1-4d1d-8046-ca78ced61d6e
With GetContainerFactory() removed, JavaPeerProxy<T> no longer constructs
T (it only passes typeof(T) to the base), so the constructor
DynamicallyAccessedMembers annotation on T is no longer needed. Remove the
Constructors const and the now-unused System.Diagnostics.CodeAnalysis using.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1240235b-7dc1-4d1d-8046-ca78ced61d6e
…all proxies

JavaPeerProxy<T> only existed to auto-populate TargetType from typeof(T) and
(previously) to expose GetContainerFactory()/root T's constructors. With
GetContainerFactory() and the DynamicallyAccessedMembers annotation on T gone,
the generic base carries no remaining value: peers are always activated through
the generated CreateInstance / InvokerType, never by reflecting over T.

- Remove the JavaPeerProxy<T> class (and its PublicAPI entries).
- Generator now emits every proxy (concrete class, interface, open generic)
  deriving from the non-generic JavaPeerProxy base with the (string, Type) ctor,
  removing the JavaPeerProxy`1 type reference and the base-type branch.
- Update model/test comments and the base-ctor-chain assertions accordingly.

Validated: Microsoft.Android.Sdk.TrimmableTypeMap builds; 581 unit tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1240235b-7dc1-4d1d-8046-ca78ced61d6e
…xy entries

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1240235b-7dc1-4d1d-8046-ca78ced61d6e
There is only one JavaPeerProxy base class and the generic variant was never
shipped, so comments, the emitter field name (_javaPeerProxyNonGenericRef ->
_javaPeerProxyRef), and test names/InlineData no longer need to describe the
base as "non-generic".

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1240235b-7dc1-4d1d-8046-ca78ced61d6e
Collapsing every proxy onto the single JavaPeerProxy base removed the only
reader of the JavaPeerProxyData.IsInterface model property (it previously
selected the base type). It is now write-only, so drop the property and its
assignment in ModelBuilder. JavaPeerInfo.IsInterface (the scanner model) is
unaffected and still drives ACW gating and JCW generation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1240235b-7dc1-4d1d-8046-ca78ced61d6e
With AnchorRank/per-rank array-map entries gone, every TypeMap and
TypeMapAssociation attribute is emitted against the single default anchor.
The per-anchor ctor-ref caches (_typeMapAttr2ArgCtorRefByAnchor,
_typeMapAttr3ArgCtorRefByAnchor, _typeMapAssociationAttrCtorRefByAnchor)
and their GetOrAdd* helpers had no remaining callers, so remove them; the
default-anchor ctor refs are still built directly in the *CtorRef emitters.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1240235b-7dc1-4d1d-8046-ca78ced61d6e
…ref helpers

After removing the per-anchor caches, AddTypeMapAttr2ArgCtorRef and
AddTypeMapAttr3ArgCtorRef each have a single caller that always passes
_anchorTypeHandle. Drop the parameter and reference _anchorTypeHandle
directly, matching EmitTypeMapAssociationAttributeCtorRef.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1240235b-7dc1-4d1d-8046-ca78ced61d6e
@simonrozsival
simonrozsival marked this pull request as ready for review July 15, 2026 21:27
Copilot AI review requested due to automatic review settings July 15, 2026 21:27

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 is a cleanup follow-up to the trimmable typemap work: it removes the legacy generated array-proxy machinery and JavaPeerContainerFactory/JavaPeerProxy<T> infrastructure, consolidating the runtime + generator around the newer runtime-safe factories and a single non-generic JavaPeerProxy base.

Changes:

  • Removes runtime support for generated array proxies and container factories (JavaArrayProxy, JavaPeerContainerFactory, TrimmableTypeMap.TryGetArrayProxy, and per-rank array-map plumbing).
  • Simplifies the generator/MSBuild pipeline by deleting max-array-rank plumbing and array-entry emission, and making all proxies derive from JavaPeerProxy with (string, Type) ctor chaining.
  • Updates tests and PublicAPI tracking to match the removed symbols and updated proxy base behavior.
Show a summary per file
File Description
tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs Drops legacy TryGetArrayProxy_* device tests now that array-proxy support is removed.
tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaPeerProxyTests.cs Removes coverage for the deleted generic JavaPeerProxy<T> base.
tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs Removes assertions tied to the deleted array-entry scanning flags.
tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs Removes array-entry model tests and updates proxy base-type commentary.
tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs Updates generator tests to validate the single JavaPeerProxy base + 2-arg ctor chaining.
tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs Removes root-generator tests for per-rank array sentinel emission/target attributes.
src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets Stops caching the removed _AndroidTrimmableTypeMapMaxArrayRank property.
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs Removes build assertions that depended on array-rank sentinels and Cecil-based inspection.
src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs Deletes the MaxArrayRank task parameter and stops passing it to the generator.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets Removes _AndroidTrimmableTypeMapMaxArrayRank defaults and task parameter wiring.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets Removes MaxArrayRank parameter wiring for CoreCLR typemap generation.
src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt Removes unshipped API entries for deleted array/container proxy types and members.
src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt Removes unshipped JavaArrayProxy entries for API-36 surface.
src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt Removes unshipped JavaArrayProxy entries for API-36.1 surface.
src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt Removes unshipped JavaArrayProxy entries for API-35 surface.
src/Mono.Android/Mono.Android.csproj Removes compilation of the deleted JavaPeerContainerFactory.cs.
src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs Removes array-proxy lookup/cache and overloads taking per-rank array maps.
src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs Removes per-universe/per-rank array-map storage and lookup.
src/Mono.Android/Microsoft.Android.Runtime/ITypeMap.cs Removes the array-proxy lookup contract from the typemap abstraction.
src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs Removes array-proxy lookup aggregation across universes.
src/Mono.Android/Java.Interop/JavaPeerProxy.cs Deletes GetContainerFactory(), JavaPeerProxy<T>, and JavaArrayProxy definitions.
src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs Deletes the container-factory implementation file.
src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs Removes maxArrayRank argument/validation and stops threading it through generation.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs Removes framework-peer array-entry marking logic and related initialization.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs Removes the GenerateArrayEntries flag from the scanner model.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs Removes referenced-type-name indexing that only supported array-entry decisions.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs Removes maxArrayRank from assembly generator API and model building.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs Removes rank sentinels + array-proxy emission and standardizes proxy base emission.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs Removes array-sentinel targeting and array-map initialization IL emission.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs Removes array-entry and primitive-array-proxy model generation.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs Removes array-proxy model types, MaxArrayRank, and rank-anchored association data.

Copilot's findings

  • Files reviewed: 31/31 changed files
  • Comments generated: 1

Comment on lines +621 to +625
var proxyBaseType = (EntityHandle) _javaPeerProxyRef;
var baseCtorRef = _pe.AddMemberRef (_javaPeerProxyRef, ".ctor",
sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2,
rt => rt.Void (),
p => {
NUnit instantiates JavaListTest<T> reflectively from the
[TestFixture(typeof(...))] arguments, so the trimmable/NativeAOT typemap
never sees the concrete `new T ()` in Setup and trims the parameterless
constructors of JavaList / JavaList<string> (the trimmable typemap only
roots the (IntPtr, JniHandleOwnership) activation constructor of Java
peers). That made all 15 JavaListTest cases fail on the
NativeAOTTrimmable flavor with:

    System.MissingMethodException : MissingConstructor_Name, Android.Runtime.JavaList

Since there are exactly two fixtures, switch on typeof(T) and call the
constructors directly instead of relying on the `new()` constraint, which
cannot be honored under NativeAOT reflection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f9ec4f09-ffc5-4f66-99bf-ec40ea2728e8
@simonrozsival simonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jul 16, 2026
simonrozsival added a commit that referenced this pull request Jul 16, 2026
The trimmable typemap defaulted _AndroidTrimmableTypeMapMaxArrayRank to 3
whenever dynamic code was unavailable (NativeAOT / PublishAot), which emits
per-element-type __ArrayMapRank{N} array-proxy types. Those proxies are dead
code at runtime: array and collection creation now goes through the AOT-safe
SafeArrayFactory / ValueTypeFactory (added in #12030), and
TrimmableTypeMap.TryGetArrayProxy has no callers.

Generating them only bloats NativeAOT ILC compilation. On multi-ABI apps this
pushed ILC past the test-harness build cap, timing out NativeAOT lanes
(Check9PatchFilesAreProcessed, WorkManager, BundleToolTests) that passed
quickly (5-13 min) before this default became NativeAOT.

Default MaxArrayRank to 0 on every runtime (still overridable via
$(_AndroidTrimmableTypeMapMaxArrayRank)). This is a subset of the codegen
removal in #12126; the per-type JavaPeerContainerFactory<T> bloat is not
addressed here and still needs #12126.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: da26833c-c9ea-41a6-b8d7-7a87a60ce8fc
@jonathanpeppers

Copy link
Copy Markdown
Member

/review

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Android PR Reviewer completed successfully!

@github-actions github-actions Bot 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.

✅ LGTM (with one 💡 nit)

This is a clean, well-scoped follow-up to #12030 that deletes the now-dead generated array-proxy / JavaPeerContainerFactory machinery. I reviewed the runtime, generator, MSBuild task/targets, public API, and test changes independently before reading the description.

What I verified

  • No dangling references. Repo-wide sweep for every removed symbol (TryGetArrayProxy, JavaArrayProxy, JavaPeerContainerFactory, JavaPeerProxy<T>, GetContainerFactory, __ArrayMapRank, MaxArrayRank, _AndroidTrimmableTypeMapMaxArrayRank, GenerateArrayEntries, ArrayProxyData, IsInterface model prop, per-anchor ctor-ref caches) returns zero hits outside of the deletions themselves.
  • Consistent plumbing removal. maxArrayRank is dropped cleanly through GenerateTrimmableTypeMap task → .targetsTrimmableTypeMapGenerator → both generators, and the Initialize(...) overloads that carried array maps are gone with their callers updated to the 2-arg / no-array forms.
  • Proxy base consolidation. Every proxy now derives from the single non-generic JavaPeerProxy(string, Type) base. This matches the prior useNonGenericBase branch (open generics + interfaces already used it), and the retained comment correctly documents why TargetType stays correct without reflecting over T.
  • JavaListTest fix. The reflection-free new() workaround for the NativeAOT NUnit fixture is reasonable and well-commented; the NotSupportedException default guards against silently mishandling a new fixture type.
  • PublicAPI.Unshipped.txt updated for all API levels.

CI

Checks were still pending (no results yet) at review time. The PR carries the ready-to-review label noting any failures are expected-flaky. Please confirm the full Mono.Android build + device tests go green before merge, since the authoring environment couldn't run them.

Nit

One inline 💡 suggestion about an unused TypeSpec now emitted in the shared-universe path — trivial, not blocking.

Nice, thorough cleanup. 🎯

Generated by Android PR Reviewer for #12126 · 154 AIC · ⌖ 13 AIC · ⊞ 6.8K
Comment /review to run again

var initializeRef = AddInitializeSingleNoArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef);
EmitInitializeWithSingleTypeMapNoArrays (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef, assemblyName);
}
var initializeRef = AddInitializeSingleNoArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef);

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.

🤖 💡 Generator codegen — After removing the array path, externalDictTypeSpec (computed a few lines up, unconditionally) is now only consumed in the aggregate else branch. In the shared-universe path (this branch — the common Release/AOT default) it's unused, so every shared-universe root assembly still gets a spurious IReadOnlyDictionary<string, Type> TypeSpec added to its PE metadata. Consider moving the var externalDictTypeSpec = MakeIReadOnlyDictTypeSpec (...) line into the else branch alongside proxyDictTypeSpec so it's only emitted when actually referenced.

(Trivial; harmless correctness-wise — just avoids an unused metadata token.)

@jonathanpeppers
jonathanpeppers merged commit 27b0192 into dotnet:main Jul 16, 2026
44 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants