Skip to content

Commit 23ec9dc

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 06e0cc3 commit 23ec9dc

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
@@ -1377,7 +1377,7 @@ void AddMarshalMethod (List<MarshalMethodInfo> methods, RegisterInfo registerInf
13771377
string declaringAssemblyName = "";
13781378
ParseConnectorDeclaringType (registerInfo.Connector, out declaringTypeName, out declaringAssemblyName);
13791379

1380-
bool mayCallManagedMethodDirectly = ShouldCallManagedMethodDirectly (isConstructor, isExport, declaringTypeName);
1380+
bool mayCallManagedMethodDirectly = ShouldCallManagedMethodDirectly (isConstructor, isExport, declaringTypeName, isInterfaceImplementation);
13811381

13821382
// Only decode TypeRefData signatures for methods that need direct dispatch IL
13831383
// generation; static n_* callback forwarders already encode from the JNI signature.
@@ -1420,7 +1420,7 @@ void AddMarshalMethod (List<MarshalMethodInfo> methods, RegisterInfo registerInf
14201420
});
14211421
}
14221422

1423-
static bool ShouldCallManagedMethodDirectly (bool isConstructor, bool isExport, string declaringTypeName)
1423+
static bool ShouldCallManagedMethodDirectly (bool isConstructor, bool isExport, string declaringTypeName, bool isInterfaceImplementation)
14241424
{
14251425
if (isExport) {
14261426
return true;
@@ -1430,6 +1430,17 @@ static bool ShouldCallManagedMethodDirectly (bool isConstructor, bool isExport,
14301430
return false;
14311431
}
14321432

1433+
// Methods collected from an implemented Java interface (e.g. a listener Implementor)
1434+
// declare their n_* callback as a *private static* method on the interface type, which
1435+
// lives in the (separately ILC-trimmed) binding assembly. Nothing in the trimmable path
1436+
// references that callback within its own assembly, so ILC trims it and the generated
1437+
// proxy's forwarder "will always throw" (or fails to load). Dispatch directly to the
1438+
// managed method instead — this mirrors exactly what the static n_* callback does
1439+
// (GetObject<TInterface> + callvirt the interface method) but keeps the proxy self-contained.
1440+
if (isInterfaceImplementation) {
1441+
return true;
1442+
}
1443+
14331444
// Direct [Register] methods have no connector-declared callback owner, so forwarding
14341445
// through n_* may bind to an inherited callback. If the type hides a base virtual
14351446
// 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)