Skip to content

Commit 53bfb4a

Browse files
authored
[generator] Add support for 'compatVirtualMethod'. (#1088)
Fixes: #810 Context: dotnet/android@f96fcf9 Context: https://learn.microsoft.com/en-us/dotnet/core/compatibility/categories#binary-compatibility Context: https://github.com/xamarin/xamarin-android/blob/f3592b3c42674f2161c14d1f4246083a85fe17ab/Documentation/workflow/mono-android-api-compatibility.md Context: dotnet/android@1f4c8be .NET and Java have different behaviors around ABI compatibility when adding new `abstract` methods to types which cross module boundaries. (Within a module boundary, adding a new `abstract` method will be an *API* break and the compiler will not compile the code.) For example, consider "version 1": // Lib.dll; version 1 public abstract partial class LibBase { } // App.exe class MyType : LibBase { } // … Console.WriteLine (new MyType().ToString ()); Then for "version 2" we update `LibBase` in `Lib.dll` to: // Lib.dll; version 2 public abstract partial class LibBase { public abstract void M(); } *We **do not** rebuild `App.exe`*. What Happens™? [.NET says][0] Don't Do That™: > * ❌ DISALLOWED: Adding an abstract member to a public type that has > accessible (public or protected) constructors and that is not sealed If you attempt this anyway, then Bad Things™ happen; a `TypeLoadException` is thrown when attempting to instantiate `MyType`: Unhandled exception. System.TypeLoadException: Method 'M' in type 'MyType' from assembly 'App, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' does not have an implementation. This happens even if the new `abstract` method is not invoked. (MonoVM is -- was? -- worse; it would flat out *abort* the process!) Java, meanwhile [*says* that it breaks compatibility][1]: > Changing a method that is not declared `abstract` to be declared > `abstract` will break compatibility with pre-existing binaries that > previously invoked the method, causing an `AbstractMethodError`. but the *form* of the runtime behavior differs significantly from .NET: In Java, *so long as* the new `abstract` is not invoked, runtime behavior doesn't change. If the new `abstract` method *is* invoked, an `AbstractMethodError` is thrown. In Java, adding `abstract` methods is thus "safe" (-ish): existing subclasses or interface implementations don't need to be recompiled, and if someone *does* invoke the method, `AbstractMethodError` is thrown, which can be caught and handled, if necessary. Consequently, when we look at the history of Android, it is not unusual for new `abstract` methods to be added over time! @jonpryor's "favorite" example is [`android.database.Cursor`][2], which added new `abstract` methods in: * API-11 (`getType()`) * API-19 (`getNotificationUri()`) * API-23 (`setExtras()`) * API-29 (`getNotificationUris()`, `setNotificationUris()`) In "Classic" Xamarin.Android, we "solved" this problem via: 1. "Not caring"; each new Android API would correspond to a new `$(TargetFrameworkVersion)`, which would be a new/different version of `Mono.Android.dll`. Added `abstract` methods would be added, which could result in *API* breakage if/when a project changed their `$(TargetFrameworkVersion)`. 2. *ABI* compatibility was preserved by way of a post-build linker step -- dotnet/android@f96fcf93 -- which would look for "missing" `abstract` methods and *add* them with Cecil. The added method would simply throw `new AbstractMethodError()`. .NET Android still has the post-build linker step, but did not adopt the "Classic" Xamarin.Android approach of having a separate binding assembly per API level. Instead, .NET Android: * For new `abstract` methods in classes, the `abstract` method would be turned into a `virtual` method which throws `AbstractMethodError`. * For new non-default methods in interfaces, [Default Interface Methods][3] would be used to to maintain API and ABI compatibility. The default interface method would throw `AbstractMethodError`. However, this was done manually; see dotnet/android@1f4c8be9. `<remove-node/>` was used to *remove* the new `abstract` method, and a `partial` class declaration was added which contained `generator` output for the "original" `abstract` method declaration, manually patched up to instead introduce a `virtual` method. The default `generator` output of: public partial class CellInfo { public abstract Android.Telephony.CellIdentity CellIdentity { [Register ("getCellIdentity", "()Landroid/telephony/CellIdentity;", "GetGetCellIdentityHandler", ApiSince = 30)] get; } // …plus marshal method invocation infrastructure… } would be manually altered to become: public partial class CellInfo { public unsafe virtual Android.Telephony.CellIdentity CellIdentity { [Register ("getCellIdentity", "()Landroid/telephony/CellIdentity;", "GetGetCellIdentityHandler", ApiSince = 30)] get { const string __id = "getCellIdentity.()Landroid/telephony/CellIdentity;"; try { var __rm = _members.InstanceMethods.InvokeVirtualObjectMethod (__id, this, null); return Object.GetObject<CellIdentity> (__rm.Handle, JniHandleOwnership.TransferLocalRef); } catch (Java.Lang.NoSuchMethodError) { throw new Java.Lang.AbstractMethodError (__id); } } } // Plus all related "marshal method glue code" } This is cumbersome and error prone. Improve on this process by adding support for new `compatVirtualMethod` metadata, a boolean value which can be applied to `//method`: <attr api-since="34" path="/api/package[@name='java.nio']/class[@name='ByteBuffer']/method[@name='slice' and count(parameter)=2]" >true</attr> which updates the [`ByteBuffer.slice(int, int`)][4] method -- a newly introduced `abstract` method in API-34 -- so that it will emit a `virtual` method instead of an `abstract` method, and the `virtual` method body translates `NoSuchMethodError` into `AbstractMethodError`: partial class ByteBuffer { [Register (…)] public virtual unsafe Slice (int index, int length) { const string __id = "slice.(II)Ljava/nio/ByteBuffer;"; try { return … } catch (NoSuchMethodError) { throw new AbstractMethodError (__id); } finally { } } } This allows us to "more reasonably" maintain ABI & API compatibility, without all that pesky manual fixup. [0]: https://learn.microsoft.com/en-us/dotnet/core/compatibility/library-change-rules [1]: https://docs.oracle.com/javase/specs/jls/se7/html/jls-13.html#jls-13.4.16 [2]: https://developer.android.com/reference/android/database/Cursor [3]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/default-interface-methods [4]: https://developer.android.com/reference/java/nio/ByteBuffer#slice(int,%20int)
1 parent 73ebad2 commit 53bfb4a

File tree

5 files changed

+60
-0
lines changed

5 files changed

+60
-0
lines changed

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,30 @@ public void SkipInvokerMethodsMetadata ()
352352
Assert.False (writer.ToString ().Contains ("public abstract Com.Xamarin.Android.MyBaseClass DoStuff ();"), $"was: `{writer}`");
353353
}
354354

355+
[Test]
356+
public void CompatVirtualMethod_Class ()
357+
{
358+
var xml = @"<api>
359+
<package name='java.lang' jni-name='java/lang'>
360+
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
361+
</package>
362+
<package name='com.xamarin.android' jni-name='com/xamarin/android'>
363+
<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;'>
364+
<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' compatVirtualMethod='true'></method>
365+
</class>
366+
</package>
367+
</api>";
368+
369+
var gens = ParseApiDefinition (xml);
370+
var klass = gens.Single (g => g.Name == "MyClass");
371+
372+
generator.Context.ContextTypes.Push (klass);
373+
generator.WriteType (klass, string.Empty, new GenerationInfo ("", "", "MyAssembly"));
374+
generator.Context.ContextTypes.Pop ();
375+
376+
Assert.True (writer.ToString ().NormalizeLineEndings ().Contains ("catch (Java.Lang.NoSuchMethodError) { throw new Java.Lang.AbstractMethodError (__id); }".NormalizeLineEndings ()), $"was: `{writer}`");
377+
}
378+
355379
[Test]
356380
public void WriteDuplicateInterfaceEventArgs ()
357381
{

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,5 +490,29 @@ public void WriteInterfaceFieldAsDimProperty ()
490490

491491
AssertTargetedExpected (nameof (WriteInterfaceFieldAsDimProperty), writer.ToString ());
492492
}
493+
494+
[Test]
495+
public void CompatVirtualMethod_Interface ()
496+
{
497+
var xml = @"<api>
498+
<package name='java.lang' jni-name='java/lang'>
499+
<class abstract='false' deprecated='not deprecated' final='false' name='Object' static='false' visibility='public' jni-signature='Ljava/lang/Object;' />
500+
</package>
501+
<package name='com.xamarin.android' jni-name='com/xamarin/android'>
502+
<interface abstract='true' deprecated='not deprecated' final='false' name='Cursor' static='false' visibility='public' jni-signature='Landroid/database/Cursor;'>
503+
<method abstract='true' deprecated='not deprecated' final='false' name='getNotificationUri' jni-signature='()Ljava/lang/Object;' bridge='false' native='false' return='java.lang.Object' jni-return='Ljava/lang/Object;' static='false' synchronized='false' synthetic='false' visibility='public' compatVirtualMethod='true' />
504+
</interface>
505+
</package>
506+
</api>";
507+
508+
var gens = ParseApiDefinition (xml);
509+
var klass = gens.Single (g => g.Name == "ICursor");
510+
511+
generator.Context.ContextTypes.Push (klass);
512+
generator.WriteType (klass, string.Empty, new GenerationInfo ("", "", "MyAssembly"));
513+
generator.Context.ContextTypes.Pop ();
514+
515+
Assert.True (writer.ToString ().NormalizeLineEndings ().Contains ("catch (Java.Lang.NoSuchMethodError) { throw new Java.Lang.AbstractMethodError (__id); }".NormalizeLineEndings ()), $"was: `{writer}`");
516+
}
493517
}
494518
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@ public static Method CreateMethod (GenBase declaringType, XElement elem, CodeGen
371371
GenericArguments = elem.GenericArguments (),
372372
IsAbstract = elem.XGetAttribute ("abstract") == "true",
373373
IsAcw = true,
374+
IsCompatVirtualMethod = elem.XGetAttribute ("compatVirtualMethod") == "true",
374375
IsFinal = elem.XGetAttribute ("final") == "true",
375376
IsReturnEnumified = elem.Attribute ("enumReturn") != null,
376377
IsStatic = elem.XGetAttribute ("static") == "true",
@@ -384,6 +385,10 @@ public static Method CreateMethod (GenBase declaringType, XElement elem, CodeGen
384385
Visibility = elem.Visibility ()
385386
};
386387

388+
// CompatVirtualMethods aren't abstract
389+
if (method.IsCompatVirtualMethod)
390+
method.IsAbstract = false;
391+
387392
method.IsVirtual = !method.IsStatic && elem.XGetAttribute ("final") == "false";
388393

389394
if (elem.Attribute ("managedName") != null)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public Method (GenBase declaringType) : base (declaringType)
1919
public bool GenerateAsyncWrapper { get; set; }
2020
public bool GenerateDispatchingSetter { get; set; }
2121
public bool IsAbstract { get; set; }
22+
public bool IsCompatVirtualMethod { get; set; }
2223
public bool IsFinal { get; set; }
2324
public bool IsInterfaceDefaultMethod { get; set; }
2425
public Method OverriddenInterfaceMethod { get; set; }

tools/generator/SourceWriters/Extensions/SourceWriterExtensions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,12 @@ public static void AddMethodBody (List<string> body, Method method, CodeGenerati
266266
body.Add ($"\treturn {method.RetVal.ReturnCast}{method.RetVal.FromNative (opt, r, true) + opt.GetNullForgiveness (method.RetVal)};");
267267
}
268268

269+
if (method.IsCompatVirtualMethod) {
270+
body.Add ("}");
271+
body.Add ("catch (Java.Lang.NoSuchMethodError) {");
272+
body.Add (" throw new Java.Lang.AbstractMethodError (__id);");
273+
}
274+
269275
body.Add ("} finally {");
270276

271277
foreach (string cleanup in method.Parameters.GetCallCleanup (opt))

0 commit comments

Comments
 (0)