Commit 53bfb4a
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- tests/generator-Tests/Unit-Tests
- tools/generator
- Java.Interop.Tools.Generator.Importers
- Java.Interop.Tools.Generator.ObjectModel
- SourceWriters/Extensions
5 files changed
+60
-0
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
352 | 352 | | |
353 | 353 | | |
354 | 354 | | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
355 | 379 | | |
356 | 380 | | |
357 | 381 | | |
| |||
Lines changed: 24 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
490 | 490 | | |
491 | 491 | | |
492 | 492 | | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
493 | 517 | | |
494 | 518 | | |
Lines changed: 5 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
371 | 371 | | |
372 | 372 | | |
373 | 373 | | |
| 374 | + | |
374 | 375 | | |
375 | 376 | | |
376 | 377 | | |
| |||
384 | 385 | | |
385 | 386 | | |
386 | 387 | | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
387 | 392 | | |
388 | 393 | | |
389 | 394 | | |
| |||
Lines changed: 1 addition & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| 22 | + | |
22 | 23 | | |
23 | 24 | | |
24 | 25 | | |
| |||
Lines changed: 6 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
266 | 266 | | |
267 | 267 | | |
268 | 268 | | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
269 | 275 | | |
270 | 276 | | |
271 | 277 | | |
| |||
0 commit comments