Skip to content

Commit 29f9707

Browse files
jpobstjonpryor
authored andcommitted
[generator] Support default interface methods. (#459)
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
1 parent 8ed9677 commit 29f9707

27 files changed

+831
-99
lines changed

src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods_Invoke.tt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ namespace Java.Interop {
4444
JniPeerMembers.AssertSelf (self);
4545

4646
var declaringType = DeclaringType;
47-
if (Members.ShouldUseVirtualDispatch (self, declaringType)) {
47+
if (Members.UsesVirtualDispatch (self, declaringType)) {
4848
var m = GetMethodInfo (encodedMember);
4949
<#= returnType.ReturnType != "void" ? "return " : "" #>JniEnvironment.InstanceMethods.Call<#= returnType.JniCallType #>Method (self.PeerReference, m, parameters);
5050
<#= returnType.ReturnType == "void" ? "return;" : "" #>

tools/generator/CodeGenerationOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ internal CodeGenerator CreateCodeGenerator (TextWriter writer)
4848
public bool UseShortFileNames { get; set; }
4949
public int ProductVersion { get; set; }
5050
public bool SupportInterfaceConstants { get; set; }
51+
public bool SupportDefaultInterfaceMethods { get; set; }
5152
public bool UseShallowReferencedTypes { get; set; }
5253

5354
bool? buildingCoreAssembly;

tools/generator/CodeGenerator.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ static void Run (CodeGeneratorOptions options, DirectoryAssemblyResolver resolve
6666
UseShortFileNames = options.UseShortFileNames,
6767
ProductVersion = options.ProductVersion,
6868
SupportInterfaceConstants = options.SupportInterfaceConstants,
69+
SupportDefaultInterfaceMethods = options.SupportDefaultInterfaceMethods,
6970
};
7071

7172
// Load reference libraries
@@ -143,7 +144,7 @@ static void Run (CodeGeneratorOptions options, DirectoryAssemblyResolver resolve
143144
// disable interface default methods here, especially before validation.
144145
gens = gens.Where (g => !g.IsObfuscated && g.Visibility != "private").ToList ();
145146
foreach (var gen in gens) {
146-
gen.StripNonBindables ();
147+
gen.StripNonBindables (opt);
147148
if (gen.IsGeneratable)
148149
AddTypeToTable (opt, gen);
149150
}

tools/generator/CodeGeneratorOptions.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public CodeGeneratorOptions ()
4040
public bool OnlyRunApiXmlAdjuster { get; set; }
4141
public string ApiXmlAdjusterOutput { get; set; }
4242
public bool SupportInterfaceConstants { get; set; }
43+
public bool SupportDefaultInterfaceMethods { get; set; }
4344

4445
public static CodeGeneratorOptions Parse (string[] args)
4546
{
@@ -87,8 +88,11 @@ public static CodeGeneratorOptions Parse (string[] args)
8788
"SDK Platform {VERSION}/API level.",
8889
v => opts.ApiLevel = v },
8990
{ "lang-features=",
90-
"For internal use. (Flags: interface-constants)",
91-
v => opts.SupportInterfaceConstants = v?.Contains ("interface-constants") == true },
91+
"For internal use. (Flags: interface-constants,default-interface-methods)",
92+
v => {
93+
opts.SupportInterfaceConstants = v?.Contains ("interface-constants") == true;
94+
opts.SupportDefaultInterfaceMethods = v?.Contains ("default-interface-methods") == true;
95+
}},
9296
{ "preserve-enums",
9397
"For internal use.",
9498
v => opts.PreserveEnums = v != null },
@@ -157,6 +161,11 @@ public static CodeGeneratorOptions Parse (string[] args)
157161

158162
opts.ApiDescriptionFile = apis [0];
159163

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

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

Lines changed: 81 additions & 36 deletions
Large diffs are not rendered by default.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ internal override void WriteMethodIdField (Method method, string indent)
134134
// No method id_ field required; it's now an `id` constant in the binding.
135135
}
136136

137-
internal override void WriteMethodBody (Method method, string indent)
137+
internal override void WriteMethodBody (Method method, string indent, GenBase type)
138138
{
139139
writer.WriteLine ("{0}const string __id = \"{1}.{2}\";", indent, method.JavaName, method.JniSignature);
140140
foreach (string prep in method.Parameters.GetCallPrep (opt))
@@ -159,7 +159,7 @@ internal override void WriteMethodBody (Method method, string indent)
159159
writer.WriteLine ("_members.InstanceMethods.InvokeNonvirtual{0}Method (__id, this{1});",
160160
invokeType,
161161
method.Parameters.GetCallArgs (opt, invoker: false));
162-
} else if (method.IsVirtual && !method.IsAbstract) {
162+
} else if ((method.IsVirtual && !method.IsAbstract) || method.IsInterfaceDefaultMethod) {
163163
writer.WriteLine ("_members.InstanceMethods.InvokeVirtual{0}Method (__id, this{1});",
164164
invokeType,
165165
method.Parameters.GetCallArgs (opt, invoker: false));

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ void GenerateJNICall (Method method, string indent, string call, bool declare_re
123123
writer.WriteLine ("{0}return {1};", indent, method.RetVal.FromNative (opt, call, true));
124124
}
125125

126-
internal override void WriteMethodBody (Method method, string indent)
126+
internal override void WriteMethodBody (Method method, string indent, GenBase type)
127127
{
128128
writer.WriteLine ("{0}if ({1} == IntPtr.Zero)", indent, method.EscapedIdName);
129129
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);

tools/generator/Java.Interop.Tools.Generator.ObjectModel/GenBase.cs

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ public virtual void FixupExplicitImplementation ()
290290

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

303303
// Interface default methods can be overriden. We want to process them differently.
304-
foreach (var m in Methods.Where (m => m.IsInterfaceDefaultMethod)) {
305-
foreach (var bt in GetAllDerivedInterfaces ()) {
306-
var bm = bt.Methods.FirstOrDefault (mm => mm.Name == m.Name && ParameterList.Equals (mm.Parameters, m.Parameters));
304+
var checkDimOverrideTargets = opt.SupportDefaultInterfaceMethods ? Methods : Methods.Where (m => m.IsInterfaceDefaultMethod);
305+
306+
// We need to check all the implemented interfaces of all the base types.
307+
var allIfaces = new List<InterfaceGen> ();
308+
309+
for (var gen = this; gen != null; gen = gen.BaseGen)
310+
gen.GetAllDerivedInterfaces (allIfaces);
311+
312+
foreach (var m in checkDimOverrideTargets.Where (m => !m.IsStatic)) {
313+
foreach (var bt in allIfaces.Distinct ()) {
314+
// We mark a method as an override if (1) it is a DIM, or (2) if the base method is DIM
315+
// (i.e. we don't mark as override if a class method "implements" normal iface method.)
316+
var bm = bt.Methods.FirstOrDefault (mm => (m.IsInterfaceDefaultMethod || !mm.IsAbstract) && mm.Name == m.Name && ParameterList.Equals (mm.Parameters, m.Parameters));
317+
307318
if (bm != null) {
308-
m.IsInterfaceDefaultMethodOverride = true;
319+
m.OverriddenInterfaceMethod = bm;
309320
break;
310321
}
311322
}
@@ -411,7 +422,7 @@ void visit (ISymbol isym)
411422
}
412423

413424
public IEnumerable<Method> GetAllMethods () =>
414-
Methods.Concat (Properties.Select (p => p.Getter)).Concat (Properties.Select (p => p.Setter).Where (m => m != null));
425+
Methods.Concat (Properties.Select (p => p.Getter)).Concat (Properties.Select (p => p.Setter)).Where (m => m != null);
415426

416427
GenBase GetBaseGen (CodeGenerationOptions opt)
417428
{
@@ -661,9 +672,12 @@ protected virtual bool OnValidate (CodeGenerationOptions opt, GenericParameterDe
661672
}
662673
Fields = valid_fields;
663674

664-
int method_cnt = Methods.Count;
675+
// If we can't validate a default interface method it's ok to ignore it and still bind the interface
676+
var method_cnt = Methods.Where (m => !m.IsInterfaceDefaultMethod).Count ();
677+
665678
Methods = Methods.Where (m => ValidateMethod (opt, m, context)).ToList ();
666-
MethodValidationFailed = method_cnt != Methods.Count;
679+
MethodValidationFailed = method_cnt != Methods.Where (m => !m.IsInterfaceDefaultMethod).Count ();
680+
667681
foreach (Method m in Methods) {
668682
if (m.IsVirtual)
669683
HasVirtualMethods = true;
@@ -736,15 +750,16 @@ bool ReturnTypeMatches (Method m, Method mm)
736750

737751
public bool ShouldGenerateAnnotationAttribute => IsAnnotation;
738752

739-
public void StripNonBindables ()
753+
public void StripNonBindables (CodeGenerationOptions opt)
740754
{
741-
// As of now, if we generate bindings for interface default methods, that means users will
742-
// have to "implement" those methods because they are declared and you have to implement
743-
// any declared methods in C#. That is going to be problematic a lot.
744-
Methods = Methods.Where (m => !m.IsInterfaceDefaultMethod).ToList ();
755+
// Strip out default interface methods if not desired
756+
if (!opt.SupportDefaultInterfaceMethods)
757+
Methods = Methods.Where (m => !m.IsInterfaceDefaultMethod).ToList ();
758+
745759
NestedTypes = NestedTypes.Where (n => !n.IsObfuscated && n.Visibility != "private").ToList ();
760+
746761
foreach (var n in NestedTypes)
747-
n.StripNonBindables ();
762+
n.StripNonBindables (opt);
748763
}
749764

750765
static readonly HashSet<string> ThrowableRequiresNew = new HashSet<string> (

tools/generator/Java.Interop.Tools.Generator.ObjectModel/InterfaceGen.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ public IEnumerable<Field> GetGeneratableFields (CodeGenerationOptions options)
141141
return Fields.Where (f => !f.NeedsProperty && !(f.DeprecatedComment?.Contains ("constant will be removed") == true));
142142
}
143143

144+
public bool HasDefaultMethods => GetAllMethods ().Any (m => m.IsInterfaceDefaultMethod);
145+
144146
public bool IsConstSugar {
145147
get {
146148
if (Methods.Count > 0 || Properties.Count > 0)

tools/generator/Java.Interop.Tools.Generator.ObjectModel/Method.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public Method (GenBase declaringType) : base (declaringType)
2020
public bool IsAbstract { get; set; }
2121
public bool IsFinal { get; set; }
2222
public bool IsInterfaceDefaultMethod { get; set; }
23-
public bool IsInterfaceDefaultMethodOverride { get; set; }
23+
public Method OverriddenInterfaceMethod { get; set; }
2424
public bool IsReturnEnumified { get; set; }
2525
public bool IsStatic { get; set; }
2626
public bool IsVirtual { get; set; }
@@ -123,6 +123,9 @@ internal string GetAdapterName (CodeGenerationOptions opt, string adapter)
123123
return adapter + AssemblyName;
124124
}
125125

126+
// Connectors for DIM are defined on the interface, not the implementing type
127+
public string GetConnectorNameFull (CodeGenerationOptions opt) => ConnectorName + (opt.SupportDefaultInterfaceMethods && IsInterfaceDefaultMethod ? $":{DeclaringType.FullName}, " + (AssemblyName ?? opt.AssemblyName) : string.Empty);
128+
126129
internal string GetDelegateType ()
127130
{
128131
var parms = Parameters.DelegateTypeParams;

0 commit comments

Comments
 (0)