-
Notifications
You must be signed in to change notification settings - Fork 54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[generator] generator --lang-features=emit-legacy-interface-invokers
#1145
Conversation
Fixes: #910 Context: bc5bcf4 Context: #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 different 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 required. 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 #858 enters the picture: interface invokers were full of Android-isms -- `Android.Runtime.JNIEnv.GetMethodID()`! -- 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 inherited 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_ViewManager = …; static readonly JniPeerMembers _members_WindowManager = …; public void AddView(View view, ViewGroup.LayoutParams @params) { const string __id = "addView.…"; _members_ViewManager.InstanceMethods.InvokeAbstractVoidMethod (__id, this, …); } public void RemoveViewImmediate(View view) { const string __id = "removeViewImmediate.…"; _members_WindowManager.InstanceMethods.InvokeAbstractVoidMethod (__id, this, …); } } This has two advantages: 1. More caching! 2. Desktop `Java.Base` binding can now have interface invokers. TODO: Performance? How much better is this? Update tests/invocation-overhead. Update `tests/generator-Tests` expected output. Note: to keep this patch small, only JavaInterop1 output uses the new pattern. XAJavaInterop1 output is unchanged, *even though* it will use the new output *by default*. 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, 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. [0]: https://learn.microsoft.com/en-us/dotnet/csharp/misc/cs0114
TODO:
|
How bad are instance `jmethodID`s? (Or, how much better is using `JniPeerMembers`?) 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 `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. Oddly -- suggesting that something is wrong with my test, review welcome! -- if 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.)
…java.interop into jonp-iface-invokers-910
Context: dotnet/java-interop#1145 Does It Build™?
DO NOT MERGE until we're targeting .NET 9. and dotnet/android#8339 is good. |
Trying to build in xamarin-android resulted in lots of errors: …/mcw/Android.Text.IInputType.cs(34,17): error CS0122: 'Object._members' is inaccessible due to its protection level Oops. Looks like at least *some* tests need to run w/ XAJavaInterop1 output! Update `BaseGeneratorTest` to use `XAJavaInterop1` output for the tests, update `generator` so that the tests *pass*, and update the expected output accordingly.
Context: dotnet/java-interop#1145 Does It Build™?
Context: dotnet/android#8339 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`. Update `tests/generator-Tests/Integration-Tests/Interfaces.cs` so that we test interface generation for JavaInterop1. Update `tests/generator-Tests/SupportFiles/*.cs` so that `Interfaces.cs` test output can be compiled. Update `tests/generator-Tests/expected.ji/TestInterface/TestInterface.xml` so that some parameter names for `set*()` methods are *not* already named `value`, to induce the original error. Flush `tests/generator-Tests/expected.ji/TestInterface`.
A concern with *just* using the interface name is when the same interface name exists in multiple packages, and you're implementing *all* of them: package p1; public interface List {} package p2; public interface List {} package example; public interface MyList implements p1.List, p2.List {} With this API, the interface invoker for `IMyListInvoker` would try to emit `_members_List` *twice*, which is a good way to get compilation errors. To avoid this, we could *hash* the package name + type name, as we do with Java Callable Wrappers, but if you see `_members_hash1` vs. `_members_hash2`, it's difficult to review what's going on. Instead, use the full Java-side name, "mangled" so that it can be an identifier. This results in: partial class IMyListInvoker : Java.Lang.Object, IMyList { static readonly JniPeerMembers _members_p1_List = …; static readonly JniPeerMembers _members_p2_List = …; static readonly JniPeerMembers _members_example_MyList = … } This can result in long member names, but we should be well within C# language limits (512 characters?), and it's readable!
Consider `java.util.Deque`: /* partial */ interface Collection<E> { boolean add(E e); void clear(); } /* partial */ interface Queue<E> implements Collection<E> { boolean add(E e); } /* partial */ interface Deque<E> implements Queue<E> { boolean add(E e); } When implementing `IDequeInvoker`, we need to implement `ICollection.Clear()`, which is only declared in `Collection`: partial class IDequeInvoker { static readonly JniPeerMembers _members_java_util_Deque = new JniPeerMembers ("java/util/Deque", typeof (IDequeInvoker)); static readonly JniPeerMembers _members_java_util_Queue = new JniPeerMembers ("java/util/Queue", typeof (IDequeInvoker)); public unsafe void Clear () { const string __id = "clear.()V"; try { _members_java_util_Collection.InstanceMethods.InvokeAbstractVoidMethod (__id, this, null); } finally { } } } The problem? `_members_java_util_Collection` didn't exist, because we only emitted `_members_*` fields for interfaces returned by `GenBase.GetAllImplementedInterfaces()`, which only returns "toplevel" implemented interfaces, but not the full graph of interfaces. In this case, we got `_members_java_util_Queue`, but not `_members_java_util_Collection`, which meant that `IDequeInvoker.Clear()` was attempting to access a field which did not exist. Update `InterfaceInvokerClass` so that the complete set of implemented interfaces is retrieved and emitted.
Context: dotnet/java-interop#1145 Does It Build™?
Context: dotnet/java-interop#1145 Does It Build™?
Context: dotnet/java-interop#1145 Does It Build™?
HashSet has indeterminate ordering, so rebuilds may change the field order, which could make things weird. Sort them.
Performance Tests?
…which makes me think there's something wrong with the current performance tests. Increasing |
broken in 2a20a08. Order changed!
v => { | ||
opts.EmitLegacyInterfaceInvokers = v?.Contains ("emit-legacy-interface-invokers") == true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the intention with this flag? Will we use it internally? Is it an escape hatch if there are issues with this implementation? Are we going to publicly expose it for users?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the intention is that if the new idea screws somebody up -- they can't compile, whatever -- then they'll have a way to disable it. As such, yes, we'd need to publicly expose the toggle via a .NET Android MSBuild property.
@@ -366,6 +366,23 @@ public void FixupMethodOverrides (CodeGenerationOptions opt) | |||
set => support.FullName = value; | |||
} | |||
|
|||
(object Package, object Name, string Id) javaFullNameIdCache; | |||
|
|||
public string JavaFullNameId { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to add a comment to this property explaining what the format is and what uses it. There are so many *Name
properties that I would like to start documenting what each one is for.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doc-comment added.
Context: dotnet/java-interop#1145 Does It Build™?
Fixes: dotnet/java-interop#910 Fixes: dotnet/java-interop#1139 Changes: dotnet/java-interop@3c83179...1adb796 * dotnet/java-interop@1adb7964: [generator] `generator --lang-features=emit-legacy-interface-invokers` (dotnet/java-interop#1145) * dotnet/java-interop@6bd7ae48: [Xamarin.Android.Tools.Bytecode] Kotlin internal prop visibility (dotnet/java-interop#1151) Updates `class-parse` to better support parsing the visibility of Kotlin properties. Updates `generator` to emit optimized interface Invokers. Add support for a new `$(AndroidEmitLegacyInterfaceInvokers)` MSBuild property; when `true`, causes `generator` to emit legacy interface Invokers. The default value is `false`.
Fixes: dotnet/java-interop#910 Fixes: dotnet/java-interop#1139 Changes: dotnet/java-interop@3c83179...38c8a82 * dotnet/java-interop@38c8a827: [Xamarin.Android.Tools.Bytecode] Kotlin unsigned internal props (dotnet/java-interop#1156) * dotnet/java-interop@1adb7964: [generator] `generator --lang-features=emit-legacy-interface-invokers` (dotnet/java-interop#1145) * dotnet/java-interop@6bd7ae48: [Xamarin.Android.Tools.Bytecode] Kotlin internal prop visibility (dotnet/java-interop#1151) Updates `class-parse` to better support parsing the visibility of Kotlin properties. Updates `generator` to emit optimized interface Invokers. The previous interface invoker codegen strategy can be enabled by setting `$(_AndroidEmitLegacyInterfaceInvokers)`=True.
Fixes: #910
Context: bc5bcf4
Context: #858
Consider the Java
java.lang.Runnable
interface:This is bound as:
with some slight differences depending on whether we're dealing with .NET Android (
generator --codegen-target=xajavainterop1
) orsrc/Java.Base
(generator --codegen-target=javainterop1
).Now, assume a Java API + corresponding binding which returns a
Runnable
instance:You can invoke
IRunnable.Run()
on the return value: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:Once Upon A Time™, the interface invoker implementation mirrored that of classes: a static
IntPtr
field held thejmethodID
value, which would be looked up on first-use and cached for subsequent invocations:This approach works until you have interface inheritance and methods which come from different interfaces:
This would be bound as:
Unfortunately, invoking
IViewManager.AddView()
through anIWindowManagerInvoker
would crash!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:Pro: no more crash!
Con: every different instance of
IWindowManagerInvoker
needs to separately lookup whatever methods are required. There is some caching, so repeated calls toAddView()
on the same instance will hit the cache, but if you obtain a differentIWindowManager
instance,jmethodID
values will need to be looked up again.This was "fine", until #858 enters the picture: interface invokers were full of Android-isms --
Android.Runtime.JNIEnv.GetMethodID()
! -- 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 useJniPeerMembers
for method lookup and invocation, allowingjmethodID
values to be cached across instances. In order to prevent the runtime crash, an interface may have multipleJniPeerMembers
values, one per inherited interface, which is used to invoke methods from that interface.IWindowManagerInvoker
now becomes:This has two advantages:
Java.Base
binding can now have interface invokers.TODO: Performance? How much better is this?
Update tests/invocation-overhead.
Update
tests/generator-Tests
expected output.Note: to keep this patch small, only JavaInterop1 output uses the new pattern. XAJavaInterop1 output is unchanged, even though it will use the new output by default.
Added CS0114 to
$(NoWarn)
inJava.Base.csproj
to ignore warnings such as:Ignoring CS0114 is also done in
Mono.Android.dll
as well, so this is not a new or unique requirement.Update
Java.Interop.dll
so thatJniRuntime.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 abstractclass
orinterface
.Update
tests/Java.Base-Tests
to test for implicit*Invoker
lookup and invocation support.