Skip to content

Commit 0ef5aab

Browse files
[TrimmableTypeMap] Direct-dispatch interface-implementation proxy methods
Marshal methods collected from an implemented Java interface (e.g. a listener Implementor) declare their n_* callback as a *private static* method on the interface type, which lives in the separately ILC-trimmed binding assembly. Nothing in the trimmable path references that callback within its own assembly, so ILC trims it away and the generated proxy forwarder 'will always throw' (or, for generic JavaPeerProxy<TInterface> closed over a bare interface, fails to load with a TypeLoadException). Dispatch these methods directly to the managed method instead -- this mirrors exactly what the static n_* callback does internally (GetObject<TInterface> + callvirt the interface method) but keeps the generated proxy self-contained and independent of whether the binding's private n_* survives trimming. Reproduced with Xamarin.AndroidX.Fragment (IOnBackStackChangedListener et al.): the ILC 'will always throw' warnings are gone and built-in Mono.Android listeners (Button.Click/LongClick) still build clean. Fixes the MergeLibraryManifest and RemovePermissionTest NativeAOT failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5e9346d commit 0ef5aab

2 files changed

Lines changed: 27 additions & 2 deletions

File tree

src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,7 +1117,7 @@ void AddMarshalMethod (List<MarshalMethodInfo> methods, RegisterInfo registerInf
11171117
string declaringAssemblyName = "";
11181118
ParseConnectorDeclaringType (registerInfo.Connector, out declaringTypeName, out declaringAssemblyName);
11191119

1120-
bool mayCallManagedMethodDirectly = ShouldCallManagedMethodDirectly (isConstructor, isExport, declaringTypeName);
1120+
bool mayCallManagedMethodDirectly = ShouldCallManagedMethodDirectly (isConstructor, isExport, declaringTypeName, isInterfaceImplementation);
11211121

11221122
// Only decode TypeRefData signatures for methods that need direct dispatch IL
11231123
// generation; static n_* callback forwarders already encode from the JNI signature.
@@ -1160,7 +1160,7 @@ void AddMarshalMethod (List<MarshalMethodInfo> methods, RegisterInfo registerInf
11601160
});
11611161
}
11621162

1163-
static bool ShouldCallManagedMethodDirectly (bool isConstructor, bool isExport, string declaringTypeName)
1163+
static bool ShouldCallManagedMethodDirectly (bool isConstructor, bool isExport, string declaringTypeName, bool isInterfaceImplementation)
11641164
{
11651165
if (isExport) {
11661166
return true;
@@ -1170,6 +1170,17 @@ static bool ShouldCallManagedMethodDirectly (bool isConstructor, bool isExport,
11701170
return false;
11711171
}
11721172

1173+
// Methods collected from an implemented Java interface (e.g. a listener Implementor)
1174+
// declare their n_* callback as a *private static* method on the interface type, which
1175+
// lives in the (separately ILC-trimmed) binding assembly. Nothing in the trimmable path
1176+
// references that callback within its own assembly, so ILC trims it and the generated
1177+
// proxy's forwarder "will always throw" (or fails to load). Dispatch directly to the
1178+
// managed method instead — this mirrors exactly what the static n_* callback does
1179+
// (GetObject<TInterface> + callvirt the interface method) but keeps the proxy self-contained.
1180+
if (isInterfaceImplementation) {
1181+
return true;
1182+
}
1183+
11731184
// Direct [Register] methods have no connector-declared callback owner, so forwarding
11741185
// through n_* may bind to an inherited callback. If the type hides a base virtual
11751186
// member with "new virtual" but keeps the same JNI method, that inherited callback

tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/InterfaceMethodDetectionTests.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@ public void ImplicitInterfaceImpl_DetectsOnClickWithCorrectSignatureAndConnector
2121
Assert.Equal ("Android.Views.IOnClickListenerInvoker", onClick.DeclaringTypeName);
2222
}
2323

24+
[Fact]
25+
public void ImplicitInterfaceImpl_UsesDirectManagedDispatch ()
26+
{
27+
// Interface-implementation marshal methods must dispatch directly to the managed
28+
// method rather than forwarding through the interface's private static n_* callback.
29+
// That callback lives in the (separately ILC-trimmed) binding assembly and is trimmed
30+
// away in the trimmable path, which otherwise makes the generated proxy forwarder
31+
// "will always throw" (or fail to load). See JavaPeerScanner.ShouldCallManagedMethodDirectly.
32+
var peer = FindFixtureByJavaName ("my/app/ImplicitClickListener");
33+
var onClick = peer.MarshalMethods.First (m => m.JniName == "onClick");
34+
Assert.True (onClick.IsInterfaceImplementation);
35+
Assert.True (onClick.CallManagedMethodDirectly);
36+
}
37+
2438
[Fact]
2539
public void ImplicitMultiInterface_BothMethodsDetected ()
2640
{

0 commit comments

Comments
 (0)