|
| 1 | +--- |
| 2 | +layout: singlepage-overview |
| 3 | +title: Binary Compatibility for library authors |
| 4 | + |
| 5 | +discourse: true |
| 6 | +permalink: /tutorials/:title.html |
| 7 | +--- |
| 8 | + |
| 9 | +## Introduction |
| 10 | + |
| 11 | +A diverse and comprehensive set of libraries is important to any productive software ecosystem. While it is easy to develop and distribute Scala libraries, good library authorship goes far |
| 12 | +beyond just writing code and publishing them. |
| 13 | + |
| 14 | +In this guide, we will cover the important topic of **Binary Compatibility**: |
| 15 | + |
| 16 | +* How binary incompatibility can cause production failures in your applications |
| 17 | +* How library authors can avoid breaking binary compatibility, and/or convey breakages clearly to library users when they happen |
| 18 | + |
| 19 | +Before we start, first we need to understand how code is compiled and executed on the Java Virtual Machine (JVM). |
| 20 | + |
| 21 | +## The JVM execution model |
| 22 | + |
| 23 | +Code compiled to run on the JVM is compiled to a platform-independent format called **JVM bytecode** and stored in **Class File** format (with `.class` extension) and these class files are stored |
| 24 | +in JAR files. The bytecode is what we refer to as the **Binary** format. |
| 25 | + |
| 26 | +When application or library code is compiled, their bytecode invokes named references of classes/methods from their dependencies instead of including the dependencies' actual bytecode |
| 27 | +(unless inlining is explicitly requested). During runtime, the JVM classloader will search through the provided class files for classes/methods referenced by and invoke them. |
| 28 | + |
| 29 | +Let's illustrate with an example: |
| 30 | + |
| 31 | +We got an application `App` that depends on `A` which itself depends on library `C`. When starting the application we need to provide the class files |
| 32 | +for all of `App`, `A` and `C` (something like `java -cp App.jar:A.jar:C.jar:. MainClass`). If we did not provide `C.jar`, or if we provided a `C.jar` that does not contain certain classes/methods |
| 33 | +which `A` expected to exist in library `C`, we will get an exception when our code attempt to invoke the missing classes/methods. |
| 34 | + |
| 35 | +This is what we call **Binary Incompatibility Errors** - The bytecode interface used for compilation differs and is incompatible with the bytecode provided during runtime. |
| 36 | + |
| 37 | +## What are Evictions, Source Compatibility and Binary Compatibility? |
| 38 | + |
| 39 | +Since the classloader only loads the first match of a class, having multiple versions of the same library in the classpath is redundant. |
| 40 | +Therefore when deciding which JARs to use for compilation, SBT only selects one version from each library (by default the highest), |
| 41 | +and all other versions of the same library are **evicted**. When packaging applications, the same versions of libraries that was used for compiling the |
| 42 | +application is packaged and used during runtime. |
| 43 | + |
| 44 | +Two library versions are said to be **Source Compatible** if switching one for the other does not incur any compile errors. For example, If we can switch from `v1.0.0` of a dependency to `v2.0.0` and |
| 45 | +recompile our code without causing any compilation errors, `v2.0.0` is said to be source compatible with `v1.0.0`. |
| 46 | + |
| 47 | +Two library versions are said to be **Binary Compatible** if the compiled bytecode of these versions are compatible. Using the example above, removing a class will render two version |
| 48 | +binary incompatible too, as the compiled bytecode for v2.0.0 will no longer contain the removed class. |
| 49 | + |
| 50 | +When talking about two versions being compatible, the direction matters too. If we can use `v2.0.0` in place of `v1.0.0`, `v2.0.0` is said to be **backwards compatible** with `v1.0.0`. Conversely, |
| 51 | +if we say that any library release of `v1.x.x` will be forwards compatible, we can use `v1.0.0` anywhere where `v1.1.0` was originally used. |
| 52 | +For the rest of the guide, when the 'compatible' is used we mean backwards compatible, as it is the more common case of compatibility guarantee. |
| 53 | + |
| 54 | +An important note to make is that while breaking source compatibility normally results in breaking binary compatibility, they are actually orthorgonal |
| 55 | +(breaking one does not imply breaking the other). See below for more examples (TODO: make sure we have examples?) |
| 56 | +TODO: more facts? |
| 57 | + |
| 58 | +## Why binary compatibility matters |
| 59 | + |
| 60 | +Let's look at an example where binary incompatibility between versions of a library can have catastrophic consequences: |
| 61 | + |
| 62 | +Our application depends on library `A` and `B`. Both `A` and `B` depends on library `C`. Initially both `A` and `B` depends on `C v1.0.0`. |
| 63 | + |
| 64 | +{: style="width: 280px; margin: auto; display: block;"} |
| 65 | + |
| 66 | +Some time later, we see `B v1.1.0` is now available and we upgraded the version in our `build.sbt`. Our code compiles and seems to work so we push it to production and goes home for dinner. |
| 67 | + |
| 68 | +Unfortunately at 2am, we got frantic calls from customers saying that our App is broken! Looking at the logs, you find lots of `NoSuchMethodError` is being thrown by some code in `A`! |
| 69 | + |
| 70 | +{: style="width: 280px; margin: auto; display: block;"} |
| 71 | + |
| 72 | +Why did we get a `NoSuchMethodError`? Remember that `A v1.0.0` is compiled with `C v1.0.0` and thus calls methods availble in `Cv1.0.0`. While `B` and |
| 73 | +our App has been recompiled with available classes/methods in `C v2.0.0`, `A v1.0.0`'s bytecode hasn't changed - it still calls the same method that is now missing in `C v2.0.0`! |
| 74 | + |
| 75 | +This situation can only be resolved by ensuring that the chosen version of `C` is binary compatible with all other evicted versions of `C`. In this case, we need a new version of `A` that depends |
| 76 | +on `C v2.0.0` (or any other future `C` version that is binary compatible with `C v2.0.0`). |
| 77 | + |
| 78 | +Now imagine if our App is more complex with lots of dependencies themselves depending on `C` (either directly or transitively) - it becomes extremely difficult to upgrade any dependencies because it now |
| 79 | +pulls in a version of `C` that is incompatible with the rest of the versions of `C` in our dependency tree! In the example below, we cannot upgrade `D` because it will transitively pull in `C v2.0.0`, causing breakages |
| 80 | +due to binary incompatibility. This inability to upgrade any packages without breaking anything is common known as **Dependency Hell**. |
| 81 | + |
| 82 | + |
| 83 | + |
| 84 | +How can we, as library authors, spare our users of runtime errors and dependency hell? |
| 85 | + |
| 86 | +* Use **Migration Manager** (MiMa) to catch unintended binary compatibility breakages before releasing a new library version |
| 87 | +* **Avoid breaking binary compatibility** through careful design and evolution of your library interfaces |
| 88 | +* Communicate binary compatibility breakages clearly through **versioning** |
| 89 | + |
| 90 | +## MiMa - Check Binary Compatibility with Previous Library Versions |
| 91 | + |
| 92 | +The [Migration Manager for Scala](https://github.com/typesafehub/migration-manager) (MiMa) is a tool for diagnosing binary incompatibilities between different library versions. |
| 93 | + |
| 94 | +When run standalone in the command line, it will compare the .class files in the two provided JARs and report any binary incompatibilities found. Most library authors use the [SBT plugin](https://github.com/typesafehub/migration-manager/wiki/Sbt-plugin) |
| 95 | +to help spot binary incompatibility between library releases. (Follow the link for instructions on how to use it in your project) |
| 96 | + |
| 97 | +## Designing for Evolution - without breaking binary compatibility |
| 98 | + |
| 99 | +TODO |
| 100 | + |
| 101 | +## Versioning Scheme - Communicate binary compatiblity breakages |
| 102 | + |
| 103 | +We recommend using the following schemes to communicate binary and source compatibility to your users: |
| 104 | + |
| 105 | +* Any release with the same major version are **Binary Backwards Compatible** with each other |
| 106 | +* A minor version bump signals new features and **may contain minor source incompatibilities** that can be easily fixed by the end user |
| 107 | +* Patch version for bugfixes and minor behavioural changes |
| 108 | +* For **expreimental library versions** (where the major version is `0`, such as `v0.1.0`), a minor version bump **may contain both source and binary breakages** |
| 109 | +* Some libraries may take a harder stance on maintaining source compatibility, bumping the major version number for ANY source incompatibility even if they are binary compatible |
| 110 | + |
| 111 | +Some examples: |
| 112 | + |
| 113 | +* `v1.0.0 -> v2.0.0` is <span style="color: red">binary incompatible</span>. Cares needs to be taken to make sure no evicted versions are still in the `v1.x.x` range to avoid runtime errors |
| 114 | +* `v1.0.0 -> v1.1.0` is <span style="color: blue">binary compatible</span> and maybe source incompatible |
| 115 | +* `v1.0.0 -> v1.0.1` is <span style="color: blue">binary compatible</span> and source compatible |
| 116 | +* `v0.4.0 -> v0.5.0` is <span style="color: red">binary incompatible</span> and maybe source incompatible |
| 117 | +* `v0.4.0 -> v0.4.1` is <span style="color: blue">binary compatible</span> and source compatible |
| 118 | + |
| 119 | +Many libraries in the Scala ecosystem has adopted this versioning scheme. A few examples are [Akka](http://doc.akka.io/docs/akka/2.5/scala/common/binary-compatibility-rules.html), |
| 120 | +[Cats](https://github.com/typelevel/cats#binary-compatibility-and-versioning) and [Scala.js](https://www.scala-js.org/). |
| 121 | + |
| 122 | +### Explanation |
| 123 | + |
| 124 | +Why do we use the major version number to signal binary compatibility releases? |
| 125 | + |
| 126 | +From our [example](#why-binary-compatibility-matters) above, we have learnt two important lessons: |
| 127 | + |
| 128 | +* Binary incompatibility releases often leads to dependency hell, rendering your users unable to update any of their libraries without breaking their application |
| 129 | +* If a new library version is binary compatible but source incompatible, the user can simply fix the compile errors in their application and everything will work |
| 130 | + |
| 131 | +Therefore, **binary incompatible releases should be avoided if possible** and be more noticeable when they happen, warranting the use of the major version number. While source compatibility |
| 132 | +is also important, if they are minor breakages that does not require effort to fix, then it is best to let the major number signal just binary compatibility. |
| 133 | + |
| 134 | +## Conclusion |
| 135 | + |
| 136 | +In this guide we covered the importance of binary compatibility and showed you a few tricks to avoid breaking binary compatibility. Finally, we laid out a versioning scheme to communicate |
| 137 | +binary compatibility breakages clearly to your users. |
| 138 | + |
| 139 | +If we follow these guidelines, we as a community can spend less time untangling dependency hell and more time making cool things! |
| 140 | + |
0 commit comments