|
| 1 | +# `Mono.Android.dll` API Compatibility |
| 2 | + |
| 3 | +[Historically](#history), there was a separate `Mono.Android.dll` for each |
| 4 | +`$(TargetFrameworkVersion)` value, and these assemblies were *not* completely |
| 5 | +API compatible with each other. |
| 6 | + |
| 7 | +Starting with `$(TargetFrameworkVersion)` v10.0, we will no longer provide a |
| 8 | +separate `Mono.Android.dll` per API level. Instead, all *stable* bindings of |
| 9 | +future API levels will reside in the same `Mono.Android.dll`. |
| 10 | + |
| 11 | +This "single" `Mono.Android.dll` will require C# 8 features. |
| 12 | + |
| 13 | + |
| 14 | +# Preserving Compatibility |
| 15 | + |
| 16 | +Java and C# have different ideas about what constitutes a change which is |
| 17 | +source- and binary-compatible. Consequently, the `Mono.Android.dll` binding |
| 18 | +needs to do extra work to bridge these gaps. |
| 19 | + |
| 20 | + |
| 21 | +## Added Required Interface Methods |
| 22 | + |
| 23 | +It is an *binary* compatible change to add non-`default` methods to a Java |
| 24 | +interface. `Mono.Android.dll` will support this by using C#8 default |
| 25 | +interface methods which try to invoke the underlying Java method or |
| 26 | +throw `Java.Lang.AbstractMethodError()`. |
| 27 | + |
| 28 | +For example, given Java API-*X*: |
| 29 | + |
| 30 | +```java |
| 31 | +package example; |
| 32 | + |
| 33 | +public interface Fooable { |
| 34 | + void foo(); |
| 35 | +} |
| 36 | +``` |
| 37 | + |
| 38 | +which is then updated for API-*Y*: |
| 39 | + |
| 40 | +```java |
| 41 | +package example; |
| 42 | + |
| 43 | +public interface Fooable { |
| 44 | + void foo(); |
| 45 | + void bar(); |
| 46 | +} |
| 47 | +``` |
| 48 | + |
| 49 | +Then the API-*Y* binding will be: |
| 50 | + |
| 51 | +```csharp |
| 52 | +namespace Example { |
| 53 | + public partial interface IFooable : IJavaObject, IJavaPeerable { |
| 54 | + [Register ("foo", "()V", "…")] |
| 55 | + void Foo(); |
| 56 | + |
| 57 | + [Register ("bar", "()V", "…")] |
| 58 | + void Bar() |
| 59 | + { |
| 60 | + const string __id = "bar.()V"; |
| 61 | + try { |
| 62 | + var __rm = _members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, null); |
| 63 | + } |
| 64 | + catch (Java.Lang.NoSuchMethodError e) { |
| 65 | + throw new Java.Lang.AbstractMethodError (__id); |
| 66 | + } |
| 67 | + } |
| 68 | + } |
| 69 | +} |
| 70 | +``` |
| 71 | + |
| 72 | +Question: will checking for `NoSuchMethodError` actually work with |
| 73 | +new required interface methods? |
| 74 | + |
| 75 | + |
| 76 | +<a name="added-abstract-methods"> |
| 77 | + |
| 78 | +## Added Abstract Methods |
| 79 | + |
| 80 | +When a Java class adds a new `abstract` method, the binding needs to alter |
| 81 | +the method to instead be `virtual`, with an implementation which eventually |
| 82 | +throws `AbstractMethodError`: |
| 83 | + |
| 84 | +```java |
| 85 | +package example; |
| 86 | + |
| 87 | +public abstract class Foo { |
| 88 | + // Abstract in API-X |
| 89 | + public abstract void a(); |
| 90 | + |
| 91 | + // Added in API-Y |
| 92 | + public abstract void b(); |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +will be bound as: |
| 97 | + |
| 98 | +```csharp |
| 99 | +namespace Example { |
| 100 | + public abstract partial class Foo { |
| 101 | + public abstract void A(); |
| 102 | + |
| 103 | + public virtual void B() |
| 104 | + { |
| 105 | + const string __id = "b.()V"; |
| 106 | + try { |
| 107 | + // Works on API levels which (1) declare the method, and |
| 108 | + // (2) the method is overridden in the runtime type of `this`. |
| 109 | + var __rm = _members.InstanceMethods.InvokeVirtuaVoidMethod (__id, this, null); |
| 110 | + } |
| 111 | + catch (Java.Lang.NoSuchMethodError e) { |
| 112 | + // Triggered on API levels which don't contain the method |
| 113 | + throw new Java.Lang.AbstractMethodError (__id); |
| 114 | + } |
| 115 | + } |
| 116 | + } |
| 117 | +} |
| 118 | +``` |
| 119 | + |
| 120 | + |
| 121 | +## Added Covariant Return Types |
| 122 | + |
| 123 | +When a base class adds a method which is overridden in a derived class, |
| 124 | +Java may make use of covariant return types. For example, in API-29 |
| 125 | +the [`android.telephony.CellInfo`][cellinfo] and |
| 126 | +[`android.telephony.CellInfoCdma`][celllinfocdma] types: |
| 127 | + |
| 128 | +[cellinfo]: https://developer.android.com/reference/android/telephony/CellInfo |
| 129 | +[celllinfocdma]: https://developer.android.com/reference/android/telephony/CellInfoCdma |
| 130 | + |
| 131 | +```java |
| 132 | +package android.telephony; |
| 133 | + |
| 134 | +public abstract class CellInfo { |
| 135 | +} |
| 136 | + |
| 137 | +public class CellInfoCdma extends CellInfo { |
| 138 | + public CellIdentityCdma getCellIdentity() {…} |
| 139 | +} |
| 140 | + |
| 141 | +public abstract class CellIdentity {…} |
| 142 | +public class CellIdentityCdma extends CellIdentity {…} |
| 143 | +``` |
| 144 | + |
| 145 | +is bound as: |
| 146 | + |
| 147 | +```csharp |
| 148 | +namespace Android.Telephony { |
| 149 | + public abstract partial class CellInfo { |
| 150 | + } |
| 151 | + |
| 152 | + public sealed partial class CellInfoCdma : CellInfo { |
| 153 | + public CellIdentityCdma CellIdentity {get;} |
| 154 | + } |
| 155 | + |
| 156 | + public abstract partial class CellIdentity {} |
| 157 | + public partial class CellIdentityCdma : CellIdentity {} |
| 158 | +} |
| 159 | +``` |
| 160 | + |
| 161 | +In [API-R Developer Preview 1][cellinfo-r-dp1], `CellInfo` is updated: |
| 162 | + |
| 163 | +[cellinfo-r-dp1]: https://developer.android.com/sdk/api_diff/r-dp1/changes/android.telephony.CellInfo |
| 164 | + |
| 165 | +```java |
| 166 | +package android.telephony; |
| 167 | + |
| 168 | +public class CellInfo { |
| 169 | + public abstract CellIdentity getCellIdentity(); |
| 170 | +} |
| 171 | +``` |
| 172 | + |
| 173 | +Note that this is a new `abstract` method. As per the |
| 174 | +[Added Abstract Methods](#added-abstract-methods) section, |
| 175 | +`CellInfo.getCellIdentity()` will need to be bound as a *`virtual`* property: |
| 176 | + |
| 177 | +```csharp |
| 178 | +namespace Android.Telephony { |
| 179 | + // API-R binding, take 1 |
| 180 | + public abstract partial class CellInfo { |
| 181 | + public virtual CellIdentity CellIdentity { |
| 182 | + get => …; |
| 183 | + } |
| 184 | + } |
| 185 | +} |
| 186 | +``` |
| 187 | + |
| 188 | +However, this is incomplete, for two reasons: |
| 189 | + |
| 190 | + 1. `CellInfoCdma.CellIdentity` will issue a CS0114 warning, as it hides the |
| 191 | + added `CellInfo.CellIdentity` property. Note that we cannot change the |
| 192 | + type of `CellInfoCdma.CellIdentity`, as that would break the C# API. |
| 193 | + |
| 194 | + 2. At runtime, the expectation is that invoking the `CellInfo.CellIdentity` |
| 195 | + property on a `CellInfoCdma` instance *won't* throw `AbstractMethodError`, |
| 196 | + as it is "overridden" in the Java-side `CellInfoCdma`. |
| 197 | + |
| 198 | +(2) is implicitly handled, if you squint "just right", by having the binding |
| 199 | +of `CellInfo.CellIdentity` invoke the Java-side method. If the runtime type |
| 200 | +is e.g. `CellInfoCdma`, then this will hit `CellInfoCdma.getCellIdentity()`, |
| 201 | +which will return the same instance as the `CellInfo.CellIdentity` property. |
| 202 | + |
| 203 | +```csharp |
| 204 | +namespace Android.Telephony { |
| 205 | + // API-R binding, final |
| 206 | + public abstract partial class CellInfo { |
| 207 | + public virtual CellIdentity CellIdentity { |
| 208 | + get { |
| 209 | + const string __id = "getCellIdentity.()Landroid/telephony/CellIdentity;"; |
| 210 | + try { |
| 211 | + var __rm = _members.InstanceMethods.InvokeVirtualObjectMethod (__id, this, null); |
| 212 | + return global::Java.Lang.Object.GetObject<Android.Telephony.CellIdentity> (__rm.Handle, JniHandleOwnership.TransferLocalRef); |
| 213 | + } |
| 214 | + catch (Java.Lang.NoSuchMethodError e) { |
| 215 | + throw new Java.Lang.AbstractMethodError (__id); |
| 216 | + } |
| 217 | + } |
| 218 | + } |
| 219 | + } |
| 220 | + |
| 221 | + public sealed partial class CellInfoCdma : CellInfo { |
| 222 | + public new CellIdentityCdma CellIdentity {get;} |
| 223 | + } |
| 224 | +} |
| 225 | +``` |
| 226 | + |
| 227 | + |
| 228 | +<a name="history"> |
| 229 | + |
| 230 | +# History |
| 231 | + |
| 232 | +Java and C# have different ideas about what constitutes a change which is |
| 233 | +source- and binary-compatible. In Java, it is ABI compatible to add new |
| 234 | +required methods to an interface and to abstract classes. This is not |
| 235 | +*source* compatible -- the newly required methods will need to be added when |
| 236 | +recompiling code -- but it's *binary* compatible, and this is something that |
| 237 | +Android has frequently made use of. |
| 238 | + |
| 239 | +Consider the [`android.database.Cursor`][cursor] interface, which changed in: |
| 240 | + |
| 241 | + * [API-19][cursor-api-19], adding [`Cursor.getNotificationUri()`][cgnu]. |
| 242 | + * [API-29][cursor-api-29], adding [`Cursor.getNotificationUris()`][cgnus] |
| 243 | + and [`Cursor.setNotificationUris()][csnus]. |
| 244 | + |
| 245 | +The `Cursor` interface is bound as [`Android.Database.ICursor][icursor], and |
| 246 | +before C# 8 it was not possible to add members to a C# interface. |
| 247 | + |
| 248 | +How were these new Java members supported? |
| 249 | + |
| 250 | +Such changes were supported by *breaking* API, and using a *new* |
| 251 | +`$(TargetFrameworkVersion)` to contain the updated API. `Mono.Android.dll` |
| 252 | +from v4.3 (API-18) would not contain a binding for |
| 253 | +`Cursor.getNotificationUri()`, while the binding in v9.0 (API-28) would not |
| 254 | +contain bindings for `Cursor.getNotificationUris()` and |
| 255 | +`Cursor.setNotificationUris()`. |
| 256 | + |
| 257 | +*So long as* the `$(TargetFrameworkVersion)` value didn't change, source code |
| 258 | +would continue to compile without any errors. Changing the |
| 259 | +`$(TargetFrameworkVersion)` value *may* result in new compiler errors due to |
| 260 | +added members not being implemented, and we considered this acceptable. |
| 261 | + |
| 262 | +(The alternative was to bind *no* new methods after the initial binding, which |
| 263 | +we didn't consider acceptable.) |
| 264 | + |
| 265 | +Binary compatibility -- using a library built against an older binding with |
| 266 | +an app using a newer binding -- was preserved by using a [linker step][linker-fix] |
| 267 | +which |
| 268 | +would look for "missing" abstract methods and insert them so that they would |
| 269 | +throw `AbstractMethodError`. |
| 270 | + |
| 271 | + |
| 272 | +[cursor]: https://developer.android.com/reference/android/database/Cursor |
| 273 | +[cursor-api-19]: https://developer.android.com/sdk/api_diff/19/changes/android.database.Cursor |
| 274 | +[cursor-api-29]: https://developer.android.com/sdk/api_diff/29/changes/android.database.Cursor |
| 275 | +[cgnu]: https://developer.android.com/reference/android/database/Cursor.html#getNotificationUri() |
| 276 | +[cgnus]: https://developer.android.com/reference/android/database/Cursor.html#getNotificationUris() |
| 277 | +[csnus]: https://developer.android.com/reference/android/database/Cursor.html#setNotificationUris(android.content.ContentResolver,%20java.util.List%3Candroid.net.Uri%3E) |
| 278 | +[icursor]: https://docs.microsoft.com/en-us/dotnet/api/android.database.icursor?view=xamarin-android-sdk-9 |
| 279 | +[linker-fix]: https://github.com/xamarin/xamarin-android/commit/f96fcf93e157472072576bcc0a8698302899e8cf |
0 commit comments