Skip to content

Commit

Permalink
[generator] Support default interface methods. (#459)
Browse files Browse the repository at this point in the history
Fixes: #25

Context: #341

Java 8 supports [interface default methods][0]:

	public interface HelloJava8 {
	    public void a ();
	    public default int getFoo () {
	        return 8;
	    }
	    public default void setFoo (int newValue) {
	        throw new UnsupportedOperationException();
	    }
	}

With C#8, C# also supports [default interface members][1].

Add a new `generator --lang-features=default-interface-methods` flag
which takes advantage of C#8 default interface members to bind Java 8
default interface methods:

	// C# binding of HelloJava8
	public partial interface IHelloJava8 : IJavaObject, IJavaPeerable {
	    static new readonly JniPeerMembers _members = new JniPeerMembers ("HelloJava8", typeof (IHelloJava8));

	    void A();

	    virtual unsafe int Foo {
	        [Regsiter ("getFoo", "()I", "…")]
	        get {
	            return _members.InstanceMethods.InvokeVirtualInt32Method ("getFoo.()I", this, null);
	        }
	        [Regsiter ("setFoo", "(I)V", "…")]
	        set {
	            JniArgumentValue* __args = stackalloc JniArgumentValue [1];
	            __args [0] = new JniArgumentValue (value);
	            return _members.InstanceMethods.InvokeVirtualVoidMethod ("setFoo.(I)V", this, __args);
	        }
	    }
	}

C#8 Default Interface Members cannot be used with legacy
`generator --codegen-target=XamarinAndroid`, as they require the
`IJavaPeerable` infrastructure in order to work.

Connector Methods are emitted within the interface binding, and not
within the corresponding `*Invoker` type.

If a Java default interface method is "invalid", we just skip binding
the method instead of invalidating the entire interface, just as we
do with classes and non-`abstract` methods.

Finally, the default interface method implementation uses `virtual`
dispatch, *not* non-`virtual` dispatch, in order to support Java-side
versioning.  For example, imagine `exampele.jar` v1:

	// Java
	public interface Fooable {
	    default void foo() {
	        System.out.println ("Fooable.foo");
	    }
	}
	public class Example implements Fooable {
	}

In v1, `Example` does *not* contain an `Example.foo()` method, though
`foo()` can be invoked on it, because of `Fooable.foo()`:

	Fooable value = new Example();
	value.foo();  // Invokes Fooable.foo()

In v2, `Example` overrides `Fooable.foo`:

	public class Example implements Fooable {
	    public void foo() {
	        System.out.println ("Example.foo");
	    }
	}

If our binding used non-`virtual` dispatch for `IFooable.Foo()`, and
bound `example.jar` v1, then if we updated `example.jar` to v2
*without* producing a new binding -- and why should a new binding
be required? -- then we would continue invoking `Fooable.foo()` when
we *should* be invoking `Example.foo()`.  Use of `virtual` dispatch
thus ensures we support Java-side versioning.

[0]: https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html
[1]: https://github.com/dotnet/csharplang/blob/f7952cdddf85316a4beec493a0ecc14fcb3241c8/proposals/csharp-8.0/default-interface-methods.md
  • Loading branch information
jpobst authored and Steve Pfister committed Aug 30, 2019
1 parent 01f9fa9 commit 2420da4
Show file tree
Hide file tree
Showing 27 changed files with 831 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ namespace Java.Interop {
JniPeerMembers.AssertSelf (self);

var declaringType = DeclaringType;
if (Members.ShouldUseVirtualDispatch (self, declaringType)) {
if (Members.UsesVirtualDispatch (self, declaringType)) {
var m = GetMethodInfo (encodedMember);
<#= returnType.ReturnType != "void" ? "return " : "" #>JniEnvironment.InstanceMethods.Call<#= returnType.JniCallType #>Method (self.PeerReference, m, parameters);
<#= returnType.ReturnType == "void" ? "return;" : "" #>
Expand Down
1 change: 1 addition & 0 deletions tools/generator/CodeGenerationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ internal CodeGenerator CreateCodeGenerator (TextWriter writer)
public bool UseShortFileNames { get; set; }
public int ProductVersion { get; set; }
public bool SupportInterfaceConstants { get; set; }
public bool SupportDefaultInterfaceMethods { get; set; }
public bool UseShallowReferencedTypes { get; set; }

bool? buildingCoreAssembly;
Expand Down
3 changes: 2 additions & 1 deletion tools/generator/CodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ static void Run (CodeGeneratorOptions options, DirectoryAssemblyResolver resolve
UseShortFileNames = options.UseShortFileNames,
ProductVersion = options.ProductVersion,
SupportInterfaceConstants = options.SupportInterfaceConstants,
SupportDefaultInterfaceMethods = options.SupportDefaultInterfaceMethods,
};

// Load reference libraries
Expand Down Expand Up @@ -143,7 +144,7 @@ static void Run (CodeGeneratorOptions options, DirectoryAssemblyResolver resolve
// disable interface default methods here, especially before validation.
gens = gens.Where (g => !g.IsObfuscated && g.Visibility != "private").ToList ();
foreach (var gen in gens) {
gen.StripNonBindables ();
gen.StripNonBindables (opt);
if (gen.IsGeneratable)
AddTypeToTable (opt, gen);
}
Expand Down
13 changes: 11 additions & 2 deletions tools/generator/CodeGeneratorOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public CodeGeneratorOptions ()
public bool OnlyRunApiXmlAdjuster { get; set; }
public string ApiXmlAdjusterOutput { get; set; }
public bool SupportInterfaceConstants { get; set; }
public bool SupportDefaultInterfaceMethods { get; set; }

public static CodeGeneratorOptions Parse (string[] args)
{
Expand Down Expand Up @@ -87,8 +88,11 @@ public static CodeGeneratorOptions Parse (string[] args)
"SDK Platform {VERSION}/API level.",
v => opts.ApiLevel = v },
{ "lang-features=",
"For internal use. (Flags: interface-constants)",
v => opts.SupportInterfaceConstants = v?.Contains ("interface-constants") == true },
"For internal use. (Flags: interface-constants,default-interface-methods)",
v => {
opts.SupportInterfaceConstants = v?.Contains ("interface-constants") == true;
opts.SupportDefaultInterfaceMethods = v?.Contains ("default-interface-methods") == true;
}},
{ "preserve-enums",
"For internal use.",
v => opts.PreserveEnums = v != null },
Expand Down Expand Up @@ -157,6 +161,11 @@ public static CodeGeneratorOptions Parse (string[] args)

opts.ApiDescriptionFile = apis [0];

if (opts.SupportDefaultInterfaceMethods && opts.CodeGenerationTarget == CodeGenerationTarget.XamarinAndroid) {
Console.Error.WriteLine (Report.Format (true, Report.ErrorInvalidArgument, "lang-features=default-interface-methods is not compatible with codegen-target=xamarinandroid."));
return null;
}

return opts;
}

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ internal override void WriteMethodIdField (Method method, string indent)
// No method id_ field required; it's now an `id` constant in the binding.
}

internal override void WriteMethodBody (Method method, string indent)
internal override void WriteMethodBody (Method method, string indent, GenBase type)
{
writer.WriteLine ("{0}const string __id = \"{1}.{2}\";", indent, method.JavaName, method.JniSignature);
foreach (string prep in method.Parameters.GetCallPrep (opt))
Expand All @@ -159,7 +159,7 @@ internal override void WriteMethodBody (Method method, string indent)
writer.WriteLine ("_members.InstanceMethods.InvokeNonvirtual{0}Method (__id, this{1});",
invokeType,
method.Parameters.GetCallArgs (opt, invoker: false));
} else if (method.IsVirtual && !method.IsAbstract) {
} else if ((method.IsVirtual && !method.IsAbstract) || method.IsInterfaceDefaultMethod) {
writer.WriteLine ("_members.InstanceMethods.InvokeVirtual{0}Method (__id, this{1});",
invokeType,
method.Parameters.GetCallArgs (opt, invoker: false));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ void GenerateJNICall (Method method, string indent, string call, bool declare_re
writer.WriteLine ("{0}return {1};", indent, method.RetVal.FromNative (opt, call, true));
}

internal override void WriteMethodBody (Method method, string indent)
internal override void WriteMethodBody (Method method, string indent, GenBase type)
{
writer.WriteLine ("{0}if ({1} == IntPtr.Zero)", indent, method.EscapedIdName);
writer.WriteLine ("{0}\t{1} = JNIEnv.Get{2}MethodID (class_ref, \"{3}\", \"{4}\");", indent, method.EscapedIdName, method.IsStatic ? "Static" : String.Empty, method.JavaName, method.JniSignature);
Expand Down
43 changes: 29 additions & 14 deletions tools/generator/Java.Interop.Tools.Generator.ObjectModel/GenBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ public virtual void FixupExplicitImplementation ()

public void FixupMethodOverrides (CodeGenerationOptions opt)
{
foreach (var m in Methods.Where (m => !m.IsInterfaceDefaultMethod)) {
foreach (var m in Methods.Where (m => !m.IsStatic && !m.IsInterfaceDefaultMethod)) {
for (var bt = GetBaseGen (opt); bt != null; bt = bt.GetBaseGen (opt)) {
var bm = bt.Methods.FirstOrDefault (mm => mm.Name == m.Name && mm.Visibility == m.Visibility && ParameterList.Equals (mm.Parameters, m.Parameters));
if (bm != null && bm.RetVal.FullName == m.RetVal.FullName) { // if return type is different, it could be still "new", not "override".
Expand All @@ -301,11 +301,22 @@ public void FixupMethodOverrides (CodeGenerationOptions opt)
}

// Interface default methods can be overriden. We want to process them differently.
foreach (var m in Methods.Where (m => m.IsInterfaceDefaultMethod)) {
foreach (var bt in GetAllDerivedInterfaces ()) {
var bm = bt.Methods.FirstOrDefault (mm => mm.Name == m.Name && ParameterList.Equals (mm.Parameters, m.Parameters));
var checkDimOverrideTargets = opt.SupportDefaultInterfaceMethods ? Methods : Methods.Where (m => m.IsInterfaceDefaultMethod);

// We need to check all the implemented interfaces of all the base types.
var allIfaces = new List<InterfaceGen> ();

for (var gen = this; gen != null; gen = gen.BaseGen)
gen.GetAllDerivedInterfaces (allIfaces);

foreach (var m in checkDimOverrideTargets.Where (m => !m.IsStatic)) {
foreach (var bt in allIfaces.Distinct ()) {
// We mark a method as an override if (1) it is a DIM, or (2) if the base method is DIM
// (i.e. we don't mark as override if a class method "implements" normal iface method.)
var bm = bt.Methods.FirstOrDefault (mm => (m.IsInterfaceDefaultMethod || !mm.IsAbstract) && mm.Name == m.Name && ParameterList.Equals (mm.Parameters, m.Parameters));

if (bm != null) {
m.IsInterfaceDefaultMethodOverride = true;
m.OverriddenInterfaceMethod = bm;
break;
}
}
Expand Down Expand Up @@ -411,7 +422,7 @@ void visit (ISymbol isym)
}

public IEnumerable<Method> GetAllMethods () =>
Methods.Concat (Properties.Select (p => p.Getter)).Concat (Properties.Select (p => p.Setter).Where (m => m != null));
Methods.Concat (Properties.Select (p => p.Getter)).Concat (Properties.Select (p => p.Setter)).Where (m => m != null);

GenBase GetBaseGen (CodeGenerationOptions opt)
{
Expand Down Expand Up @@ -661,9 +672,12 @@ protected virtual bool OnValidate (CodeGenerationOptions opt, GenericParameterDe
}
Fields = valid_fields;

int method_cnt = Methods.Count;
// If we can't validate a default interface method it's ok to ignore it and still bind the interface
var method_cnt = Methods.Where (m => !m.IsInterfaceDefaultMethod).Count ();

Methods = Methods.Where (m => ValidateMethod (opt, m, context)).ToList ();
MethodValidationFailed = method_cnt != Methods.Count;
MethodValidationFailed = method_cnt != Methods.Where (m => !m.IsInterfaceDefaultMethod).Count ();

foreach (Method m in Methods) {
if (m.IsVirtual)
HasVirtualMethods = true;
Expand Down Expand Up @@ -736,15 +750,16 @@ bool ReturnTypeMatches (Method m, Method mm)

public bool ShouldGenerateAnnotationAttribute => IsAnnotation;

public void StripNonBindables ()
public void StripNonBindables (CodeGenerationOptions opt)
{
// As of now, if we generate bindings for interface default methods, that means users will
// have to "implement" those methods because they are declared and you have to implement
// any declared methods in C#. That is going to be problematic a lot.
Methods = Methods.Where (m => !m.IsInterfaceDefaultMethod).ToList ();
// Strip out default interface methods if not desired
if (!opt.SupportDefaultInterfaceMethods)
Methods = Methods.Where (m => !m.IsInterfaceDefaultMethod).ToList ();

NestedTypes = NestedTypes.Where (n => !n.IsObfuscated && n.Visibility != "private").ToList ();

foreach (var n in NestedTypes)
n.StripNonBindables ();
n.StripNonBindables (opt);
}

static readonly HashSet<string> ThrowableRequiresNew = new HashSet<string> (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ public IEnumerable<Field> GetGeneratableFields (CodeGenerationOptions options)
return Fields.Where (f => !f.NeedsProperty && !(f.DeprecatedComment?.Contains ("constant will be removed") == true));
}

public bool HasDefaultMethods => GetAllMethods ().Any (m => m.IsInterfaceDefaultMethod);

public bool IsConstSugar {
get {
if (Methods.Count > 0 || Properties.Count > 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public Method (GenBase declaringType) : base (declaringType)
public bool IsAbstract { get; set; }
public bool IsFinal { get; set; }
public bool IsInterfaceDefaultMethod { get; set; }
public bool IsInterfaceDefaultMethodOverride { get; set; }
public Method OverriddenInterfaceMethod { get; set; }
public bool IsReturnEnumified { get; set; }
public bool IsStatic { get; set; }
public bool IsVirtual { get; set; }
Expand Down Expand Up @@ -123,6 +123,9 @@ internal string GetAdapterName (CodeGenerationOptions opt, string adapter)
return adapter + AssemblyName;
}

// Connectors for DIM are defined on the interface, not the implementing type
public string GetConnectorNameFull (CodeGenerationOptions opt) => ConnectorName + (opt.SupportDefaultInterfaceMethods && IsInterfaceDefaultMethod ? $":{DeclaringType.FullName}, " + (AssemblyName ?? opt.AssemblyName) : string.Empty);

internal string GetDelegateType ()
{
var parms = Parameters.DelegateTypeParams;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// Metadata.xml XPath interface reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']"
[Register ("java/code/IMyInterface", "", "java.code.IMyInterfaceInvoker")]
public partial interface IMyInterface : IJavaObject {
// Metadata.xml XPath interface reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface2']"
[Register ("java/code/IMyInterface2", "", "java.code.IMyInterface2Invoker")]
public partial interface IMyInterface2 : java.code.IMyInterface {

// Metadata.xml XPath method reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/method[@name='DoSomething' and count(parameter)=0]"
[global::Java.Interop.JavaInterfaceDefaultMethod]
[Register ("DoSomething", "()V", "GetDoSomethingHandler:java.code.IMyInterfaceInvoker, ")]
[Register ("DoSomething", "()V", "GetDoSomethingHandler:java.code.IMyInterface2Invoker, MyAssembly")]
void DoSomething ();

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
[global::Android.Runtime.Register ("java/code/IMyInterface", DoNotGenerateAcw=true)]
internal partial class IMyInterfaceInvoker : global::Java.Lang.Object, IMyInterface {

internal static new readonly JniPeerMembers _members = new JniPeerMembers ("java/code/IMyInterface", typeof (IMyInterfaceInvoker));

static IntPtr java_class_ref {
get { return _members.JniPeerType.PeerReference.Handle; }
}

public override global::Java.Interop.JniPeerMembers JniPeerMembers {
get { return _members; }
}

protected override IntPtr ThresholdClass {
get { return class_ref; }
}

protected override global::System.Type ThresholdType {
get { return _members.ManagedPeerType; }
}

IntPtr class_ref;

public static IMyInterface GetObject (IntPtr handle, JniHandleOwnership transfer)
{
return global::Java.Lang.Object.GetObject<IMyInterface> (handle, transfer);
}

static IntPtr Validate (IntPtr handle)
{
if (!JNIEnv.IsInstanceOf (handle, java_class_ref))
throw new InvalidCastException (string.Format ("Unable to convert instance of type '{0}' to type '{1}'.",
JNIEnv.GetClassNameFromInstance (handle), "java.code.IMyInterface"));
return handle;
}

protected override void Dispose (bool disposing)
{
if (this.class_ref != IntPtr.Zero)
JNIEnv.DeleteGlobalRef (this.class_ref);
this.class_ref = IntPtr.Zero;
base.Dispose (disposing);
}

public IMyInterfaceInvoker (IntPtr handle, JniHandleOwnership transfer) : base (Validate (handle), transfer)
{
IntPtr local_ref = JNIEnv.GetObjectClass (((global::Java.Lang.Object) this).Handle);
this.class_ref = JNIEnv.NewGlobalRef (local_ref);
JNIEnv.DeleteLocalRef (local_ref);
}

static Delegate cb_DoDeclaration;
#pragma warning disable 0169
static Delegate GetDoDeclarationHandler ()
{
if (cb_DoDeclaration == null)
cb_DoDeclaration = JNINativeWrapper.CreateDelegate ((Action<IntPtr, IntPtr>) n_DoDeclaration);
return cb_DoDeclaration;
}

static void n_DoDeclaration (IntPtr jnienv, IntPtr native__this)
{
java.code.IMyInterface __this = global::Java.Lang.Object.GetObject<java.code.IMyInterface> (jnienv, native__this, JniHandleOwnership.DoNotTransfer);
__this.DoDeclaration ();
}
#pragma warning restore 0169

IntPtr id_DoDeclaration;
public unsafe void DoDeclaration ()
{
if (id_DoDeclaration == IntPtr.Zero)
id_DoDeclaration = JNIEnv.GetMethodID (class_ref, "DoDeclaration", "()V");
JNIEnv.CallVoidMethod (((global::Java.Lang.Object) this).Handle, id_DoDeclaration);
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Metadata.xml XPath interface reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']"
[Register ("java/code/IMyInterface", "", "java.code.IMyInterfaceInvoker")]
public partial interface IMyInterface : IJavaObject, IJavaPeerable {
static new readonly JniPeerMembers _members = new JniPeerMembers ("java/code/IMyInterface", typeof (IMyInterface));

static Delegate cb_DoSomething;
#pragma warning disable 0169
static Delegate GetDoSomethingHandler ()
{
if (cb_DoSomething == null)
cb_DoSomething = JNINativeWrapper.CreateDelegate ((Action<IntPtr, IntPtr>) n_DoSomething);
return cb_DoSomething;
}

static void n_DoSomething (IntPtr jnienv, IntPtr native__this)
{
java.code.IMyInterface __this = global::Java.Lang.Object.GetObject<java.code.IMyInterface> (jnienv, native__this, JniHandleOwnership.DoNotTransfer);
__this.DoSomething ();
}
#pragma warning restore 0169

// Metadata.xml XPath method reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/method[@name='DoSomething' and count(parameter)=0]"
[Register ("DoSomething", "()V", "GetDoSomethingHandler:java.code.IMyInterface, MyAssembly")]
virtual unsafe void DoSomething ()
{
const string __id = "DoSomething.()V";
try {
_members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, null);
} finally {
}
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Metadata.xml XPath interface reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']"
[Register ("java/code/IMyInterface", "", "java.code.IMyInterfaceInvoker")]
public partial interface IMyInterface : IJavaObject, IJavaPeerable {
static new readonly JniPeerMembers _members = new JniPeerMembers ("java/code/IMyInterface", typeof (IMyInterface));

static Delegate cb_get_Value;
#pragma warning disable 0169
static Delegate Getget_ValueHandler ()
{
if (cb_get_Value == null)
cb_get_Value = JNINativeWrapper.CreateDelegate ((Func<IntPtr, IntPtr, int>) n_get_Value);
return cb_get_Value;
}

static int n_get_Value (IntPtr jnienv, IntPtr native__this)
{
java.code.IMyInterface __this = global::Java.Lang.Object.GetObject<java.code.IMyInterface> (jnienv, native__this, JniHandleOwnership.DoNotTransfer);
return __this.Value;
}
#pragma warning restore 0169

static Delegate cb_set_Value_I;
#pragma warning disable 0169
static Delegate Getset_Value_IHandler ()
{
if (cb_set_Value_I == null)
cb_set_Value_I = JNINativeWrapper.CreateDelegate ((Action<IntPtr, IntPtr, int>) n_set_Value_I);
return cb_set_Value_I;
}

static void n_set_Value_I (IntPtr jnienv, IntPtr native__this, int value)
{
java.code.IMyInterface __this = global::Java.Lang.Object.GetObject<java.code.IMyInterface> (jnienv, native__this, JniHandleOwnership.DoNotTransfer);
__this.Value = value;
}
#pragma warning restore 0169

virtual unsafe int Value {
// Metadata.xml XPath method reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/method[@name='get_Value' and count(parameter)=0]"
[Register ("get_Value", "()I", "Getget_ValueHandler:java.code.IMyInterface, MyAssembly")]
get {
const string __id = "get_Value.()I";
try {
var __rm = _members.InstanceMethods.InvokeVirtualInt32Method (__id, this, null);
return __rm;
} finally {
}
}
// Metadata.xml XPath method reference: path="/api/package[@name='java.code']/interface[@name='IMyInterface']/method[@name='set_Value' and count(parameter)=1 and parameter[1][@type='int']]"
[Register ("set_Value", "(I)V", "Getset_Value_IHandler:java.code.IMyInterface, MyAssembly")]
set {
const string __id = "set_Value.(I)V";
try {
JniArgumentValue* __args = stackalloc JniArgumentValue [1];
__args [0] = new JniArgumentValue (value);
_members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, __args);
} finally {
}
}
}

}

Loading

0 comments on commit 2420da4

Please sign in to comment.