Skip to content

Commit 73ebad2

Browse files
authored
[generator] Support Buffer.duplicate() binding in API-34 (#1086)
Context: dotnet/android#7796 Context: 22d5687 Context: 5a0e37e Imagine the following covariant-return-type -using construct, which is new in API-34 `android.jar`: // Java public abstract class Buffer { public abstract Buffer duplicate (); } public abstract class CharBuffer extends Buffer { public abstract CharBuffer duplicate (); } By default this is bound as: // C# [Register(…)] public abstract partial class Buffer { [Register(…)] public abstract Buffer Duplicate (); } [Register(…)] public abstract partial class CharBuffer : Buffer { [Register(…)] public abstract CharBuffer Duplicate (); } Alas, this results in [error CS0533][0]: error CS0533: 'CharBuffer.Duplicate()' hides inherited abstract member 'Buffer.Duplicate()' C# supports this semantic if we use [`abstract override`][1], similar to [reabstraction of Default Interface Methods][2] in 22d5687: public abstract class CharBuffer : Buffer { public override abstract CharBuffer Duplicate (); } Theoretically, we can tell `generator` how to fix this via the `//attr[@name='managedOverride']` metadata property (5a0e37e): <attr path="/api/package[@name='java.nio']/class[@name='CharBuffer']/method[@name='duplicate']" name="managedOverride">override</attr> However, `//attr[@name='managedOverride']` was not written to support updating`abstract` methods. Fix that oversight so that using `//attr[@name='managedOverride']` can be used to emit `override abstract`: [Register(…)] public abstract partial class CharBuffer : Buffer { [Register(…)] public override abstract CharBuffer Duplicate (); } ~~ Method Invokers ~~ This gets us halfway there, but there is another issue: method invokers for both the class and base class methods are *also* emitted into the `*Invoker` subclass: partial class BufferInvoker : CharBuffer { public override sealed unsafe Buffer Duplicate() {…} // so far so good… } partial class CharBufferInvoker : CharBuffer { [Register ("duplicate", "()Ljava/nio/Buffer;", "")] public override sealed unsafe Buffer Duplicate() {…} [Register ("duplicate", "()Ljava/nio/CharBuffer;", "")] public override sealed unsafe CharBuffer Duplicate() {…} // uh oh } This results in the following error: error CS0111: Type 'CharBufferInvoker' already defines a member called 'Duplicate' with the same parameter types error CS0508: 'CharBufferInvoker.Duplicate()': return type must be 'CharBuffer' to match overridden member 'CharBuffer.Duplicate()' This perhaps(?) could be something `generator` could detect and prevent, but the feasibility and cost is unknown and expected to be high. Since this is a very rare case, we will fix it with metadata, however we have never had metadata to apply to invokers before. We have decided to support `//attr[@name='skipInvokerMethods']` metadata on the `class` that the invoker class would be created for: <attr path="/api/package[@name='java.nio']/class[@name='CharBuffer']" name="skipInvokerMethods">java/nio/Buffer.duplicate()Ljava/nio/Buffer;</attr> The value is a comma, space, and newline-separated list of "JNI signature-like" values consisting of: 1. The simplified JNI name of the declaring class that contains the the invoker method to skip, e.g. `java/nio/Buffer` 2. `.` 3. The Java name of the invoker method to skip, e.g. `duplicate`. 4. The JNI method signature of the method to skip, e.g. `()Ljava/nio/Buffer;` The above `java/nio/Buffer.duplicate()Ljava/nio/Buffer;` value tells `generator` to skip generating the `Buffer Buffer.duplicate ()` invoker method when generating the `CharBufferInvoker` type. Multiple invokers to skip can be specified as a comma or space separated list. [0]: https://learn.microsoft.com/en-us/dotnet/csharp/misc/cs0533 [1]: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/classes#1467-abstract-methods [2]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/default-interface-methods#reabstraction
1 parent 77800dd commit 73ebad2

File tree

6 files changed

+75
-7
lines changed

6 files changed

+75
-7
lines changed

src/Xamarin.SourceWriter/Models/MethodWriter.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ public virtual void WriteSignature (CodeWriter writer)
8888

8989
if (IsOverride)
9090
writer.Write ("override ");
91-
else if (IsVirtual)
91+
92+
if (IsVirtual)
9293
writer.Write ("virtual ");
9394
else if (IsAbstract)
9495
writer.Write ("abstract ");

tests/generator-Tests/Unit-Tests/CodeGeneratorTests.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,30 @@ public void ManagedOverrideMethod_Override ()
150150
Assert.True (writer.ToString ().Contains ("public override unsafe int DoStuff ()"), $"was: `{writer.ToString ()}`");
151151
}
152152

153+
[Test]
154+
public void ManagedOverrideAbstractMethod_Override ()
155+
{
156+
var xml = @"<api>
157+
<package name='java.lang' jni-name='java/lang'>
158+
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
159+
</package>
160+
<package name='com.xamarin.android' jni-name='com/xamarin/android'>
161+
<class abstract='false' deprecated='not deprecated' extends='java.lang.Object' extends-generic-aware='java.lang.Object' jni-extends='Ljava/lang/Object;' final='false' name='MyClass' static='false' visibility='public' jni-signature='Lcom/xamarin/android/MyClass;'>
162+
<method abstract='true' deprecated='not deprecated' final='true' name='DoStuff' jni-signature='()I' bridge='false' native='false' return='int' jni-return='I' static='false' synchronized='false' synthetic='false' visibility='public' managedOverride='override'></method>
163+
</class>
164+
</package>
165+
</api>";
166+
167+
var gens = ParseApiDefinition (xml);
168+
var klass = gens.Single (g => g.Name == "MyClass");
169+
170+
generator.Context.ContextTypes.Push (klass);
171+
generator.WriteType (klass, string.Empty, new GenerationInfo ("", "", "MyAssembly"));
172+
generator.Context.ContextTypes.Pop ();
173+
174+
Assert.True (writer.ToString ().Contains ("public override abstract int DoStuff ();"), $"was: `{writer}`");
175+
}
176+
153177
[Test]
154178
public void ManagedOverrideMethod_None ()
155179
{
@@ -298,6 +322,36 @@ public void ManagedOverrideInterfaceProperty_Reabstract ()
298322
Assert.True (writer.ToString ().Contains ("abstract int Name {"), $"was: `{writer}`");
299323
}
300324

325+
[Test]
326+
public void SkipInvokerMethodsMetadata ()
327+
{
328+
var xml = @"<api>
329+
<package name='java.lang' jni-name='java/lang'>
330+
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
331+
</package>
332+
<package name='com.xamarin.android' jni-name='com/xamarin/android'>
333+
<class abstract='true' deprecated='not deprecated' extends='java.lang.Object' extends-generic-aware='java.lang.Object' jni-extends='Ljava/lang/Object;' final='false' name='MyBaseClass' static='false' visibility='public' jni-signature='Lcom/xamarin/android/MyBaseClass;'>
334+
<method abstract='true' deprecated='not deprecated' final='false' name='doStuff' jni-signature='()Lcom/xamarin/android/MyBaseClass;' bridge='false' native='false' return='com.xamarin.android.MyBaseClass' jni-return='Lcom/xamarin/android/MyBaseClass;' static='false' synchronized='false' synthetic='false' visibility='public'></method>
335+
</class>
336+
<class abstract='true' deprecated='not deprecated' extends='com.xamarin.android.MyBaseClass' extends-generic-aware='com.xamarin.android.MyBaseClass' jni-extends='Lcom/xamarin/android/MyBaseClass;' final='false' name='MyClass' static='false' visibility='public' jni-signature='Lcom/xamarin/android/MyClass;' skipInvokerMethods='com/xamarin/android/MyBaseClass.doStuff()Lcom/xamarin/android/MyBaseClass;'>
337+
<method abstract='true' deprecated='not deprecated' final='false' name='doStuff' jni-signature='()Lcom/xamarin/android/MyClass;' bridge='false' native='false' return='com.xamarin.android.MyClass' jni-return='Lcom/xamarin/android/MyClass;' static='false' synchronized='false' synthetic='false' visibility='public' managedOverride='override' return-not-null='true'></method>
338+
</class>
339+
</package>
340+
</api>";
341+
342+
var gens = ParseApiDefinition (xml);
343+
var klass = gens.Single (g => g.Name == "MyClass");
344+
345+
generator.Context.ContextTypes.Push (klass);
346+
generator.WriteType (klass, string.Empty, new GenerationInfo ("", "", "MyAssembly"));
347+
generator.Context.ContextTypes.Pop ();
348+
349+
// `override abstract` causes both invoker methods to get generated. We use metadata
350+
// to suppress the base class's method to prevent a conflict.
351+
Assert.True (writer.ToString ().Contains ("public override abstract Com.Xamarin.Android.MyClass DoStuff ();"), $"was: `{writer}`");
352+
Assert.False (writer.ToString ().Contains ("public abstract Com.Xamarin.Android.MyBaseClass DoStuff ();"), $"was: `{writer}`");
353+
}
354+
301355
[Test]
302356
public void WriteDuplicateInterfaceEventArgs ()
303357
{

tools/generator/Java.Interop.Tools.Generator.Importers/XmlApiImporter.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ public static ClassGen CreateClass (XElement pkg, XElement elem, CodeGenerationO
116116
!options.SupportNestedInterfaceTypes
117117
};
118118

119+
if (elem.Attribute ("skipInvokerMethods")?.Value is string skip)
120+
foreach (var m in skip.Split (new char [] { ',', ' ', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries))
121+
klass.SkippedInvokerMethods.Add (m);
122+
119123
FillApiSince (klass, pkg, elem);
120124
SetLineInfo (klass, elem, options);
121125

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ namespace MonoDroid.Generation
1111
public class ClassGen : GenBase
1212
{
1313
bool fill_explicit_implementation_started;
14+
HashSet<string> skipped_invoker_methods;
1415

1516
public List<Ctor> Ctors { get; private set; } = new List<Ctor> ();
1617

@@ -355,6 +356,8 @@ public override void ResetValidation ()
355356
base.ResetValidation ();
356357
}
357358

359+
public HashSet<string> SkippedInvokerMethods => skipped_invoker_methods ??= new HashSet<string> ();
360+
358361
public override string ToNative (CodeGenerationOptions opt, string varname, Dictionary<string, string> mappings = null)
359362
{
360363
if (opt.CodeGenerationTarget == CodeGenerationTarget.JavaInterop1) {

tools/generator/SourceWriters/BoundMethodAbstractDeclaration.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ public BoundMethodAbstractDeclaration (GenBase gen, Method method, CodeGeneratio
4444

4545
NewFirst = true;
4646

47+
if (method.ManagedOverride?.ToLowerInvariant () == "override")
48+
IsOverride = true;
49+
4750
if (opt.CodeGenerationTarget != CodeGenerationTarget.JavaInterop1) {
4851
method_callback = new MethodCallback (impl, method, opt, null, method.IsReturnCharSequence);
4952
}

tools/generator/SourceWriters/ClassInvokerClass.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,21 +64,21 @@ public ClassInvokerClass (ClassGen klass, CodeGenerationOptions opt)
6464
Properties.Add (new ThresholdTypeGetter ());
6565
}
6666

67-
AddMemberInvokers (klass, opt, new HashSet<string> ());
67+
AddMemberInvokers (klass, opt, new HashSet<string> (), klass.SkippedInvokerMethods);
6868
}
6969

70-
void AddMemberInvokers (ClassGen klass, CodeGenerationOptions opt, HashSet<string> members)
70+
void AddMemberInvokers (ClassGen klass, CodeGenerationOptions opt, HashSet<string> members, HashSet<string> skipInvokers)
7171
{
7272
AddPropertyInvokers (klass, klass.Properties, members, opt);
73-
AddMethodInvokers (klass, klass.Methods, members, null, opt);
73+
AddMethodInvokers (klass, klass.Methods, members, skipInvokers, null, opt);
7474

7575
foreach (var iface in klass.GetAllDerivedInterfaces ()) {
7676
AddPropertyInvokers (klass, iface.Properties.Where (p => !klass.ContainsProperty (p.Name, false, false)), members, opt);
77-
AddMethodInvokers (klass, iface.Methods.Where (m => (opt.SupportDefaultInterfaceMethods || !m.IsInterfaceDefaultMethod) && !klass.ContainsMethod (m, false, false) && !klass.IsCovariantMethod (m) && !klass.ExplicitlyImplementedInterfaceMethods.Contains (m.GetSignature ())), members, iface, opt);
77+
AddMethodInvokers (klass, iface.Methods.Where (m => (opt.SupportDefaultInterfaceMethods || !m.IsInterfaceDefaultMethod) && !klass.ContainsMethod (m, false, false) && !klass.IsCovariantMethod (m) && !klass.ExplicitlyImplementedInterfaceMethods.Contains (m.GetSignature ())), members, skipInvokers, iface, opt);
7878
}
7979

8080
if (klass.BaseGen != null && klass.BaseGen.FullName != "Java.Lang.Object")
81-
AddMemberInvokers (klass.BaseGen, opt, members);
81+
AddMemberInvokers (klass.BaseGen, opt, members, skipInvokers);
8282
}
8383

8484
void AddPropertyInvokers (ClassGen klass, IEnumerable<Property> properties, HashSet<string> members, CodeGenerationOptions opt)
@@ -100,9 +100,12 @@ void AddPropertyInvokers (ClassGen klass, IEnumerable<Property> properties, Hash
100100
}
101101
}
102102

103-
void AddMethodInvokers (ClassGen klass, IEnumerable<Method> methods, HashSet<string> members, InterfaceGen gen, CodeGenerationOptions opt)
103+
void AddMethodInvokers (ClassGen klass, IEnumerable<Method> methods, HashSet<string> members, HashSet<string> skipInvokers, InterfaceGen gen, CodeGenerationOptions opt)
104104
{
105105
foreach (var m in methods) {
106+
if (skipInvokers.Contains ($"{m.DeclaringType.RawJniName}.{m.JavaName}{m.JniSignature}"))
107+
continue;
108+
106109
var sig = m.GetSignature ();
107110

108111
if (members.Contains (sig))

0 commit comments

Comments
 (0)