Skip to content

Commit ef092a8

Browse files
jpobstjonpryor
authored andcommitted
[generator] All bound interfaces should inherit IJavaPeerable (#455)
Context: #341 Context: #341 (comment) Context: #25 Java 8 introduced support for [interface default methods][0]: // Java public interface Example { int defaultInterfaceMethod() { return 42; } } The question is, how should we bind them? Currently, we *don't* bind interface default methods: // C# public interface IExample : IJavaObject { // No `DefaultInterfaceMethod()` method } This means that C# types which implement `IExample` don't need to implement `Example.defaultInterfaceMethod()`, reducing the work that needs to be done: // C# public class MyExample : Java.Lang.Object, IExample { // No need to implement Example.defaultInterfaceMethod()! } However, if a C# type *does* wish to implement `Example.defaultInterfaceMethod()`, they *can* do so by using [`ExportAttribute`][1], but this can be painful, as it requires ensuring that the Java side and C# sides are consistent, without compiler assistance: // C# partial class MyExample : Java.Lang.Object, IExample { [Java.Interop.Export ("defaultInterfaceMethod")] // If the above string is wrong, e.g. via typo, the method isn't overridden! public int DefaultInterfaceMethod() { return 42*2; } } We want to improve support for this scenario by binding Java interface default methods as [C#8 Default Interface Members][2], as C#8 Default Interface Methods have similar semantics as Java 8 interface default methods, and means that `[ExportAttribute]` would no longer be necessary to override them: // C#8? partial class MyExample : Java.Lang.Object, IExample { // Just Works™, and the compiler will complain if there are typos. public override int DefaultInterfaceMethod() { return 42*2; } } However, a question arises: how do we *do* that? // C#8 public interface IExample : IJavaObject { [Register ("defaultInterfaceMethod", "()I", ...)] int DefaultInterfaceMethod() { // How do we call `Example.defaultInterfaceMethod()` here? } } The desirable thing would be for `IExample.DefaultInterfaceMethod()` to have the same implementation as any other method binding, e.g. when using `generator --codegen-target=XAJavaInterop1`: // C#8 partial interface IExample { [Register ("defaultInterfaceMethod", "()I", ...)] void DefaultInterfaceMethod() { const string __id = "defaultInterfaceMethod.()I"; return _members.InstanceMethods.InvokeVirtualInt32Method (__id, this, null); } } The problem is twofold: 1. There is no `_members` field to access here, and 2. Even if there were a `_members` field, `JniPeerMembers.JniInstanceMethods.InvokeVirtualInt32Method()` requires an `IJavaPeerable` instance, and `IExample` doesn't implement `IJavaPeerable`. (1) is straight forwardly solvable, as C#8 Default Interface Members also adds support for static members of all sorts, so it should be possible to add a static `_members` field, if deemed necessary. (2) is the holdup, and has a simple solution: "Just" have every bound interface *also* implement `IJavaPeerable`! - public partial interface IExample : IJavaObject + public partial interface IExample : IJavaObject, IJavaPeerable "But!", someone cries, "What about API compatibility?!" Isn't adding `IJavaPeerable` to the implements list of an interface a Breaking Change? In theory and *most* practice, *Yes*, this *would* be a Breaking Change, and thus something to be avoided. *However*, Xamarin.Android is *not* "most" practice. It has been an XA4212 error since ab3c2b2 / Xamarin.Android 8.0 to implement an interface that implements `IJavaObject` *without* inheriting from `Java.Lang.Object` or `Java.Lang.Throwable`: // elicits XA4212 error class MyBadClass : IExample { // Implements IJavaObject.Handle public IntPtr Handle { get {return IntPtr.Zero;} } } Meanwhile, both `Java.Lang.Object` and `Java.Lang.Throwable` have implemented `IJavaPeerable` since Xamarin.Android *6.1*. *Because* it has been an error to manually implement `IJavaObject` for nearly two years now, it should be Reasonably Safe™ to update interfaces to implement *both* `IJavaObject` *and* `IJavaPeerable`. Doing so should not break any code -- unless they've overridden the `$(AndroidErrorOnCustomJavaObject)` MSBuild property to False, in which case they deserve what happens to them. (It's not really possible to implement `IJavaObject` in a sane manner, and the "straightforward" approach means that passing instances of e.g. `MyBadClass` to Java code will result in passing *`null`* to Java, which almost certainly is NOT what is intended, hence XA4212!) [0]: https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html [1]: https://docs.microsoft.com/en-us/xamarin/android/platform/java-integration/working-with-jni#exportattribute-and-exportfieldattribute [2]: https://github.com/dotnet/csharplang/blob/f7952cdddf85316a4beec493a0ecc14fcb3241c8/proposals/csharp-8.0/default-interface-methods.md
1 parent be58159 commit ef092a8

File tree

21 files changed

+86
-19
lines changed

21 files changed

+86
-19
lines changed

tools/generator/Java.Interop.Tools.Generator.CodeGeneration/CodeGenerator.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ protected CodeGenerator (TextWriter writer, CodeGenerationOptions options)
2222
opt = options;
2323
}
2424

25+
internal virtual string GetAllInterfaceImplements () => "IJavaObject";
26+
2527
internal abstract void WriteClassHandle (ClassGen type, string indent, bool requireNew);
2628

2729
internal abstract void WriteClassHandle (InterfaceGen type, string indent, string declaringType);
@@ -522,7 +524,7 @@ public void WriteInterfaceDeclaration (InterfaceGen @interface, string indent)
522524
if (@interface.TypeParameters != null && @interface.TypeParameters.Any ())
523525
writer.WriteLine ("{0}{1}", indent, @interface.TypeParameters.ToGeneratedAttributeString ());
524526
writer.WriteLine ("{0}{1} partial interface {2}{3} {{", indent, @interface.Visibility, @interface.Name,
525-
@interface.IsConstSugar ? string.Empty : @interface.Interfaces.Count == 0 || sb.Length == 0 ? " : IJavaObject" : " : " + sb.ToString ());
527+
@interface.IsConstSugar ? string.Empty : @interface.Interfaces.Count == 0 || sb.Length == 0 ? " : " + GetAllInterfaceImplements () : " : " + sb.ToString ());
526528
WriteInterfaceFields (@interface, indent + "\t");
527529
writer.WriteLine ();
528530
WriteInterfaceProperties (@interface, indent + "\t");

tools/generator/Java.Interop.Tools.Generator.CodeGeneration/JavaInteropCodeGenerator.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ static string GetInvokeType (string type)
2323
}
2424
}
2525

26+
internal override string GetAllInterfaceImplements () => "IJavaObject, IJavaPeerable";
2627

2728
internal override void WriteClassHandle (ClassGen type, string indent, bool requireNew)
2829
{

tools/generator/Tests-Core/expected.ji/Android.Text.ISpanned.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Android.Text {
77

88
// Metadata.xml XPath interface reference: path="/api/package[@name='android.text']/interface[@name='Spanned']"
99
[Register ("android/text/Spanned", "", "Android.Text.ISpannedInvoker")]
10-
public partial interface ISpanned : IJavaObject {
10+
public partial interface ISpanned : IJavaObject, IJavaPeerable {
1111

1212
// Metadata.xml XPath method reference: path="/api/package[@name='android.text']/interface[@name='Spanned']/method[@name='getSpanFlags' and count(parameter)=1 and parameter[1][@type='java.lang.Object']]"
1313
[return:global::Android.Runtime.GeneratedEnum]

tools/generator/Tests-Core/expected.ji/Android.Views.View.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public partial class View : Java.Lang.Object {
1111

1212
// Metadata.xml XPath interface reference: path="/api/package[@name='android.view']/interface[@name='View.OnClickListener']"
1313
[Register ("android/view/View$OnClickListener", "", "Android.Views.View/IOnClickListenerInvoker")]
14-
public partial interface IOnClickListener : IJavaObject {
14+
public partial interface IOnClickListener : IJavaObject, IJavaPeerable {
1515

1616
// Metadata.xml XPath method reference: path="/api/package[@name='android.view']/interface[@name='View.OnClickListener']/method[@name='onClick' and count(parameter)=1 and parameter[1][@type='android.view.View']]"
1717
[Register ("onClick", "(Landroid/view/View;)V", "GetOnClick_Landroid_view_View_Handler:Android.Views.View/IOnClickListenerInvoker, ")]

tools/generator/Tests/Unit-Tests/CodeGeneratorExpectedResults/JavaInterop1/WriteInterface.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public abstract class MyInterfaceConsts : MyInterface {
3030

3131
// Metadata.xml XPath interface reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']"
3232
[Register ("java/code/IMyInterface", "", "java.code.IMyInterfaceInvoker")]
33-
public partial interface IMyInterface : IJavaObject {
33+
public partial interface IMyInterface : IJavaObject, IJavaPeerable {
3434

3535
int Count {
3636
// Metadata.xml XPath method reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/method[@name='get_Count' and count(parameter)=0]"
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Metadata.xml XPath interface reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']"
2+
[Register ("java/code/IMyInterface", "", "java.code.IMyInterfaceInvoker")]
3+
public partial interface IMyInterface : IJavaObject, IJavaPeerable {
4+
5+
int Count {
6+
// Metadata.xml XPath method reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/method[@name='get_Count' and count(parameter)=0]"
7+
[Register ("get_Count", "()I", "Getget_CountHandler:java.code.IMyInterfaceInvoker, ")] get;
8+
// Metadata.xml XPath method reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/method[@name='set_Count' and count(parameter)=1 and parameter[1][@type='int']]"
9+
[Register ("set_Count", "(I)V", "Getset_Count_IHandler:java.code.IMyInterfaceInvoker, ")] set;
10+
}
11+
12+
java.lang.String Key {
13+
// Metadata.xml XPath method reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/method[@name='get_Key' and count(parameter)=0]"
14+
[Register ("get_Key", "()Ljava/lang/String;", "Getget_KeyHandler:java.code.IMyInterfaceInvoker, ")] get;
15+
// Metadata.xml XPath method reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/method[@name='set_Key' and count(parameter)=1 and parameter[1][@type='java.lang.String']]"
16+
[Register ("set_Key", "(Ljava/lang/String;)V", "Getset_Key_Ljava_lang_String_Handler:java.code.IMyInterfaceInvoker, ")] set;
17+
}
18+
19+
int AbstractCount {
20+
// Metadata.xml XPath method reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/method[@name='get_AbstractCount' and count(parameter)=0]"
21+
[Register ("get_AbstractCount", "()I", "Getget_AbstractCountHandler:java.code.IMyInterfaceInvoker, ")] get;
22+
// Metadata.xml XPath method reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/method[@name='set_AbstractCount' and count(parameter)=1 and parameter[1][@type='int']]"
23+
[Register ("set_AbstractCount", "(I)V", "Getset_AbstractCount_IHandler:java.code.IMyInterfaceInvoker, ")] set;
24+
}
25+
26+
// Metadata.xml XPath method reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/method[@name='GetCountForKey' and count(parameter)=1 and parameter[1][@type='java.lang.String']]"
27+
[Register ("GetCountForKey", "(Ljava/lang/String;)I", "GetGetCountForKey_Ljava_lang_String_Handler:java.code.IMyInterfaceInvoker, ")]
28+
int GetCountForKey (string key);
29+
30+
// Metadata.xml XPath method reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/method[@name='Key' and count(parameter)=0]"
31+
[Register ("Key", "()Ljava/lang/String;", "GetKeyHandler:java.code.IMyInterfaceInvoker, ")]
32+
string Key ();
33+
34+
// Metadata.xml XPath method reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/method[@name='AbstractMethod' and count(parameter)=0]"
35+
[Register ("AbstractMethod", "()V", "GetAbstractMethodHandler:java.code.IMyInterfaceInvoker, ")]
36+
void AbstractMethod ();
37+
38+
}
39+

tools/generator/Tests/Unit-Tests/CodeGeneratorExpectedResults/JavaInterop1/WriteInterfaceFields.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Metadata.xml XPath interface reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']"
22
[Register ("java/code/IMyInterface", "", "java.code.IMyInterfaceInvoker")]
3-
public partial interface IMyInterface : IJavaObject {
3+
public partial interface IMyInterface : IJavaObject, IJavaPeerable {
44

55
// Metadata.xml XPath field reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/field[@name='MyConstantField']"
66
[Register ("MyConstantField")]
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Metadata.xml XPath interface reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']"
2+
[Register ("java/code/IMyInterface", "", "java.code.IMyInterfaceInvoker")]
3+
public partial interface IMyInterface : IJavaObject, IJavaPeerable {
4+
5+
// Metadata.xml XPath method reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/method[@name='DoSomething' and count(parameter)=0]"
6+
[global::Java.Interop.JavaInterfaceDefaultMethod]
7+
[Register ("DoSomething", "()V", "GetDoSomethingHandler:java.code.IMyInterfaceInvoker, ")]
8+
void DoSomething ();
9+
10+
}
11+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Metadata.xml XPath interface reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']"
2+
[Register ("java/code/IMyInterface", "", "java.code.IMyInterfaceInvoker")]
3+
public partial interface IMyInterface : IJavaObject {
4+
5+
// Metadata.xml XPath method reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/method[@name='DoSomething' and count(parameter)=0]"
6+
[global::Java.Interop.JavaInterfaceDefaultMethod]
7+
[Register ("DoSomething", "()V", "GetDoSomethingHandler:java.code.IMyInterfaceInvoker, ")]
8+
void DoSomething ();
9+
10+
}
11+

0 commit comments

Comments
 (0)