Skip to content
This repository was archived by the owner on Jul 6, 2026. It is now read-only.

Commit 1adb796

Browse files
authored
[generator] generator --lang-features=emit-legacy-interface-invokers (#1145)
Fixes: #910 Context: bc5bcf4 Context: https://github.com/xamarin/java.interop/issues/858 Consider the Java `java.lang.Runnable` interface: package java.lang; public interface Runnable { void run (); } This is bound as: package Java.Lang; public interface IRunnable : IJavaPeerable { void Run (); } with some slight differences depending on whether we're dealing with .NET Android (`generator --codegen-target=xajavainterop1`) or `src/Java.Base` (`generator --codegen-target=javainterop1`). Now, assume a Java API + corresponding binding which returns a `Runnable` instance: package example; public class Whatever { public static Runnable createRunnable(); } You can invoke `IRunnable.Run()` on the return value: IRunnable r = Whatever.CreateRunnable(); r.Run(); but how does that work? This works via an "interface Invoker", which is a class emitted by `generator` which implements the interface and invokes the interface methods through JNI: internal partial class IRunnableInvoker : Java.Lang.Object, IRunnable { public void Run() => … } Once Upon A Time™, the interface invoker implementation mirrored that of classes: a static `IntPtr` field held the `jmethodID` value, which would be looked up on first-use and cached for subsequent invocations: partial class IRunnableInvoker { static IntPtr id_run; public unsafe void Run() { if (id_run == IntPtr.Zero) id_run = JNIEnv.GetMethodID (class_ref, "run", "()V"); JNIEnv.CallVoidMethod (Handle, id_run, …); } } This approach works until you have interface inheritance and methods which come from inherited interfaces: package android.view; public /* partial */ interface ViewManager { void addView(View view, ViewGroup.LayoutParams params); } public /* partial */ interface WindowManager extends ViewManager { void removeViewImmediate(View view); } This would be bound as: namespace Android.Views; public partial interface IViewManager : IJavaPeerable { void AddView (View view, ViewGroup.LayoutParams @params); } public partial IWindowManager : IViewManager { void RemoveViewImmediate (View view); } internal partial class IWindowManagerInvoker : Java.Lang.Object, IWindowManager { static IntPtr id_addView; public void AddView(View view, ViewGroup.LayoutParams @params) { if (id_addView == IntPtr.Zero) id_run = JNIEnv.GetMethodID (class_ref, "addView", "…"); JNIEnv.CallVoidMethod (Handle, id_addView, …); } } Unfortunately, *invoking* `IViewManager.AddView()` through an `IWindowManagerInvoker` would crash! D/dalvikvm( 6645): GetMethodID: method not found: Landroid/view/WindowManager;.addView:(Landroid/view/View;Landroid/view/ViewGroup$LayoutParams;)V I/MonoDroid( 6645): UNHANDLED EXCEPTION: Java.Lang.NoSuchMethodError: Exception of type 'Java.Lang.NoSuchMethodError' was thrown. I/MonoDroid( 6645): at Android.Runtime.JNIEnv.GetMethodID (intptr,string,string) I/MonoDroid( 6645): at Android.Views.IWindowManagerInvoker.AddView (Android.Views.View,Android.Views.ViewGroup/LayoutParams) I/MonoDroid( 6645): at Mono.Samples.Hello.HelloActivity.OnCreate (Android.OS.Bundle) I/MonoDroid( 6645): at Android.App.Activity.n_OnCreate_Landroid_os_Bundle_ (intptr,intptr,intptr) I/MonoDroid( 6645): at (wrapper dynamic-method) object.ecadbe0b-9124-445e-a498-f351075f6c89 (intptr,intptr,intptr) Interfaces are not classes, and this is one of the places that this is most apparent. Because of this crash, we had to use *instance* `jmethodID` caches: internal partial class IWindowManagerInvoker : Java.Lang.Object, IWindowManager { IntPtr id_addView; public void AddView(View view, ViewGroup.LayoutParams @params) { if (id_addView == IntPtr.Zero) id_run = JNIEnv.GetMethodID (class_ref, "addView", "…"); JNIEnv.CallVoidMethod (Handle, id_addView, …); } } Pro: no more crash! Con: *every different instance* of `IWindowManagerInvoker` needs to separately lookup whatever methods are invoked. There is *some* caching, so repeated calls to `AddView()` on the same instance will hit the cache, but if you obtain a different `IWindowManager` instance, `jmethodID` values will need to be looked up again. This was "fine", until xamarin/java.interop#858 enters the picture: interface invokers were full of Android-isms -- `Android.Runtime.JNIEnv.GetMethodID()`! `JNIEnv.CallVoidMethod()`! -- and thus ***not*** APIs that @jonpryor wished to expose within desktop Java.Base bindings. Enter `generator --lang-features=emit-legacy-interface-invokers`: when *not* specified, interface invokers will now use `JniPeerMembers` for method lookup and invocation, allowing `jmethodID` values to be cached *across* instances. In order to prevent the runtime crash, an interface may have *multiple* `JniPeerMembers` values, one per implemented interface, which is used to invoke methods from that interface. `IWindowManagerInvoker` now becomes: internal partial class IWindowManagerInvoker : Java.Lang.Object, IWindowManager { static readonly JniPeerMembers _members_android_view_ViewManager = …; static readonly JniPeerMembers _members_android_view_WindowManager = …; public void AddView(View view, ViewGroup.LayoutParams @params) { const string __id = "addView.…"; _members_android_view_ViewManager.InstanceMethods.InvokeAbstractVoidMethod (__id, this, …); } public void RemoveViewImmediate(View view) { const string __id = "removeViewImmediate.…"; _members_android_view_WindowManager.InstanceMethods.InvokeAbstractVoidMethod (__id, this, …); } } This has two advantages: 1. More caching! 2. Desktop `Java.Base` binding can now have interface invokers. Update `tests/generator-Tests` expected output. Note: to keep this patch smaller, JavaInterop1 output uses the new pattern, and only *some* XAJavaInterop1 tests use the new pattern. Added [CS0114][0] to `$(NoWarn)` in `Java.Base.csproj` to ignore warnings such as: …/src/Java.Base/obj/Debug-net7.0/mcw/Java.Lang.ICharSequence.cs(195,25): warning CS0114: 'ICharSequenceInvoker.ToString()' hides inherited member 'Object.ToString()'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword. [Ignoring CS0114 is also done in `Mono.Android.dll` as well][1], so this is not a new or unique requirement. Update `Java.Interop.dll` so that `JniRuntime.JniValueManager.GetActivationConstructor()` now knows about and looks for `*Invoker` types, then uses the activation constructor from the `*Invoker` type when the source type is an abstract `class` or `interface`. Update `tests/Java.Base-Tests` to test for implicit `*Invoker` lookup and invocation support. ~~ Property Setters ~~ While testing on dotnet/android#8339, we hit this error (among others, to be addressed later): src/Mono.Android/obj/Debug/net8.0/android-34/mcw/Android.Views.IWindowInsetsController.cs(304,41): error CS0103: The name 'behavior' does not exist in the current context This was caused because of code such as: public partial interface IWindowInsetsController { public unsafe int SystemBarsBehavior { get { const string __id = "getSystemBarsBehavior.()I"; try { var __rm = _members_IWindowInsetsController.InstanceMethods.InvokeAbstractInt32Method (__id, this, null); return __rm; } finally { } } set { const string __id = "setSystemBarsBehavior.(I)V"; try { JniArgumentValue* __args = stackalloc JniArgumentValue [1]; __args [0] = new JniArgumentValue (behavior); _members_IWindowInsetsController.InstanceMethods.InvokeAbstractVoidMethod (__id, this, __args); } finally { } } } } This happened because when emitting the property setter, we need to update the `set*` method's parameter name to be `value` so that the normal property setter body is emitted properly. Update `InterfaceInvokerProperty.cs` so that the parameter name is set to `value`. ~~ Performance ~~ What does this do for performance? Add a new `InterfaceInvokerTiming` test fixture to `Java.Interop-PerformanceTests.dll`, which: 1. "Reimplements" the "legacy" and "JniPeerMembers" Invoker strategies 2. For each Invoker strategy: a. Invokes a Java method which returns a `java.lang.Runnable` instance b. Invokes `Runnable.run()` on the instance returned by (2.a) …100 times. c. Repeat (2.a) and (2.b) 100 times. The result is that using `JniPeerMembers` is *much* faster: % dotnet build tests/Java.Interop-PerformanceTests/*.csproj && \ dotnet test --logger "console;verbosity=detailed" bin/TestDebug-net7.0/Java.Interop-PerformanceTests.dll --filter "Name~InterfaceInvokerTiming" … Passed InterfaceInvokers [1 s] Standard Output Messages: ## InterfaceInvokers Timing: instanceIds: 00:00:01.1095502 ## InterfaceInvokers Timing: peerMembers: 00:00:00.1400427 Using `JniPeerMembers` takes ~1/8th the time as using `jmethodID`s. TODO: something is *probably* wrong with my test -- reviews welcome! -- as when I increase the (2.b) iteration count, the `peerMembers` time is largely unchanged (~0.14s), while the `instanceIds` time increases linearly. *Something* is wrong there. I'm not sure what. (Or *nothing* is wrong, and instance `jmethodID` are just *that* bad.) [0]: https://learn.microsoft.com/en-us/dotnet/csharp/misc/cs0114 [1]: https://github.com/xamarin/xamarin-android/blob/d5c4ec09f7658428a10bbe49c8a7a3eb2f71cb86/src/Mono.Android/Mono.Android.csproj#L12C7-L12C7
1 parent 6bd7ae4 commit 1adb796

78 files changed

Lines changed: 1835 additions & 825 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎build-tools/automation/templates/core-tests.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ steps:
132132
inputs:
133133
command: test
134134
testRunTitle: Java.Interop-Performance ($(DotNetTargetFramework) - ${{ parameters.platformName }})
135-
arguments: bin/Test$(Build.Configuration)$(NetCoreTargetFrameworkPathSuffix)/Java.Interop-PerformanceTests.dll
135+
arguments: --logger "console;verbosity=detailed" bin/Test$(Build.Configuration)$(NetCoreTargetFrameworkPathSuffix)/Java.Interop-PerformanceTests.dll
136136
continueOnError: true
137137
retryCountOnTaskFailure: 1
138138

‎src/Java.Base/Java.Base.csproj‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<TargetFramework>$(DotNetTargetFramework)</TargetFramework>
55
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
66
<Nullable>enable</Nullable>
7-
<NoWarn>$(NoWarn);8764</NoWarn>
7+
<NoWarn>$(NoWarn);8764;0114</NoWarn>
88
</PropertyGroup>
99

1010
<ImportProject="..\..\TargetFrameworkDependentValues.props" />

‎src/Java.Base/Java.Lang/ICharSequence.cs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
usingSystem.Collections;
2+
13
namespaceJava.Lang{
24

5+
partialclassICharSequenceInvoker:IEnumerable{
6+
}
7+
38
publicstaticpartialclassICharSequenceExtensions{
49

510
publicstaticICharSequence[]?ToCharSequenceArray(thisstring?[]?values)

‎src/Java.Base/Transforms/Metadata.xml‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
<metadata>
2-
<!-- For now, just bind java.lang.* -->
3-
<remove-nodepath="//api/package[not(starts-with(@name, 'java.lang')
4-
or starts-with(@name, 'java.io')
2+
<!-- For now, just bind a few packages -->
3+
<remove-nodepath="//api/package[
4+
not(
5+
starts-with(@name, 'java.lang')
6+
or starts-with(@name, 'java.io')
7+
or starts-with(@name, 'java.util.function')
58
)]" />
69

710
<!-- Type / Namespace conflicts -->
@@ -54,6 +57,9 @@
5457
]/method[@name='write']"
5558
name="explicitInterface">IDataOutput</attr>
5659

60+
<!-- CS0108 but for *static* members; TODO: how do we fix? -->
61+
<remove-nodepath="/api/package[@name='java.util.function']/interface[@name='UnaryOperator']/method[@name='identity' and count(parameter)=0]" />
62+
5763
<!-- AbstractStringBuilder is package-private; fixity fix -->
5864
<remove-nodepath="//api/package[@name='java.lang']/class[@name='AbstractStringBuilder']" />
5965

‎src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs‎

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -335,14 +335,33 @@ static Type GetPeerType (Type type)
335335

336336
staticConstructorInfo?GetActivationConstructor(Typetype)
337337
{
338-
return
339-
(fromcintype.GetConstructors(BindingFlags.Public|BindingFlags.NonPublic|BindingFlags.Instance)
340-
letp=c.GetParameters()
341-
wherep.Length==2&&p[0].ParameterType==ByRefJniObjectReference&&p[1].ParameterType==typeof(JniObjectReferenceOptions)
342-
selectc)
343-
.FirstOrDefault();
338+
if(type.IsAbstract||type.IsInterface){
339+
type=GetInvokerType(type)??type;
340+
}
341+
foreach(varcintype.GetConstructors(BindingFlags.Public|BindingFlags.NonPublic|BindingFlags.Instance)){
342+
varp=c.GetParameters();
343+
if(p.Length==2&&p[0].ParameterType==ByRefJniObjectReference&&p[1].ParameterType==typeof(JniObjectReferenceOptions))
344+
returnc;
345+
}
346+
returnnull;
344347
}
345348

349+
staticType?GetInvokerType(Typetype)
350+
{
351+
conststringsuffix="Invoker";
352+
Type[]arguments=type.GetGenericArguments();
353+
if(arguments.Length==0)
354+
returntype.Assembly.GetType(type+suffix);
355+
Typedefinition=type.GetGenericTypeDefinition();
356+
intbt=definition.FullName!.IndexOf("`",StringComparison.Ordinal);
357+
if(bt==-1)
358+
thrownewNotSupportedException("Generic type doesn't follow generic type naming convention! "+type.FullName);
359+
Type?suffixDefinition=definition.Assembly.GetType(
360+
definition.FullName.Substring(0,bt)+suffix+definition.FullName.Substring(bt));
361+
if(suffixDefinition==null)
362+
returnnull;
363+
returnsuffixDefinition.MakeGenericType(arguments);
364+
}
346365

347366
publicobject?CreateValue(refJniObjectReferencereference,JniObjectReferenceOptionsoptions,Type?targetType=null)
348367
{

‎tests/Java.Base-Tests/Java.Base/JavaToManagedTests.cs‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ public void InterfaceMethod ()
1818
Assert.IsTrue(invoked);
1919
r.Dispose();
2020
}
21+
22+
[Test]
23+
publicvoidInterfaceInvokerMethod()
24+
{
25+
intvalue=0;
26+
usingvarc=newMyIntConsumer(v =>value=v);
27+
usingvarr=JavaInvoker.CreateRunnable(c);
28+
r?.Run();
29+
Assert.AreEqual(0,value);
30+
r?.Run();
31+
Assert.AreEqual(1,value);
32+
}
2133
}
2234

2335
classJavaInvoker:JavaObject{
@@ -31,6 +43,14 @@ public static unsafe void Run (Java.Lang.IRunnable r)
3143
args[0]=newJniArgumentValue(r);
3244
_members.StaticMethods.InvokeVoidMethod("run.(Ljava/lang/Runnable;)V",args);
3345
}
46+
47+
publicstaticunsafeJava.Lang.IRunnable?CreateRunnable(Java.Util.Function.IIntConsumerc)
48+
{
49+
JniArgumentValue*args=stackallocJniArgumentValue[1];
50+
args[0]=newJniArgumentValue(c);
51+
var_rm=_members.StaticMethods.InvokeObjectMethod("createRunnable.(Ljava/util/function/IntConsumer;)Ljava/lang/Runnable;",args);
52+
returnJava.Interop.JniEnvironment.Runtime.ValueManager.GetValue<Java.Lang.IRunnable>(ref_rm,JniObjectReferenceOptions.CopyAndDispose);
53+
}
3454
}
3555

3656
[JniTypeSignature("example/MyRunnable")]
@@ -48,4 +68,20 @@ public void Run ()
4868
action();
4969
}
5070
}
71+
72+
[JniTypeSignature("example/MyIntConsumer")]
73+
classMyIntConsumer:Java.Lang.Object,Java.Util.Function.IIntConsumer{
74+
75+
Action<int>action;
76+
77+
publicMyIntConsumer(Action<int>action)
78+
{
79+
this.action=action;
80+
}
81+
82+
publicvoidAccept(intvalue)
83+
{
84+
action(value);
85+
}
86+
}
5187
}
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
packagecom.microsoft.java_base_tests;
22

3-
publicclassInvoker {
3+
importjava.util.function.IntConsumer;
4+
5+
publicfinalclassInvoker {
46

57
publicstaticvoidrun(Runnabler) {
68
r.run();
79
}
10+
11+
publicstaticRunnablecreateRunnable(finalIntConsumerconsumer) {
12+
returnnewRunnable() {
13+
intvalue;
14+
publicvoidrun() {
15+
consumer.accept(value++);
16+
}
17+
};
18+
}
819
}

‎tests/Java.Interop-PerformanceTests/Java.Interop/JavaTiming.cs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,11 @@ public unsafe JniObjectReference Timing_ToString_JniPeerMembers ()
276276
conststringid=toString_name+"."+toString_sig;
277277
return_members.InstanceMethods.InvokeVirtualObjectMethod(id,this,null);
278278
}
279+
280+
publicstaticunsafeJniObjectReferenceCreateRunnable()
281+
{
282+
return_members.StaticMethods.InvokeObjectMethod("CreateRunnable.()Ljava/lang/Runnable;",null);
283+
}
279284
}
280285

281286
[JniTypeSignature(JniTypeName)]

‎tests/Java.Interop-PerformanceTests/Java.Interop/TimingTests.cs‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,41 @@ public void GenericMarshalingOverhead_Int32ArrayArrayArray ()
776776
total.Stop();
777777
Console.WriteLine("## {0} Timing: {1}",nameof(GenericMarshalingOverhead_Int32ArrayArrayArray),total.Elapsed);
778778
}
779+
780+
}
781+
782+
[TestFixture]
783+
publicclassInterfaceInvokerTiming:Java.InteropTests.JavaVMFixture{
784+
785+
[Test]
786+
publicvoidInterfaceInvokers()
787+
{
788+
constintJavaTiming_CreateRunnable_Invocations=100;
789+
constintRunnable_Run_Invocations=100;
790+
791+
varinstanceIds=Stopwatch.StartNew();
792+
for(inti=0;i<JavaTiming_CreateRunnable_Invocations;++i){
793+
varc=JavaTiming.CreateRunnable();
794+
IMyRunnabler=newLegacyRunnableInvoker(refc,JniObjectReferenceOptions.CopyAndDispose);
795+
for(intj=0;j<Runnable_Run_Invocations;++j){
796+
r.Run();
797+
}
798+
r.Dispose();
799+
}
800+
instanceIds.Stop();
801+
varpeerMembers=Stopwatch.StartNew();
802+
for(inti=0;i<JavaTiming_CreateRunnable_Invocations;++i){
803+
varc=JavaTiming.CreateRunnable();
804+
IMyRunnabler=newJniPeerMembersRunnableInvoker(refc,JniObjectReferenceOptions.CopyAndDispose);
805+
for(intj=0;j<Runnable_Run_Invocations;++j){
806+
r.Run();
807+
}
808+
r.Dispose();
809+
}
810+
peerMembers.Stop();
811+
Console.WriteLine("## {0} Timing: instanceIds: {1}",nameof(InterfaceInvokers),instanceIds.Elapsed);
812+
Console.WriteLine("## {0} Timing: peerMembers: {1}",nameof(InterfaceInvokers),peerMembers.Elapsed);
813+
}
779814
}
780815

781816
classManagedTiming{
@@ -893,5 +928,60 @@ public override object GetValue ()
893928
returnnull;
894929
}
895930
}
931+
932+
interfaceIMyRunnable:IJavaPeerable{
933+
voidRun();
934+
}
935+
936+
classLegacyRunnableInvoker:JavaObject,IMyRunnable{
937+
staticreadonlyJniPeerMembers_members=newJniPeerMembers("java/lang/Runnable",typeof(LegacyRunnableInvoker));
938+
JniObjectReferenceclass_ref;
939+
940+
publicLegacyRunnableInvoker(refJniObjectReferencereference,JniObjectReferenceOptionsoptions)
941+
:base(refreference,options)
942+
{
943+
varr=JniEnvironment.Types.GetObjectClass(PeerReference);
944+
class_ref=r.NewGlobalRef();
945+
JniObjectReference.Dispose(refr);
946+
}
947+
948+
publicoverrideJniPeerMembersJniPeerMembers{
949+
get{return_members;}
950+
}
951+
952+
protectedoverridevoidDispose(booldisposing)
953+
{
954+
JniObjectReference.Dispose(refclass_ref);
955+
base.Dispose(disposing);
956+
}
957+
958+
JniMethodInfoid_run;
959+
960+
publicunsafevoidRun()
961+
{
962+
if(id_run==null){
963+
id_run=JniEnvironment.InstanceMethods.GetMethodID(class_ref,"run","()V");
964+
}
965+
JniEnvironment.InstanceMethods.CallObjectMethod(PeerReference,id_run);
966+
}
967+
}
968+
969+
classJniPeerMembersRunnableInvoker:JavaObject,IMyRunnable{
970+
publicJniPeerMembersRunnableInvoker(refJniObjectReferencereference,JniObjectReferenceOptionsoptions)
971+
:base(refreference,options)
972+
{
973+
}
974+
975+
staticreadonlyJniPeerMembers_members_IRunnable=newJniPeerMembers("java/lang/Runnable",typeof(JniPeerMembersRunnableInvoker));
976+
977+
publicunsafevoidRun()
978+
{
979+
conststring__id="run.()V";
980+
try{
981+
_members_IRunnable.InstanceMethods.InvokeAbstractVoidMethod(__id,this,null);
982+
}finally{
983+
}
984+
}
985+
}
896986
}
897987

‎tests/Java.Interop-PerformanceTests/java/com/xamarin/interop/performance/JavaTiming.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,14 @@ public static void StaticVoidMethod2IArgs (int obj1, int obj2)
7777
publicstaticvoidStaticVoidMethod3IArgs (intobj1, intobj2, intobj3)
7878
{
7979
}
80+
81+
publicstaticRunnableCreateRunnable ()
82+
{
83+
returnnewRunnable () {
84+
publicvoidrun ()
85+
{
86+
}
87+
};
88+
}
8089
}
8190

0 commit comments

Comments
 (0)