Skip to content

[TrimmableTypeMap] Fix interface-peer proxy generation for NativeAOT#11769

Draft
simonrozsival wants to merge 1 commit into
mainfrom
dev/simonrozsival/trimmable-interface-proxies
Draft

[TrimmableTypeMap] Fix interface-peer proxy generation for NativeAOT#11769
simonrozsival wants to merge 1 commit into
mainfrom
dev/simonrozsival/trimmable-interface-proxies

Conversation

@simonrozsival

Copy link
Copy Markdown
Member

Focused review subset of #11617

This is a small, focused PR carved out of #11617 (TrimmableTypeMap) to make review easier. It contains only the two interface-proxy fixes for the reflection-free NativeAOT typemap path. Its base branch is a snapshot of #11617 just before these two commits, so the diff shows only the interface-proxy changes (6 files).

#11617 remains the iteration branch; these ideas will land via small side PRs like this one.

What this fixes

Two distinct NativeAOT (ILC) failures caused by how generated proxies handle Java interface peers (e.g. binding listener interfaces from AndroidX / ZXing):

1. Interface-implementation methods use direct managed dispatch

Methods collected from an implemented Java interface (a listener Implementor) forwarded through the interface's private static n_* callback. That callback lives in the separately ILC-trimmed binding assembly and is unreferenced in the trimmable path, so ILC trims it and the generated proxy forwarder "will always throw."

Fix: dispatch directly to the managed method (mirroring exactly what the static n_* callback does — GetObject<TInterface> + callvirt), keeping the proxy self-contained.

Reproduced with Xamarin.AndroidX.Fragment (IOnBackStackChangedListener et al.): 8+ "will always throw" ILC warnings → 0 after the fix. Fixes the MergeLibraryManifest NativeAOT test.

2. Interface proxies derive from the non-generic JavaPeerProxy base

Generated interface proxies derived from JavaPeerProxy<TInterface>, whose type parameter is annotated [DynamicallyAccessedMembers(PublicConstructors | NonPublicConstructors)] and which builds new JavaPeerContainerFactory<T>(). Closing that generic over a constructor-less interface makes ILC fail to load the closed type:

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

which fails the entire NativeAOT build.

Fix: 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 ctor argument so runtime identity is unchanged. Instances are still created from the InvokerType; abstract classes keep the generic base since they have constructors.

Reproduced with ZXing.Net.Mobile (3.0.0-beta5 → transitively ApxLabs.FastAndroidCamera): NativeAOT build failed with the TypeLoadException before, builds successfully after. Fixes the RemovePermissionTest NativeAOT test.

Testing

  • All generator unit tests pass; added Generate_InterfaceProxyType_UsesNonGenericJavaPeerProxyBase and an interface-direct-dispatch assertion.
  • Verified locally that basic Mono.Android listeners (IOnClickListener/IOnLongClickListener) and AndroidX.Fragment still build clean (no regression on the working path).

@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.

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