Skip to content
This repository was archived by the owner on Jul 6, 2026. It is now read-only.

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

Lines changed: 831 additions & 99 deletions

‎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
publicboolUseShortFileNames{get;set;}
4949
publicintProductVersion{get;set;}
5050
publicboolSupportInterfaceConstants{get;set;}
51+
publicboolSupportDefaultInterfaceMethods{get;set;}
5152
publicboolUseShallowReferencedTypes{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(vargeningens){
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
publicboolOnlyRunApiXmlAdjuster{get;set;}
4141
publicstringApiXmlAdjusterOutput{get;set;}
4242
publicboolSupportInterfaceConstants{get;set;}
43+
publicboolSupportDefaultInterfaceMethods{get;set;}
4344

4445
publicstaticCodeGeneratorOptionsParse(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+
returnnull;
167+
}
168+
160169
returnopts;
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-
internaloverridevoidWriteMethodBody(Methodmethod,stringindent)
137+
internaloverridevoidWriteMethodBody(Methodmethod,stringindent,GenBasetype)
138138
{
139139
writer.WriteLine("{0}const string __id = \"{1}.{2}\";",indent,method.JavaName,method.JniSignature);
140140
foreach(stringprepinmethod.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-
}elseif(method.IsVirtual&&!method.IsAbstract){
162+
}elseif((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-
internaloverridevoidWriteMethodBody(Methodmethod,stringindent)
126+
internaloverridevoidWriteMethodBody(Methodmethod,stringindent,GenBasetype)
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
publicvoidFixupMethodOverrides(CodeGenerationOptionsopt)
292292
{
293-
foreach(varminMethods.Where(m =>!m.IsInterfaceDefaultMethod)){
293+
foreach(varminMethods.Where(m =>!m.IsStatic&&!m.IsInterfaceDefaultMethod)){
294294
for(varbt=GetBaseGen(opt);bt!=null;bt=bt.GetBaseGen(opt)){
295295
varbm=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(varminMethods.Where(m =>m.IsInterfaceDefaultMethod)){
305-
foreach(varbtinGetAllDerivedInterfaces()){
306-
varbm=bt.Methods.FirstOrDefault(mm =>mm.Name==m.Name&&ParameterList.Equals(mm.Parameters,m.Parameters));
304+
varcheckDimOverrideTargets=opt.SupportDefaultInterfaceMethods?Methods:Methods.Where(m =>m.IsInterfaceDefaultMethod);
305+
306+
// We need to check all the implemented interfaces of all the base types.
307+
varallIfaces=newList<InterfaceGen>();
308+
309+
for(vargen=this;gen!=null;gen=gen.BaseGen)
310+
gen.GetAllDerivedInterfaces(allIfaces);
311+
312+
foreach(varmincheckDimOverrideTargets.Where(m =>!m.IsStatic)){
313+
foreach(varbtinallIfaces.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+
varbm=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
publicIEnumerable<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
GenBaseGetBaseGen(CodeGenerationOptionsopt)
417428
{
@@ -661,9 +672,12 @@ protected virtual bool OnValidate (CodeGenerationOptions opt, GenericParameterDe
661672
}
662673
Fields=valid_fields;
663674

664-
intmethod_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+
varmethod_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(MethodminMethods){
668682
if(m.IsVirtual)
669683
HasVirtualMethods=true;
@@ -736,15 +750,16 @@ bool ReturnTypeMatches (Method m, Method mm)
736750

737751
publicboolShouldGenerateAnnotationAttribute=>IsAnnotation;
738752

739-
publicvoidStripNonBindables()
753+
publicvoidStripNonBindables(CodeGenerationOptionsopt)
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(varninNestedTypes)
747-
n.StripNonBindables();
762+
n.StripNonBindables(opt);
748763
}
749764

750765
staticreadonlyHashSet<string>ThrowableRequiresNew=newHashSet<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
returnFields.Where(f =>!f.NeedsProperty&&!(f.DeprecatedComment?.Contains("constant will be removed")==true));
142142
}
143143

144+
publicboolHasDefaultMethods=>GetAllMethods().Any(m =>m.IsInterfaceDefaultMethod);
145+
144146
publicboolIsConstSugar{
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
publicboolIsAbstract{get;set;}
2121
publicboolIsFinal{get;set;}
2222
publicboolIsInterfaceDefaultMethod{get;set;}
23-
publicboolIsInterfaceDefaultMethodOverride{get;set;}
23+
publicMethodOverriddenInterfaceMethod{get;set;}
2424
publicboolIsReturnEnumified{get;set;}
2525
publicboolIsStatic{get;set;}
2626
publicboolIsVirtual{get;set;}
@@ -123,6 +123,9 @@ internal string GetAdapterName (CodeGenerationOptions opt, string adapter)
123123
returnadapter+AssemblyName;
124124
}
125125

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

0 commit comments

Comments
 (0)