Skip to content

[TrimmableTypeMap] Add trimmable array proxy mapping#11753

Merged
simonrozsival merged 13 commits into
dotnet:mainfrom
simonrozsival:android-trimmable-typemap-array-codegen
Jun 29, 2026
Merged

[TrimmableTypeMap] Add trimmable array proxy mapping#11753
simonrozsival merged 13 commits into
dotnet:mainfrom
simonrozsival:android-trimmable-typemap-array-codegen

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Jun 26, 2026

Copy link
Copy Markdown
Member

Goal

Extract the array type-mapping slice from #11617 so the trimmable typemap can resolve and allocate JNI array shapes without relying on reflection-only or dynamic-code array construction paths.

#11617 implements the broader reflection-free TrimmableTypeMapValueManager / TrimmableTypeMapTypeManager design. This PR recreates the pieces of that work specifically needed for array mapping:

  • runtime JavaArrayProxy metadata,
  • primitive array metadata via PrimitiveArrayInfo,
  • JavaConvert container factory support for scalar and Java peer element types,
  • per-rank __ArrayMapRankN codegen and loader wiring,
  • generated array proxies for object and primitive JNI array shapes.

Background from #11617

The larger #11617 design makes generated typemap metadata the source of truth for trimmable/NativeAOT Java interop. One important part of that is array handling: NativeAOT cannot safely fall back to Array.CreateInstance() / MakeArrayType() for all shapes, and the trimmable path should not depend on late reflection to discover T[], JavaArray<T>, JavaObjectArray<T>, or primitive array wrapper representations.

Instead, #11617 generates rank-scoped typemap groups and a proxy type for each supported array shape. This PR ports that array-specific design into this branch.

Change map

Runtime

  • Adds Java.Interop.JavaArrayProxy, a self-applied generated attribute/proxy base with:
    • GetArrayTypes() for all managed representations that map to a JNI array shape.
    • CreateManagedArray(int length) for NativeAOT-safe direct array allocation.
  • Changes TrimmableTypeMap from direct Type array lookup to generated array-proxy lookup:
    • TryGetArrayProxy(Type elementType, int additionalRank, out JavaArrayProxy?).
    • per-rank lookup is keyed by the simple assembly-qualified managed element type name plus rank index (for example, Android.Net.Network, Mono.Android), avoiding a managed-to-JNI signature lookup during runtime array proxy resolution.
  • Updates JNIEnv.ArrayCreateInstance():
    • CoreCLR keeps dynamic Array.CreateInstance().
    • NativeAOT uses generated JavaArrayProxy.CreateManagedArray().
  • Adds PrimitiveArrayInfo for primitive JNI array metadata and wrappers.
  • Updates array compatibility checks in JNIEnv to derive JNI array classes from element type signatures instead of using MakeArrayType().
  • Extends JavaConvert / JavaPeerContainerFactory so trimmable conversions can create generic Java containers for scalar element types as well as Java peer types without using MakeGenericType() on the trimmable path. JavaPeerContainerFactory<T> and its Create<T> helper are unconstrained (no IJavaPeerable requirement) so scalar element types such as int and string also get a container factory.

Code generation

  • Adds ArrayProxyData and PrimitiveArrayProxyData to the typemap model.
  • Emits one generated array proxy per managed element type and supported rank.
  • Emits primitive array proxies once from _Java.Interop.TypeMap for primitive JNI keywords (Z, B, C, S, I, J, F, D).
  • Emits rank-scoped TypeMap<__ArrayMapRankN> entries keyed by simple assembly-qualified managed element type names and pointing at generated array proxies, instead of pointing directly at closed array types.
  • Emits rank-scoped TypeMapAssociation<__ArrayMapRankN> entries for each managed representation returned by the proxy:
    • object arrays: JavaObjectArray<T>, JavaArray<T>, T[];
    • primitive arrays: T[], JavaArray<T>, JavaPrimitiveArray<T>, concrete wrappers such as JavaSByteArray;
    • higher ranks: nested JavaObjectArray<...> and SZ-array forms.
  • Updates the root typemap loader to materialize both external and proxy maps for every __ArrayMapRankN group, so rank-scoped associations participate in trimming and runtime lookup.
  • Adds PE signature helpers for generated array proxy ldtoken/newarr emission.
  • Replaces the hand-written ELEMENT_TYPE_* / calling-convention signature byte literals across the generator (PEAssemblyBuilder, TypeMapAssemblyEmitter, RootTypeMapAssemblyGenerator, ExportMethodDispatchEmitter) with the named System.Reflection.Metadata enums (SignatureTypeCode, SignatureTypeKind, SignatureKind, SignatureCallingConvention, SignatureAttributes). Every emitted byte is identical; this is a readability-only change.

Why this shape

The previous local implementation mapped __ArrayMapRankN entries directly to closed managed array Types and used Array.CreateInstanceFromArrayType() as the NativeAOT allocation path. The first version of this extracted PR keyed generated array maps by JNI element name. The current shape keys them by the simple assembly-qualified managed element type name so runtime lookup starts from the Type it already has and avoids another managed-to-JNI lookup. The #11617 design is more complete because the generated proxy can describe all managed representations of a JNI array shape, not just one T[] target, and can allocate arrays with direct newarr IL.

That matters for NativeAOT because array metadata/allocation support depends on what is rooted. The generated proxy makes the needed array shape explicit and avoids late reflection fallback.

Tests

Updated coverage includes:

  • model tests that verify rank entries point to generated array proxies;
  • association tests that verify generated array proxies are associated with the same representations returned by GetArrayTypes();
  • primitive proxy synthesis tests for _Java.Interop.TypeMap;
  • PE round-trip tests for rank-scoped TypeMap and TypeMapAssociation blobs;
  • runtime test updates from TryGetArrayType() to TryGetArrayProxy(), including managed-key rank-map lookup behavior;
  • BuildTest2.BuildHasNoWarnings NativeAOT expected warning count updated from 10 to 8 — the generated array proxies make two previously reflection-based array-creation paths AOT-safe, removing two IL3050 warnings. The assertion now reports the actual vs expected warning count instead of a bare Expected: True But was: False.

Validation

Passed locally:

dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj --no-restore -v:minimal

Result: 590 passed.

Also attempted src/Mono.Android/Mono.Android.csproj build. After initializing external/Java.Interop and providing local javac/jar paths, the build progressed past C# compile checks visible in the log but was stopped when a Gradle dependency step requested interactive Azure DevOps credential login. No C# errors were present in the captured output before that prompt.

NativeAOT warning impact

A measurable outcome of routing array creation through generated proxies: a NativeAOT app build now emits 8 IL3050 AOT-analysis warnings instead of 10. The two removed warnings came from the reflection-based array-creation paths (Array.CreateInstance() / MakeArrayType()) that the generated JavaArrayProxy.CreateManagedArray() replaces. Verified from the BuildHasNoWarnings apk and aab NativeAOT build logs.

Recreate the trimmable typemap array-mapping pieces from the Java.Interop integration branch so NativeAOT can resolve JNI array shapes without reflection or dynamic array construction.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 26, 2026 11:02
@simonrozsival simonrozsival changed the title [typemap] Add trimmable array proxy mapping [TrimmableTypeMap] Add trimmable array proxy mapping Jun 26, 2026

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 extracts and lands the trimmable typemap “array slice” (from #11617) so the runtime and generator can resolve JNI array shapes via generated per-rank metadata and AOT-safe allocation (via generated JavaArrayProxy + newarr IL), avoiding reflection-based array construction on NativeAOT.

Changes:

  • Introduces generated JavaArrayProxy metadata and switches the trimmable runtime array lookup from “closed array Type” to “generated array proxy” per JNI leaf + rank.
  • Extends typemap generation to emit per-rank __ArrayMapRankN entries pointing to generated array proxies, plus rank-scoped TypeMapAssociation entries matching GetArrayTypes().
  • Updates runtime array creation and compatibility checks (JNIEnv) and trimmable container creation (JavaConvert/JavaPeerContainerFactory) to better support AOT/trimming scenarios.
Show a summary per file
File Description
tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs Updates runtime tests to validate TryGetArrayProxy() behavior and returned managed representations.
tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs Updates/extends generator model tests for array proxies, associations, primitive proxy synthesis, and PE blob round-trips.
src/Mono.Android/Mono.Android.csproj Adds the new PrimitiveArrayInfo.cs to the Mono.Android build.
src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs Replaces TryGetArrayType() with TryGetArrayProxy() and adds array-proxy caching.
src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs Renames array lookup API to return proxy types (TryGetArrayProxyType).
src/Mono.Android/Microsoft.Android.Runtime/PrimitiveArrayInfo.cs Adds helper logic for primitive array wrappers/signatures/conversions (new file).
src/Mono.Android/Microsoft.Android.Runtime/ITypeMap.cs Updates type map interface to resolve array proxy types instead of array Types.
src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs Updates aggregate type map to forward TryGetArrayProxyType.
src/Mono.Android/Java.Interop/JavaPeerProxy.cs Adds new public JavaArrayProxy : Attribute contract for generated proxies.
src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs Adjusts container factory generic/DAM plumbing to support non-peer scalar element types on the trimmable path.
src/Mono.Android/Java.Interop/JavaConvert.cs Adds scalar container factories and refactors factory-based conversions to support scalars + peers without MakeGenericType() on trimmable path.
src/Mono.Android/Android.Runtime/JNIEnv.cs Uses generated array proxies for NativeAOT array creation; updates array compatibility checks to derive array JNI classes from element signatures.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs Emits array proxy types, runtime type signatures for ldtoken/newarr, and rank-scoped association attribute ctor wiring.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs Ensures per-rank proxy maps are materialized alongside external maps for array rank groups.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs Adds helper to encode runtime type signatures (incl. SZARRAY and primitives).
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs Generates array proxy model entries + associations; synthesizes primitive array proxies for _Java.Interop.TypeMap.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs Adds model types for array proxy emission and rank-scoped associations.

Copilot's findings

  • Files reviewed: 17/17 changed files
  • Comments generated: 4

Comment thread src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs Outdated
Comment thread src/Mono.Android/Java.Interop/JavaPeerProxy.cs
Use the simple assembly-qualified managed element type name as the generated array map key, avoiding managed-to-JNI signature lookup when resolving array proxies at runtime.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival added the copilot `copilot-cli` or other AIs were used to author this label Jun 26, 2026
simonrozsival and others added 3 commits June 26, 2026 13:24
Rename TypeMapAttributeData.JniName to MapKey and use managedName in array-specific PE assertions so managed-key array maps are not described as JNI names.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add public API baseline entries for JavaArrayProxy and rename sbyte array proxy locals in runtime tests to avoid byte/sbyte ambiguity.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use the existing JavaPeerContainerFactory<T>.Instance singletons for scalar JavaConvert factories so Mono.Android can compile with the factory constructor kept private.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

Android PR Reviewer completed successfully!

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Needs Changes

Nice slice out of #11617 — the array-proxy design is genuinely AOT-friendly and the refactor is careful. One real cleanup item plus a few polish suggestions, and CI isn't green yet.

Findings

  • 1 ⚠️ warningPrimitiveArrayInfo.cs (~224 lines) is compiled into Mono.Android but has zero callers; it's dead code as currently wired. Either integrate it (with a test), defer it to the slice that consumes it, or remove it. (inline)
  • 3 💡 suggestions — missing XML docs on the new public JavaArrayProxy API; an unexplained GetProxy<...>() + Pop in EmitArrayMapsByRank; and a manual ArgumentOutOfRangeException that could use ThrowIfNegativeOrZero. (all inline)

What looks good

  • Clean, mechanical JniNameMapKey rename throughout.
  • Array-proxy lookup path avoids MakeGenericType by routing through the generated proxy + JavaPeerContainerFactory<T> — fully new-based and AOT-safe.
  • Scalar container support via the static ScalarContainerFactories map is a tidy, allocation-light addition.
  • Solid coverage: PE round-trip tests, generator/model tests, and runtime TryGetArrayProxy rank tests.

Verified as not issues (checked against full context)

  • FindArrayClassByElementType element-type→array-class +1 rank arithmetic is correct.
  • AddArrayRank/assembly-qualified key manipulation (LastIndexOf (", ")) is sound.
  • _arrayProxyCache (ConcurrentDictionary + static lambda + sentinel) is thread-safe.
  • Relaxing the class, IJavaPeerable constraint on JavaPeerContainerFactory<T> is intentional and required for scalar element types.

CI

The internal Azure DevOps builds (Linux/Mac/Windows) were still in progress / pending at review time — license/cla passed but the pipeline isn't green. Please confirm CI is green before merge; the ⚠️ verdict stands regardless of the dead-code item.

Generated by Android PR Reviewer for issue #11753 · 1.7K AIC · ⌖ 48.1 AIC · ⊞ 40K
Comment /review to run again

Comment thread src/Mono.Android/Microsoft.Android.Runtime/PrimitiveArrayInfo.cs
Comment thread src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs Outdated
Comment thread src/Mono.Android/Java.Interop/JavaPeerProxy.cs
simonrozsival and others added 2 commits June 26, 2026 15:09
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Add XML doc comments to the new public JavaArrayProxy attribute and its
GetArrayTypes ()/CreateManagedArray (int) members, consistent with the
sibling JavaPeerProxy types.

Document why EmitArrayMapsByRank discards the
GetOrCreateProxyTypeMapping<__ArrayMapRank{N}> () result: the call roots the
per-rank [TypeMapAssociation] entries so the trimmer/ILC keeps the proxy
types referenced by the external map, even though the proxy map itself is
never stored at runtime.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member Author

Self-review turned up two minor 💡 nits, both purely cosmetic with no behavioral, perf, or public-API impact:

  1. JavaPeerContainerFactory<T> is intentionally unconstrained (so internal scalar factories like int/string work) while the public Create<T>() keeps where T : class, IJavaPeerable — a clarifying comment could note the asymmetry.
  2. In JavaConvert.GetJniHandleConverter, the target.IsGenericType && !target.IsGenericTypeDefinition guard is checked at the call site and again inside both local helpers — could be collapsed to one.

Not worth doing right now: the dotnet-android matrix is a multi-hour build+test cycle, and pushing for these would cancel/restart the in-flight run for zero functional change. Leaving them for now; will fold in opportunistically if another substantive push is needed.

@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

Android PR Reviewer completed successfully!

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated review — [TrimmableTypeMap] Add trimmable array proxy mapping

Focused review of the array type-mapping extraction. The core redesign — replacing MakeArrayType() / Array.CreateInstanceFromArrayType with generator-emitted JavaArrayProxy types resolved through TrimmableTypeMap.TryGetArrayProxyCreateManagedArray — is coherent and reads well for an extraction PR.

Verified as consistent:

  • The new managed-type-key scheme ("{FullName}, {assembly}", with primitives and string pinned to System.Runtime) matches on both sides: generator TypeMapAssemblyEmitter/ModelBuilder vs. runtime TryGetManagedTypeKey / GetAssemblyNameForManagedTypeKey.
  • ArrayCreateInstance is only reached for IJavaObject / Array element types; primitive single-rank arrays keep their dedicated fast-path handlers, so the byte[]/int[]/etc. copy paths are unaffected.
  • Tests cover object and primitive leaves at multiple ranks (TryGetArrayProxy_*), and the new JavaArrayProxy API is added to all four PublicAPI.Unshipped.txt files.

Please address before merge:

  • ⚠️ PrimitiveArrayInfo.cs is unused (~224 lines, zero callers) — wire it up or move it to the follow-up that consumes it, rather than shipping dead code in Mono.Android.dll. (inline)
  • 💡 Stale comment in JNIEnv.ArrayCreateInstance still references IsDynamicCodeSupported / [FeatureGuard] after the switch to IsCoreClrRuntime. (inline)

CI: the internal Azure DevOps Linux > Build leg is green (so this compiles); the MSBuild/emulator test legs were still in progress at review time, so I'm not asserting test outcomes.

Automated review — not a substitute for maintainer sign-off, and I don't submit approvals. The "request changes" status reflects the unused-file item; feel free to dismiss if PrimitiveArrayInfo is intentionally staged for the next PR in the stack.

Generated by Android PR Reviewer for issue #11753 · 1.6K AIC · ⌖ 48.1 AIC · ⊞ 40K
Comment /review to run again

Comment thread src/Mono.Android/Android.Runtime/JNIEnv.cs
Comment thread src/Mono.Android/Microsoft.Android.Runtime/PrimitiveArrayInfo.cs
@simonrozsival simonrozsival dismissed github-actions[bot]’s stale review June 26, 2026 14:37

These nitpicks are irrelevant.

simonrozsival and others added 3 commits June 26, 2026 17:06
The trimmable array proxy codegen makes array creation AOT-safe and removes
two reflection-based IL3050 warnings, dropping the NativeAOT MSBuild warning
count from 10 to 8 (verified from the apk and aab build logs on build
1483030).

Also report the actual vs expected warning count: parse the "N Warning(s)"
MSBuild summary line and assert via Assert.AreEqual so a future mismatch
shows the real number instead of "Expected: True But was: False".

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- JavaConvert.GetJniHandleConverter: drop the redundant
  "target.IsGenericType && !target.IsGenericTypeDefinition" guard at the call
  site; TryGetFactoryBasedConverter's local helpers already perform that
  check, so the guard was triplicated.
- JavaPeerContainerFactory.Create<T>: document why the open
  JavaPeerContainerFactory<T> is unconstrained (internal scalar factories)
  while the public Create<T> keeps the IJavaPeerable constraint.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the where T : class, IJavaPeerable constraint so Create<T> matches the
already-unconstrained JavaPeerContainerFactory<T>, and drop the asymmetry
remarks that explained the now-removed mismatch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

Android PR Reviewer completed successfully!

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

Thanks @simonrozsival — this cleanly extracts the array type-mapping slice from #11617. The JniNameMapKey rename (managed-type-name keys like Android.App.Activity, Mono.Android) is consistent and well covered by the model/PE round-trip tests, the per-rank TrimmableTypeMap.TryGetArrayProxy design and JavaArrayProxy abstraction read well, and the AOT-safe newarr-based CreateManagedArray is a nice touch. I also like that BuildTest2 now asserts the actual vs. expected NativeAOT warning count with a useful message instead of a bare number.

A couple of things to address before merge:

  • ⚠️ PrimitiveArrayInfo is unused — ~220 lines with no callers; either wire it up to its consumer or drop it from this PR (inline comment).
  • 💡 Stale comment in JNIEnv.ArrayCreateInstance — still references the old IsDynamicCodeSupported [FeatureGuard] after the switch to RuntimeFeature.IsCoreClrRuntime (inline comment). Behavior is correct; the comment just no longer matches the code.

Things I checked that are fine:

  • IL3050 suppression is sound — Microsoft.Android.Sdk.NativeAOT.targets sets IsCoreClrRuntime = false with Trim="true", so the Array.CreateInstance branch is removed under NativeAOT.
  • TryGetFactoryBasedConverter dropping its inner RuntimeFeature.TrimmableTypeMap guard is safe — the caller in GetJniHandleConverter still guards it.
  • GetAssemblyNameForManagedTypeKey hardcoding System.Runtime for primitives/string correctly mirrors the generator (runtime would otherwise see System.Private.CoreLib).

Note: the internal CI build is still pending and the PR is mergeable_state: blocked, so I can't confirm a green build from here.

Requesting changes primarily for the unused PrimitiveArrayInfo; the rest is minor. Nice work overall.

Generated by Android PR Reviewer for issue #11753 · 1.3K AIC · ⌖ 47.4 AIC · ⊞ 37.9K
Comment /review to run again

Comment thread src/Mono.Android/Microsoft.Android.Runtime/PrimitiveArrayInfo.cs
Comment thread src/Mono.Android/Android.Runtime/JNIEnv.cs
@simonrozsival simonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jun 26, 2026
@simonrozsival simonrozsival enabled auto-merge (squash) June 29, 2026 07:26
simonrozsival added a commit that referenced this pull request Jun 29, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 29, 2026
#11798)

## Problem

When R8 code shrinking is enabled, R8 removes any Java type that nothing keeps. .NET for Android keeps the Java callable wrappers (JCWs) of bound managed types by generating `-keep` rules from the **acw-map** (managed peer ↔ Java type). That works for generated JCWs, but **user-authored `AndroidJavaSource` `.java` files marked `Bind` != `true` have no managed peer**, so they never appear in the acw-map — and therefore get no keep rule. With shrinking on, R8 silently deletes them, and the app fails at runtime when it references those classes (often as a `NoClassDefFoundError` that only reproduces in Release/shrunk builds).

## Fix

Pass user `AndroidJavaSource` (`Bind` != `True`) to the `R8` task and emit `-keep class <package>.<Type> { *; }` for each, so user Java survives shrinking:

* **`D8.targets`** — collect `@(AndroidJavaSource)` items where `%(Bind) != 'True'` into `_R8KeepJavaSource` and pass them to `R8` via the new `JavaSourceFiles` property.
* **`R8.cs`** — append keep rules in the same block that emits acw-map keeps. `GetUserJavaTypes()` derives the fully-qualified name as `<package>.<FileNameWithoutExtension>` (Java requires the public top-level type name to match the file name); `ReadJavaPackage()` parses the `package` declaration, skipping comments and stopping at the first `import`/type declaration. Names are de-duplicated.
* **`R8Tests.cs`** — unit tests covering `ReadJavaPackage`: package present, trailing space before `;`, comment headers, no package, and `package` after `import`/type (ignored).

## Why split out

This is being carved out of the trimmable typemap mega-PR #11617. Although the bug surfaced while bringing up the NativeAOT trimmable path, **it is not trimmable-specific** — the acw-map keep logic and R8 shrinking apply equally to legacy/MonoVM and CoreCLR, so user `AndroidJavaSource` was at risk of being shrunk there too. Landing it independently de-risks #11617 and ships a general correctness fix sooner.

## Context / related PRs

- #11617 — parent mega-PR (reflection-free trimmable typemap) this is sliced from.
- #11643 — removes the managed NativeAOT typemap in favor of the trimmable one (the path that exposed this gap).
- Sibling slices already split out off `main`: #11794 (AndroidApplicationJavaClass/multidex), #11796 (manifest generation parity). Other merged groundwork: #11749, #11751, #11752, #11753, #11764, #11769.

## Verification

24/24 R8 unit tests pass (18 existing + 6 new). `BuildAfterMultiDexIsNotRequired` verified locally on NativeAOT and CoreCLR — user Java is retained after shrinking.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival and others added 3 commits June 29, 2026 16:34
Address PR feedback: replace the raw ELEMENT_TYPE_* byte literals in
WriteTypeSignature with the built-in System.Reflection.Metadata
SignatureTypeCode / SignatureTypeKind enums. The emitted bytes are
identical; this only improves readability.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extend the SignatureTypeCode cleanup to the rest of the generator's
hand-written metadata blobs. Replace the remaining magic ELEMENT_TYPE_*
/ calling-convention byte literals in RootTypeMapAssemblyGenerator,
TypeMapAssemblyEmitter, ExportMethodDispatchEmitter and PEAssemblyBuilder
with the matching System.Reflection.Metadata enums:

  SignatureTypeCode        (Void, primitives, String, Object, IntPtr,
                            SZArray, GenericTypeInstance)
  SignatureTypeKind        (Class, ValueType)
  SignatureKind            (LocalVariables, MethodSpecification)
  SignatureCallingConvention.Default
  SignatureAttributes      (Instance, Generic)

Every emitted byte is identical to the previous literal; this is a pure
readability change. All 563 generator unit tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…emap-array-codegen

# Conflicts:
#	src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs
@simonrozsival simonrozsival merged commit d287c4d into dotnet:main Jun 29, 2026
40 checks passed
simonrozsival added a commit that referenced this pull request Jun 30, 2026
Adds TrimmableTypeMapValueManager + reflection-free TrimmableTypeMapTypeManager and
wires opt-in selection (RuntimeFeature.TrimmableTypeMap) in JNIEnvInit. Builds on the
CoreCLR JavaMarshal split. Default stays managed; depends on array-codegen (#11753)
and other slices to be fully functional at runtime.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 30, 2026
…TypeManagers (#11799)

## Summary

Bumps **external/Java.Interop** and aligns Mono.Android's reflection-based type and value managers with the new base-class contracts. The Java.Interop bump relaxes the `[DynamicallyAccessedMembers]` (DAM) requirements on the virtual members of `JniRuntime.ReflectionJniTypeManager` / `JniRuntime.ReflectionJniValueManager`, so the Mono.Android overrides no longer need to repeat those annotations and instead rely on targeted trimmer/AOT suppressions.

This is a standalone slice of #11617 with no dependency on the trimmable type-map scanner/emitter or array codegen work.

## Changes

- `external/Java.Interop` → `8d544738a` (from `70493645c`).
- Drop now-redundant `[DynamicallyAccessedMembers]` annotations from the overrides in `AndroidTypeManager`, `ManagedTypeManager`, and `TrimmableTypeMapTypeManager` (`GetInvokerTypeCore`, `GetTypeForSimpleReference`, `RegisterNativeMembers`, `ActivatePeer`), replacing them with `[UnconditionalSuppressMessage]` where the trimmer still needs reassurance.
- `JavaMarshalValueManager` now extends `JniRuntime.ReflectionJniValueManager` directly. It is marked `sealed` and carries `[RequiresDynamicCode]` / `[RequiresUnreferencedCode]`, uses the base `EnsureNotDisposed ()` helper, and drops its own dispose tracking and `ActivatePeer` override.
- Remove the superseded `AndroidReflectionJniValueManager` and `SimpleValueManager` (and their `Mono.Android.csproj` entries).
- `JNIEnvInit.CreateValueManager` creates the value manager through a local helper with the appropriate trimming/AOT suppressions for both the CoreCLR and NativeAOT paths.
- No NativeAOT default change; the trimmable type/value managers are **not** part of this PR.

## Tests / baselines

- Update the API-compatibility baseline: `Android.Graphics.ColorValueMarshaler.CreateGenericValue`'s `targetType` parameter no longer carries a DAM attribute (inherited from the Java.Interop base-class change).
- Refresh `SimpleDotNet` CoreCLR/NativeAOT apkdesc size baselines and the NativeAOT `BuildHasNoWarnings` count, plus `BuildTest2`.

## Context

Carved out of #11617. `JavaMarshalRegisteredPeers` extraction already merged via #11750. The trimmable managers, scanner/emitter, manifest, and R8 changes ship in their own PRs (#11749/#11751/#11753/#11769/#11794/#11796/#11798).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 30, 2026
Adds TrimmableTypeMapValueManager + reflection-free TrimmableTypeMapTypeManager and
wires opt-in selection (RuntimeFeature.TrimmableTypeMap) in JNIEnvInit. Builds on the
CoreCLR JavaMarshal split. Default stays managed; depends on array-codegen (#11753)
and other slices to be fully functional at runtime.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jul 2, 2026
…11801)

## Summary

This PR adds the two runtime managers that make the **trimmable typemap** a fully functional, reflection-free peer-resolution path:

- **`TrimmableTypeMapValueManager`** — creates and tracks Java↔managed peers without reflection or `Activator`.
- **`TrimmableTypeMapTypeManager`** — resolves managed↔Java type mappings by delegating to the generated `TrimmableTypeMap`.

Both are selected through the existing `RuntimeFeature.TrimmableTypeMap` switch and wired into `JNIEnvInit`. The trimmable path is **opt-in**; the NativeAOT default remains `managed`, and Mono/CoreCLR defaults are unchanged. Also included is the `JavaConvert` collection-factory refactor and a generator fix that the trimmable managers depend on.

## Background

The classic Java.Interop runtime resolves the managed `Type` for a Java instance (and vice-versa) using `Type.GetType`, `MakeGenericType`, and `Activator`-style construction. That is incompatible with trimming and NativeAOT: the trimmer can't see which types are reachable, and the types can't be created at runtime under AOT.

The **trimmable typemap** replaces those reflection calls with a build-time-generated map of `JavaPeerProxy` objects (produced by `Microsoft.Android.Sdk.TrimmableTypeMap`). Each proxy knows how to construct a specific peer and describes its JNI ↔ managed association statically. Earlier PRs landed the generator, the proxy shapes (array / generic-base / interface / unresolvable peers), and the CoreCLR `JavaMarshal` value-manager split. **This PR adds the runtime managers that consume that generated map.**

## What's in this PR

### 1. `TrimmableTypeMapValueManager` (new)

A `JniRuntime.JniValueManager` that performs peer creation with **no reflection**:

- **Peer lifetime** (`AddPeer` / `PeekPeer` / `RemovePeer` / `FinalizePeer` / `CollectPeers` / `GetSurfacedPeers`) is delegated to `JavaMarshalRegisteredPeers`, i.e. the CoreCLR `JavaMarshal` GC-bridge machinery. `WaitForGCBridgeProcessing` is intentionally a no-op (documented: the wait can't close the bridge race on CoreCLR, where JNI wrapper threads hold their own `JniObjectReference` copies).
- **`CreatePeer`** resolves the requested target type (mapping `object`/`IJavaPeerable` → `Java.Interop.JavaObject`, `Exception` → `JavaException`) and asks `TrimmableTypeMap.Instance.CreateInstance (handle, resolvedType)` to build the peer from a generated proxy.
- **`ActivatePeer`** throws `PlatformNotSupportedException` — reflection-based activation is not part of this path.
- **`NotFoundFallback`** carefully reproduces the base `JniValueManager.CreatePeer` contract so `JavaCast`/`JavaAs` still surface the correct outcome when no proxy is found:
  - target type has no Java mapping → `ArgumentException`
  - Java instance not assignable to the target's Java class → `null` (so `JavaAs` returns null / `JavaCast` throws `InvalidCastException`)
  - compatible classes but no proxy → `NotSupportedException` (a genuine generator gap, with a message pointing at the missing proxy)

  The assignability check honors `RuntimeFeature.IsAssignableFromCheck` and mirrors the legacy cast diagnostic when assembly logging is enabled.

### 2. `TrimmableTypeMapTypeManager` (new)

A `JniRuntime.JniTypeManager` that has exactly two live responsibilities and throws for everything else it doesn't need:

- **Managed → Java** via `GetTypeSignatureCore`, backed by a `ConcurrentDictionary<Type, JniTypeSignature>` cache.
- **Java → managed** via `GetTypes` / `GetTypeForSimpleReference`, delegating to `TrimmableTypeMap`.
- **Array handling** diverges by runtime: NativeAOT reads a pre-generated array-proxy map (types can't be built at runtime), while CoreCLR builds array/generic types dynamically to save app size (suppressions are scoped to the CoreCLR-only branch).

### 3. `JNIEnvInit` wiring

`CreateValueManager` / `CreateTypeManager` now return the trimmable managers when `RuntimeFeature.TrimmableTypeMap` is set. The existing Mono / CoreCLR / managed selection is preserved, and the manager constructions were refactored into small local helpers so trimming suppressions (`IL2026` / `IL3050`) apply only to the exact branch that needs them. `RegisterJniNatives` is likewise gated so the reflection-based JNI registration path isn't emitted for the trimmable typemap.

### 4. `JavaConvert` collection-factory refactor

Generic collection marshalling (`IDictionary<,>` → `JavaDictionary<,>`, `IList<>` → `JavaList<>`, `ICollection<>` → `JavaCollection<>`) is split into two branches: a **factory-based converter** on the trimmable path (no `MakeGenericType`) and the classic `MakeGenericType` path elsewhere, with the reflection-requiring code isolated behind narrowly-scoped suppressions. Also adds `Nullable<T>` converter handling (null reference → null value).

### 5. Generator fix (`ModelBuilder`)

Emits a managed→Java typemap entry for **self-peer types** (`[JniTypeSignature(GenerateJavaPeer=false)]` or MCW bindings with no activation ctor). These are constructed managed-side with `new`, but their JNI name must still resolve so the correct Java class is instantiated; without the association they fell back to the generic `mono.android.runtime.JavaObject` peer and threw `ArrayStoreException` when placed into a typed Java array.

### 6. Cleanup

Removes the dead `TrimmableTypeMap` branch from `JavaMarshalValueManager` (that logic now lives in the dedicated `TrimmableTypeMapValueManager`).

## Behavioral impact

- **Opt-in only.** With `RuntimeFeature.TrimmableTypeMap` unset, behavior is unchanged. NativeAOT still defaults to `managed`.
- No new user-facing / localized strings; error messages point at the generator when a proxy is genuinely missing.

## Status

All earlier prerequisites have **merged into `main`**, and this PR is **rebased on latest `main`**, so it is no longer stacked or blocked — it contains only the value/type-manager implementations (plus the supporting `JavaConvert` and generator changes) on top of them:

- #11799 — CoreCLR `JavaMarshal` split + Java.Interop bump
- #11753 array proxies · #11749 generic base · #11751 unresolvable peers · #11769 interface proxies
- #11794 multidex/manifest base · #11796 manifest parity · #11798 R8 keep

## Testing

- `TrimmableTypeMapTypeManagerTests` and `TypeMapModelBuilderTests` updated/extended for the new type resolution and the self-peer generator fix.
- Export tests enabled for the trimmable typemap; `TrimmableTypeMapUnsupported` cases excluded.
- NativeAOT warning-count and CoreCLR `apkdesc` baselines updated to match.
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 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