Skip to content

[TrimmableTypeMap] Use non-generic JavaPeerProxy base for interface proxies (NativeAOT)#11769

Merged
jonathanpeppers merged 1 commit into
mainfrom
dev/simonrozsival/trimmable-interface-proxies
Jun 29, 2026
Merged

[TrimmableTypeMap] Use non-generic JavaPeerProxy base for interface proxies (NativeAOT)#11769
jonathanpeppers merged 1 commit into
mainfrom
dev/simonrozsival/trimmable-interface-proxies

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Jun 27, 2026

Copy link
Copy Markdown
Member

Problem

Under the trimmable typemap (NativeAOT), a generated proxy for a Java interface peer derived from the closed generic base JavaPeerProxy<TInterface>. That base annotates its type parameter with [DynamicallyAccessedMembers(PublicConstructors | NonPublicConstructors)] and constructs a JavaPeerContainerFactory<T> from it.

Closing that generic over an interface — which has no constructors — makes ILC fail to load the closed type, aborting the entire NativeAOT build:

Failed to load type 'Java.Interop.JavaPeerProxy`1<...INonMarshalingPreviewCallback>' from assembly 'Mono.Android'

Fix

Interface peers now derive from the non-generic JavaPeerProxy base — the same base already used for open generic definitions. The interface type is passed as the TargetType constructor argument, so the runtime TargetType identity is unchanged, and instances are still created from the InvokerType in CreateInstance, so behaviour is preserved. Concrete (constructible) class peers continue to use the closed generic JavaPeerProxy<T> base.

Concretely, the base-type selection is widened from proxy.IsGenericDefinition to proxy.IsGenericDefinition || proxy.IsInterface, with a new IsInterface flag plumbed through the proxy model.

Testing

  • Added Generate_InterfaceProxyType_UsesNonGenericJavaPeerProxyBase, asserting an interface proxy derives from the non-generic JavaPeerProxy base (a plain TypeReference, not a closed TypeSpecification).
  • Updated Generate_ProxyType_UsesGenericJavaPeerProxyBase comments to reflect that concrete types use the generic base while open generic definitions and interfaces use the non-generic base.
  • Verified with ZXing.Net.Mobile (3.0.0-beta5, transitively ApxLabs.FastAndroidCamera): the NativeAOT build failed to load JavaPeerProxy1<…INonMarshalingPreviewCallback> before this change and builds successfully after. Basic Mono.Android listeners (IOnClickListener/IOnLongClickListener) and AndroidX.Fragment` still build clean.
  • All TrimmableTypeMap generator unit tests pass.

@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-interface-proxies branch from 552e290 to d406bbc Compare June 27, 2026 14:06
@simonrozsival simonrozsival changed the title [TrimmableTypeMap] Fix interface-peer proxy generation for NativeAOT (subset of #11617) [TrimmableTypeMap] Fix interface-peer proxy generation for NativeAOT Jun 27, 2026
@simonrozsival simonrozsival changed the base branch from dev/simonrozsival/trimmable-interface-proxies-base to main June 27, 2026 15:23
@simonrozsival simonrozsival changed the base branch from main to dev/simonrozsival/trimmable-interface-proxies-base June 27, 2026 15:24
@simonrozsival

Copy link
Copy Markdown
Member Author

Known limitation of this fix: interface peers now derive from the non-generic JavaPeerProxy base, so JavaPeerProxy.GetContainerFactory() returns null for them and Java collections of interface element types (e.g. IList<ISomeListener>) can't be built via the AOT-safe path. This is inherent — JavaList<IInterface> can't be instantiated under ILC (DAM-Constructors over a ctor-less interface), the invoker can't substitute due to generic invariance, and the interface can't be reflection-constructed. Tracked as a follow-up in #11770 (two-type-parameter JavaList<TInterface, TInvoker>).

…roxies

The generated proxy for an interface peer (e.g. a binding listener interface
like ApxLabs.FastAndroidCamera.INonMarshalingPreviewCallback) derived from the
closed generic JavaPeerProxy<TInterface>. That base annotates its type parameter
with [DynamicallyAccessedMembers(PublicConstructors | NonPublicConstructors)] and
returns new JavaPeerContainerFactory<T>() from GetContainerFactory(). Closing the
generic over an interface -- which has no constructors -- makes ILC fail to load
the closed type ("Failed to load type JavaPeerProxy1<...INonMarshalingPreviewCallback>
from assembly Mono.Android"), which fails the whole NativeAOT build
(ManifestTest.RemovePermissionTest, which pulls in ZXing.Net.Mobile ->
ApxLabs.FastAndroidCamera).

Interface peers now derive from the non-generic JavaPeerProxy base (the same base
already used for open generic definitions), passing the interface as the TargetType
constructor argument so runtime TargetType identity is unchanged. Instances are
still created from the InvokerType in CreateInstance, so behaviour is preserved;
abstract classes keep the generic base since they have constructors.

Reproduced locally with ZXing.Net.Mobile (3.0.0-beta5): NativeAOT build failed with
the TypeLoadException before, builds successfully after. Basic Mono.Android listener
apps (IOnClickListener/IOnLongClickListener) and AndroidX.Fragment still build clean.
Fixes RemovePermissionTest.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-interface-proxies branch from 3080560 to da1d643 Compare June 27, 2026 16:34
@simonrozsival

Copy link
Copy Markdown
Member Author

Dropped the Direct-dispatch interface-implementation proxy methods commit from this PR: it caused infinite recursion / stack overflow at runtime for interface listener callbacks (e.g. ViewTreeObserver.GlobalLayout), because the generated UCO resolved the peer as the Invoker (which forwards back to Java). This PR now contains only the non-generic JavaPeerProxy base fix (which fixes the TypeLoadException and is correct).

The remaining "will always throw" gap for binding/AndroidX interface listeners — the original motivation for the reverted direct-dispatch — is tracked in #11773 for a correct fix.

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 simonrozsival changed the title [TrimmableTypeMap] Fix interface-peer proxy generation for NativeAOT [TrimmableTypeMap] Use non-generic JavaPeerProxy base for interface proxies (NativeAOT) Jun 29, 2026
@simonrozsival simonrozsival marked this pull request as ready for review June 29, 2026 13:34
Copilot AI review requested due to automatic review settings June 29, 2026 13:34

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 fixes a NativeAOT (trimmable typemap) build failure where generated proxies for Java interface peers incorrectly derived from the closed generic JavaPeerProxy<T>, which triggers an ILC type-load failure when T is an interface (no constructors). It updates the generator model and emitter so interface proxies derive from the non-generic JavaPeerProxy base instead, preserving runtime behavior by continuing to activate instances via InvokerType.

Changes:

  • Extend proxy base-type selection to use the non-generic JavaPeerProxy for interface peers (in addition to open generic definitions).
  • Plumb an IsInterface flag through the proxy model (JavaPeerInfoJavaPeerProxyData) and use it in emission decisions.
  • Add/adjust generator unit tests to assert interface proxies use a non-generic base type handle (TypeReference) while concrete types still use the closed generic base (TypeSpecification).
Show a summary per file
File Description
tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs Adds a regression test ensuring interface proxies derive from non-generic JavaPeerProxy.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs Updates proxy base-type selection logic to use non-generic base for interfaces and open generic definitions.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs Plumbs peer.IsInterface into the generated proxy model.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs Extends JavaPeerProxyData with an IsInterface flag and documentation explaining the ILC failure scenario.

Copilot's findings

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

@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 29, 2026
@jonathanpeppers jonathanpeppers merged commit 1768282 into main Jun 29, 2026
41 checks passed
@jonathanpeppers jonathanpeppers deleted the dev/simonrozsival/trimmable-interface-proxies branch June 29, 2026 15:59
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 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