Kotlin/Native is a technology for compiling Kotlin code to native binaries. Applications and libraries written in Kotlin Native run without a virtual machine and this is useful in scenarios where a virtual machine is not available or desirable.

Since the output is native, the resultant binaries are NOT portable, but the source can be. We can also easily create multi-platform libraries where we have Kotlin source that is compiled to multiple platforms - JVM, Android, JS, Native MacOS, Native iOS, Native Windows etc.

Generating Native Instructions

LLVM

Kotlin Native utilises LLVM. LLVM (not an acronym….anymore 1) is a set of modular compiler tools. It’s backend compilers compile a language independent Intermediate Representation (IR) into various platform specific native binaries. Front end compilers can be written to compile any high level language into this IR. This allows many high level languages to be compiled into platform specific native instructions. For example,

LLVM Pipeline

Supported Platforms

Kotlin Native is still beta but already supports,

  • iOS (arm32, arm64, emulator x86_64)
  • MacOS (x86_64)
  • Android (arm32, arm64)
  • Windows (mingw x86_64)
  • Linux (x86_64, arm32, MIPS, MIPS little endian, Raspberry Pi)
  • WebAssembly (wasm32)

There are tools provided to translate C and Objective C (and therefore, indirectly, Swift) libraries into Kotlin libraries and then use them directly from Kotlin. This build step has an easy integration into Gradle.

On each platform, specific bindings for common libraries have already been created. For example,

  • platform.posix
  • platform.OpenGL
  • platform.UIKit
  • etc

Building an Application

There are several moving parts in the tool chain,

  • kotlinc - Kotlin Compiler
  • cinterop - A tool for generating Kotlin Libraries (klib) from C/ObjC headers/libraries
  • org.jetbrains.kotlin.multiplatform - Gradle plugin for multi-platform Kotlin projects

The easiest way to get started is to use the IDE (CLion or IntelliJ) to create a new Kotlin Native project.

// build.gradle.kts - Kotlin Gradle DSL
plugins {
    kotlin("multiplatform") version "1.3.50"
}

repositories {
    mavenCentral()
}

kotlin {
  macosX64("native") {
    binaries {
      executable()
    }
  }
}

This configures tasks for building and testing a native Mac OS application. The macosX64 call can be replaced depending on platform,

  • macosX64
  • iosArm32
  • linuxX64
  • linuxX64
  • mingwX64

Importing C / Objective C

As mentioned above, many frameworks and libraries are supported out of the box.

  • platform.posix
  • platform.OpenGL
  • platform.UIKit
  • etc

You can also pull in other dependencies via Gradle as you would in any other Kotlin project,

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-native:1.3.2")
}

If you need to import additional C / ObjC headers you can create .def files which describe the headers, compiler options and linker options. These are processed by the cinterop tool to produce klibs or Kotlin bindings that can be used to call the C code easily from Kotlin. Marshalling and translation is handled largely by the klib.

headers=ncursesw/ncurses.h

compilerOpts = -I/usr/local/opt/ncurses/include
linkerOpts = -L/usr/local/opt/ncurses/lib  \
             -lncursesw

You can use platform specific compiler and linker options to have a single .def file that supports multiple native platforms,

headers=ncursesw/ncurses.h

compilerOpts.mingw_x64 = -I../ncurses/mingw64/include
linkerOpts.mingw_x64 = -L../ncurses/mingw64/lib \
             -lncursesw

compilerOpts.macos_x64 = -I/usr/local/opt/ncurses/include
linkerOpts.macos_x64 = -L/usr/local/opt/ncurses/lib  \
             -lncursesw

Although the cinterop tool will do the work in the background, the easiest way to invoke it is via Gradle

kotlin {
  macosX64("native") {
    val ncurses by main.cinterops.creating

Writing Kotlin

Once the build is set up, the code you write will largely be standard Kotlin. Since it is not running on the JVM, a large swathe of the libraries you know and love will not be present - since they are really Java libraries.

But you will have access to the standard platform libraries, any native or multi-platform libraries that you import and any C / ObjC klibs that you import / generate.

import platform.posix.*

fun main() {
    ncurses.initscr()             // Initialise Screen
    ncurses.cbreak()              // Read user input without buffering

    val log = fopen(filename, "r")
    // ...
}

Memory management is still handled for you if you within your Kotlin code but the implementation is very different. At runtime it uses reference counting (with a garbage collector for cycles). So the great thing is, when writing Kotlin for Native, not much changes.

When crossing the boundary between Kotlin and other languages/platforms, there are more considerations. Native specific concerns include,

  • Native Memory Management - allocating and freeing bytes!
  • Marshalling Data - moving data back and forth between Kotlin and native code - e.g reference counting implementation is compatible with Swift / ObjC but not C
  • Threading - Kotlin Native has tight constraints on what can be shared between threads
  • Passing Functions - calling Kotlin from native and calling native from Kotlin
  • Supported Language Features - e.g. Suspend functions are not supported between Kotlin and C / ObjC

There’s too much here to get into within this post and some of this is a moving target as it is still beta. For example,

  • The threading model may evolve to support common patterns
  • Coroutines are largely limited to the main thread at the time of writing so a solution needs to be developed
  • Some standard cross platform concerns may be provided by kotlin libraries
  • Kotlin Native or multi-platform libraries and frameworks are popping up all the time

Multi-platform

With all of the different flavours of Kotlin it can be daunting planning how to manage the code. Some code will be compatible on all platforms (JVM, Android, JS and various native platforms) and some will be specific to a single platform. This is where multi-platform projects come in.

With a multi-platform project we can have a single project that includes the common and specific code. It can be used to build different artefacts e.g. JVM .jar file, native .dll or .dylib file etc.

Multi-platform Project

Within the common area we can define placeholders for entities (classes, functions, globals etc) with the expect keyword.

expect fun readImage(name: String, width: Int, height: Int): Image?

This can then be implemented specifically for each platform using the actual keyword,

actual fun readImage(name: String, width: Int, height: Int): Image {
  // Platform specific implementation
}

These projects maximise the reuse of Kotlin across multiple platforms.

Conclusion

Although beta, Kotlin Native is a very interesting project and already has lots of functionality and potential. One of the most compelling stories is using Kotlin to write common code for a mobile application in a multi-platform project. This can output a native iOS framework or an Android library. A thin native iOS (Swift) and Android (more Kotlin :-) ) layer can then be added on top, thus reducing overhead and cost in producing multi-platform mobile applications.

The same pattern can be used for sharing models between the frontend and backend. You just need to reach the critical mass of shareable logic.

At Instil, we are keeping a close eye on this one.

  1. LLVM originally stood for Low Level Virtual Machine but this acronym has been dropped to avoid confusion with actual Virtual Machines.