Native dependency in Kotlin/Multiplatform — part 1 : architecture

Salomon BRYS
Kodein Koders
Published in
4 min readJun 15, 2020
Photo by Alex wong

Other article in this serie:

Kotlin/Multiplatform allows you to run Kotlin code everywhere, whether on the server or client JVM, on Android, on iOS and on the web.
However amazing this technology is, in Kotlin/Multiplatform as in any other technology, language is only one part of the story. A successful ecosystem also means a large enough array of tools to draw from when creating applications.

Such tools may be native libraries. For example, you may want to use OpenCV for image recognition and manipulation, LevelDB for fast key-value data storage, FFmpeg for video manipulation, etc. Each platform offers a way to deal with native code, but Kotlin/Multiplatform offers no common abstraction.

In this suite of post, I want to explore what needs to be done in order to add a native dependency to a Kotlin multi-platform project.

In essence, we will discuss how to create a Kotlin layer of compatibility to the library that works on every target Kotlin supports.

So, how do we architecture such a layer of compatibility ?
Let’s start from the application that uses this library. As the application will compile to different targets, it needs a single way of interfacing with your library that works on each target. Step 1 is therefore to design an interface that exposes all the features you need from the native library.

During the first few posts, we will take a native library whose purpose is to encode or decode a ByteArray into or from a Base64 String. This is a very bad example because there’s really no reason to go through the trouble of interfacing a native library for a basic feature that is provided by every platform. This is, however, a simple enough example to allow us to focus on the important part.

Our goal is to provide an implementation of that interface on every platform, as well as a getNativeBase64 function that returns said implementation.

There’s a lot of modern & efficient native languages out there. To name a few: C++, Go or Rust (which you should really take a look into, if you haven’t already!). However, what the JVM, Kotlin/Native and Web Assembly all have in common is that they are only compatible with C, an ooold language !

C is one of the very few native languages to offer a stable & standardised ABI. The good news is that all other native languages are compatible with C. This means that no matter the actual implementation language of the library, as long as it exposes C ABI functions, you’re good to go.

Here is the C header that we will use:

The first two functions return the maximum byte length that will be output by the encode or decode function. More specifically, base64_max_encoded_len returns the maximum number of character a base64 string can be for a givcen byte array size. base64_max_decoded_len works exactly the other way around.

C has a problem. It has no standard way of expressing errors or success. Furthermore, the notion of memory management responsibility is… fluctuent. Here, we’ve made the following API choices for the encoding and decoding functions (which are by no mean a standard):
- If they succeed, they return a null (0) pointer. If they fail, they return an error message which needs to be freed by the caller.
- If they succeed, they do not allocate any memory, leaving memory management to the caller. They write the result of their computation into a previously allocated memory buffer passed as an argument (this is why we need the base64_max_*_len functions, which tell us the size of the buffers to allocate).
- If they succeed, they write into the out_len integer pointer the actual number of byte that were written into the buffer.

Having the memory being managed by the caller certainly complicates the C interface, but it allows for memory optimisations which we will discuss in a later part.

Once the Kotlin and C interfaces are defined, we can start working on each target’s native interface. Or can we?
How can we know each implementation works? By testing of course!
Kotlin/Multiplatform provides a simple test framework that works everywhere, so let’s use that! For now, we’ll consider an implementation correct if it satisfied these three tests:

Now we can move on each native interface.
The JVM and Android use JNI, Kotlin/Native uses C/Interop and the web uses Web Assembly.

The next parts will cover each native compatibility interface, as well as how they integrate into the Kotlin/Multiplatform build system.

This article is brought to you by Kodein Koders, a european service & training provider focused on Kotlin & Kotlin/Multiplatform.

--

--