Summary
In the trimmable typemap (NativeAOT), Java interface peer types (e.g. listener Implementors) declare their n_* native callback as a private static method on the interface type. For interfaces that live in a binding/AndroidX assembly, ILC trims that callback (nothing references it within its own assembly in the trimmable path), so the generated proxy forwarder "will always throw":
ILC: Method '..._IOnBackStackChangedListenerImplementor_Proxy.n_onBackStackChangeStarted_uco_*' will always throw
because: Missing method 'Void IOnBackStackChangedListener.n_OnBackStackChangeStarted_...'
This means interface listener callbacks from binding/AndroidX libraries don't work under the trimmable typemap.
What we tried (and reverted)
PR #11769 originally switched interface-implementation marshal methods to direct managed dispatch (ShouldCallManagedMethodDirectly returned true for isInterfaceImplementation), so the generated UCO would do GetObject<TInterface>(handle).Method(...) itself instead of forwarding to the (trimmed) static n_*.
This caused a runtime regression: infinite recursion / stack overflow for all interface listeners, including built-in Mono.Android ones (ViewTreeObserver.GlobalLayout, etc.). Reproduced on an arm64 emulator with a minimal app:
button.ViewTreeObserver.GlobalLayout += (s, e) =>
Android.Util.Log.Debug("BugzillaTests", "Bug 29730: GlobalLayout event handler called!");
→ Fatal signal 11 (SIGSEGV) ... Cause: stack pointer is not in a rw map; likely due to stack overflow. The callback log never appears. Disabling the direct-dispatch change makes the handler fire correctly.
The direct-dispatch UCO resolved the peer as the Invoker (GetObject(handle, typeof(TInterface)) creates the invoker when the user's Implementor peer isn't returned), and the invoker forwards the call back to Java → native → UCO → invoker → … until the stack overflows. So direct dispatch is strictly worse than the original static-n_* forwarding, which dispatches correctly for Mono.Android interfaces.
The direct-dispatch change has been reverted (the non-generic JavaPeerProxy base fix for interface proxies in #11769 is kept — that one is correct and fixes a separate TypeLoadException). This unblocks GlobalLayoutEvent_ShouldRegisterAndFire_OnActivityLaunch and avoids breaking every interface listener.
What still needs solving (this issue)
Binding/AndroidX interface listener callbacks still don't work under the trimmable typemap because the interface's private static n_* callback is trimmed → "will always throw". We need a correct fix that does not reintroduce the recursion, e.g.:
- Preserve / root the interface
n_* callbacks so ILC keeps them (the forwarder then resolves), rather than bypassing them with direct dispatch; or
- A direct-dispatch variant that provably resolves the user Implementor peer (never the Invoker) — i.e. understand why
GetObject returns the invoker here and ensure the UCO dispatches to the already-registered managed peer; or
- Generate the JCW/registration so the binding's
n_* is reachable in the trimmable assembly.
Acceptance criteria
References
Summary
In the trimmable typemap (NativeAOT), Java interface peer types (e.g. listener
Implementors) declare theirn_*native callback as aprivate staticmethod on the interface type. For interfaces that live in a binding/AndroidX assembly, ILC trims that callback (nothing references it within its own assembly in the trimmable path), so the generated proxy forwarder "will always throw":This means interface listener callbacks from binding/AndroidX libraries don't work under the trimmable typemap.
What we tried (and reverted)
PR #11769 originally switched interface-implementation marshal methods to direct managed dispatch (
ShouldCallManagedMethodDirectlyreturnedtrueforisInterfaceImplementation), so the generated UCO would doGetObject<TInterface>(handle).Method(...)itself instead of forwarding to the (trimmed) staticn_*.This caused a runtime regression: infinite recursion / stack overflow for all interface listeners, including built-in Mono.Android ones (
ViewTreeObserver.GlobalLayout, etc.). Reproduced on an arm64 emulator with a minimal app:→
Fatal signal 11 (SIGSEGV) ... Cause: stack pointer is not in a rw map; likely due to stack overflow.The callback log never appears. Disabling the direct-dispatch change makes the handler fire correctly.The direct-dispatch UCO resolved the peer as the Invoker (
GetObject(handle, typeof(TInterface))creates the invoker when the user'sImplementorpeer isn't returned), and the invoker forwards the call back to Java → native → UCO → invoker → … until the stack overflows. So direct dispatch is strictly worse than the original static-n_*forwarding, which dispatches correctly for Mono.Android interfaces.The direct-dispatch change has been reverted (the non-generic
JavaPeerProxybase fix for interface proxies in #11769 is kept — that one is correct and fixes a separateTypeLoadException). This unblocksGlobalLayoutEvent_ShouldRegisterAndFire_OnActivityLaunchand avoids breaking every interface listener.What still needs solving (this issue)
Binding/AndroidX interface listener callbacks still don't work under the trimmable typemap because the interface's
private static n_*callback is trimmed → "will always throw". We need a correct fix that does not reintroduce the recursion, e.g.:n_*callbacks so ILC keeps them (the forwarder then resolves), rather than bypassing them with direct dispatch; orGetObjectreturns the invoker here and ensure the UCO dispatches to the already-registered managed peer; orn_*is reachable in the trimmable assembly.Acceptance criteria
IOnBackStackChangedListener), with a device test.GlobalLayoutEvent_ShouldRegisterAndFire_OnActivityLaunchstays green).References
GlobalLayoutEvent_ShouldRegisterAndFire_OnActivityLaunch(NativeAOT),tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs.src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs(ShouldCallManagedMethodDirectly),Generator/ExportMethodDispatchEmitter.cs.