Create a Kotlin/Multiplatform library with Swift

Salomon BRYS
Kodein Koders
Published in
9 min readMar 4, 2021

As you probably know, Kotlin/Multiplatform allows you to abstract the backend of your application, leaving the UI frontend to be specific to the targeted platform, and programmed using their native language. You can therefore think of the Kotlin/Multiplatform code as the business layer of your app, handling everything that is specific to your app and provides value from your business perspective.

However: nothing is that simple. You’ll always need libraries to handle for you tasks that are needed by your business but not integral to it. Things like code modularisation with Kodein-DI, asynchronous programming with KotlinX-Coroutines, data persistence with Kodein-DB or SQL-Delight, HTTP requests with Ktor, date and time manipulation with KotlinX-datetime, etc.
Let’s be honest: Kotlin/Multiplatform is still a very young technology, and there is still much to be done regarding its ecosystem. Everytime you write some code that is not integral to your business, you should ask yourself whether that code would be useful to the community and if so, port that code to a library. Here’s how.

First, you need to define the platform you are going to target. There are three decisions you need to make:

  • What Kotlin/Native target is the code compatible with? It could be everyone, only Apple targets or even only iOS targets.
  • Do you need the Android SDK or just a JVM? If you do need the Android SDK, do you target only Android, or do you target both Android & the JVM?
  • Do you target JavaScript? While Kotlin/JS is a very interesting target, JS comes with its own unique set of restrictions.

Then, you can create a Gradle build script that reflects the targets you are using. Here is an example of a simple iOS and JVM library: it’s the smallest form for a Kotlin/Multiplatform library, only targeting iOS and the JVM.

Note that by adding the iOS target, you are actually adding two different targets: one for the iOS simulator named iosX64 and one for the actual iPhones named iOSArm64.

Also, you should make sure that these two lines are in your gradle.properties file as they enable IntelliJ IDEA features that make multiplatform development a lot easier:

Let’s say you want to create a library that can read a file’s entire content as a String and provide some other simple file utilities. Let’s call this library KMFile.

Android API for accessing files is actually the JVM API, so we can target the JVM rather than specifically Android, and therefore have the library be compatible with all JVM compatible platforms.

The first thing you would want to do is to create an API that abstracts the required behaviour and be usable in a common Kotlin/Multiplatform code.
Here’s an example of API we would declaire in src/commonMain/kotlin:

The KMFile is an expect class. This means that it is actually a blueprint of what is expected to exist in each platform. That’s why the path property cannot be a constructor parameter: we are expecting a KMFile class with a constructor parameter called path and we are also expecting a path property. Whether this parameter is linked to that property is left to the actual implementation.

Let’s implement this class for the JVM. It should be very simple.

The expect/actual syntax is actually a lot more constraintful than a classic interface. An actual implementation must be exactly as expected, no subtypes or generalisation allowed. Also, everything that was expected must be explicitly marked actual.

As you can see the iOS implementation is a bit more complicated, but not that much. This is because Kotlin/Native exposes the iOS Objective-C API.
Objective-C is an old language that’s basically an objective layer over the C language (hence, it’s spot-on name). Just like C, it uses notions of memory pointers and manual allocations. Kotlin/Native exposes these concepts with dedicated APIs in the kotlinx.cinterop package.

Let’s have a closer look at two snippets and understand what’s going on.

The memScope function opens a scope that allows to allocate memory in it, and will deallocate said memory when the scope closes. It is very useful in functions where we want to allocate memory that needs to live only during the function execution. We use it here because we need to allocate the memory for a Boolean value and pass its pointer to the fileExistsAtPath function. This function will fill the pointed memory with a boolean defining whether or not the path parameter points to a directory.

Here we need to copy the memory content of an Objective-C NSData to a Kotlin byte array. Because the ByteArray is a Kotlin object, it is managed by the Kotlin memory manager, which may move objects inside its internal memory for optimization purposes. We therefore need to pin it, asking the Kotlin memory manager to not move its memory around while it is pinned. Once pinned, we can access its backing raw memory pointer, and pass it to the Posix memcpy C function that, as its name suggests, copies memory.

As you should have understood by now, using C and Objective-C in Kotlin is not trivial. The good news is that it is not very complex either. Once you understand notions like object pinning, memory scopes, and pointers dereferencing, things become easy pretty quick. We will go into more details about C & Objective-C Interop in a later article.

Apple is heavily invested in Swift, its in-house programming language and has started to expose some of iOS’s newer APIs in Swift only. This may be a problem as Kotlin/Native currently only interops with Objective-C, and the Kotlin team has put the item “direct interoperability with Swift” in the “postponed for later” column of their official roadmap (at the time of writing).
All is not lost, however, as Swift can interop pretty well with Objective-C, so we can use Objective-C as a bridge between Swift and Kotlin.

An example of iOS API that is only exposed in Swift is the CryptoKit API that was introduced as part of iOS 13. Let’s say we want to use the Chacha20-Poly1305 algorithm it provides.
For reference, ChaCha20-Poly1305, affectionately known as ChaChaPoly is an Authenticated Encryption that, as its name suggests, allows to encrypt and authenticate a secret message. It is widely used in modern protocols as it is highly optimizable on standard X86 and ARM processors.

First, we need to create an XCode project that will provide an Objective-C bridge to the Swift API we want to consume. For that we’ll create a Swift static library.

As CryptoKit is only available in Swift, we need to create a Swift static library, and write our own API as a facade over it.

We need to provide a way for our API to return either a data result or an error. The Swift Result type is not available in Objective-C because it cannot handle Swift generics. Let’s write our own.

XCode will automatically generate an Objective-C interoperability layer provided that two conditions are met:

  • A Swift class exposed to Objective-C must extend NSObject
  • All symbols exposed to Objective-C, whether classes, methods or properties, must be public and annotated with @objc.

As you can see in this code, we have only annotated the DataResult class and its two properties. The constructors are not annotated as a DataResult will only be constructed in Swift.

We can now write our interop layer:

Just like DataResult, the SwiftChachaPoly class follows the same Objective-C interop conventions. Its functions take Data parameters which are automatically translated to NSData in Objective-C, and return our very own DataResult.
Internally, these functions use CryptoKit and its ChaChaPoly class and subclasses. These types are purposefully not exposed to Objective-C. Only a simple interface that’s easily translatable is exposed.

There’s a last piece of the puzzle we need to configure in XCode: the automatically generated Swift interop Objective-C header.
First we need to create an empty bridging header file, and configure it in the static library configuration. Even though it is empty, this header informs XCode that it needs to run the Objective-C interop generation.

We also need to export the generated interop header with the compiled static library. This is done with a Run Script build phase.

Now, once the library is compiled, it generates an Objective-C header that exposes our very own ChachaPoly API.

Back to our Kotlin/Multiplatform library project.
To use the static library we just created in our Kotlin/Native code, we need to ask Kotlin/Native to generate its own interoperability layer for the Objective-C API defined in the header.

For this, we need to add an interop phase to our build. Interop phases are configured using a definition and a Gradle configuration.
If we want to add a “SwiftChachaPoly” interop phase, we need to create a src/nativeInterop/cinterop/SwiftChachaPoly.def file:

In this file, we define the package in which the generated Kotlin code will live, the header the interop tool needs to read to find the API we want to access, the static library to embed with the Kotlin library and to later link against, as well as some linker flags that are needed when linking with a library containing Swift code.

I’d like to draw your attention to the “-*_version_min” flags as they add the requirement that the iOS application that will use the library be targeted to iOS 13 or newer. Swift interop using static libraries require this version of iOS at minimum. At the time of writing, iOS 13 or newer represents a bit more than 90% of iOS devices.

Next, we need to configure Gradle to add the defined interop phase:

With this, we can run the cinteropSwiftChachaPolyIos Gradle task that will generate the Kotlin code to access the Objective-C API.

Note that using an interop layer currently breaks IntelliJ IDEA code autocompletion and analysis. We therefore need to use a workaround in the form of the iosTarget lambda we first defined. This workaround won’t be needed anymore once Jetbrains releases a commonizer compatible with interoperability layers.
This bug also means that we need to disable enableGranularSourceSetsMetadata in gradle.properties.

Finally, we can write our own Kotlin bridge to use that ChachaPoly API.
First, we need to transform ByteArrays to and from NSData:

Note that when converting a ByteArray to NSData, we use object pinning, as described earlier, to have the NSData access directly the byte array memory, and thus avoiding copying memory.
This optimization is not possible the other way around, so we do need to copy memory when converting an NSData to a byte array.

Next, we can write our Kotlin facade of the API:

As you can see, once we have all in place, the final piece of the puzzle is a simple facade that transforms arguments and return types.
We use a custom unwrap extension function on DataResult to throw an exception if the code fails, and we use autoreleasepool to ensure that all NSData created in these methods are properly released when the method returns.

So, all that’s left for us is to test these functions, to ensure that all pieces of the puzzle nicely fit together.
We can write a very simple Kotlin/Multiplatform test that checks this implementation of Chacha20-Poly1305:

This test creates a fake encryption key and nonce, encrypts the “Hello, World!” message and checks the result bytes, then decrypts it, and checks that the decrypted message is equal to the original “Hello, World!”.
With this, we can ensure that our Kotlin-to-ObjectiveC-to-Swift pipeline works as expected.

Of course, all the code is available in a sample repository.

Now that we have worked so hard for our library, it would be nice to release it to the world, and provide it to the community !
In the next article, Romain will walk you through everything needed to release a Kotlin/Multiplatform library in your local repository, in an online specific repository, and finally to make it available publically in Maven Central!

I hope this piece has inspired you to write libraries and make contributions to the Kotlin/Multiplatform ecosystem and community.

See you around !

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

--

--