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 jonpryor committed Aug 23, 2019
1 parent 8ed9677 commit 29f9707
Show file tree
Hide file tree
Showing 27 changed files with 831 additions and 99 deletions.
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
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
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
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.

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
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
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
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
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
@@ -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
@@ -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);
}

}

@@ -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 {
}
}

}

@@ -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 {
}
}
}

}

0 comments on commit 29f9707

Please sign in to comment.