Article Image
read

Sample project accompanying this blog post can be found at GitHub

Good news, everyone.

During my time at Clue, one of our Android developers, Eugenio Marletti, had an obsession with Kotlin. It quickly become a running gag in our daily conversations, and I can't even try to describe the look of joy on his face when he approached me one day with great news: "You'll be able to run Kotlin on iOS!". Yeah, sure.

What Eugenio failed to disclose were few little details about the Kotlin/Native Tech Preview:

  • Right now there's no support for either 32-bit devices or the iOS Simulator, and while the former wouldn't be much of an issue if you only target iOS 11, the latter is quite a big problem in terms of development speed.
  • There's no interoperability whatsoever (yet) with standard libraries used in iOS development, be it plain Foundation or UIKit...
  • ... in fact Konan, Kotlin's compiler, can currently only produce executable binaries, so either you'll write your whole application in Kotlin or you'll need to figure out a way how to build and link the library yourself since such use-case is not yet supported.

I promised Eugenio that I would try to make it work anyway. How hard could it be? My previous endeavors with trying to run Swift on bare metal and Nintendo 3DS proved that anything can be done with just enough terrible hacks and compiler abuse.

How many layers of wrappers are you on?

Every Kotlin/Native program is wrapped in 2 layers of launcher code during the linking phase. One layer is written in Kotlin and the other in C++.

The Kotlin part is a very simple function responsible for locating an entry point into the actual code, executing it and handling exceptions. This function has a special compiler annotation that makes sure it will be exported to the second launcher.

The C++ counterpart is a little bit more complicated: it initializes the Kotlin runtime, transforms the launch arguments into Kotlin's native Array type containing Kotlin objects of String type and then executes the Kotlin version of the launcher.

Since the topmost layer is the C++ one I hoped at least for Obj-C++ interoperability. I also realized that the Kotlin part of the launching mechanism can be dropped since we can simply annotate our own code and then call it directly from the C++ launcher. The C++ launcher had to stay, but also had to be modified to accommodate our needs.

I wrote a simple Kotlin function that returns a string:

import konan.internal.ExportForCppRuntime

@ExportForCppRuntime
fun kotlin_main() : String {
    return "Hello from the other side!"
}

And then rewrote the launcher as:

#include "Memory.h"
#include "Natives.h"
#include "Runtime.h"
#include "KString.h"
#include "Types.h"

#include <stdlib.h>
#include <stdio.h>
#include <string>
#include "utf8.h"

extern "C" KString kotlin_main();

extern "C" const char* kotlin_wrapper() {
  RuntimeState* state = InitRuntime();

  if (state == nullptr) {
    return "Failed to initialize the kotlin runtime";
  }

  KString exitMessage;
  {
    ObjHolder args;
    exitMessage = kotlin_main();
  }

  const KChar* utf16 = CharArrayAddressOfElementAt(exitMessage, 0);
  std::string utf8;
  utf8::utf16to8(utf16, utf16 + exitMessage->count_, back_inserter(utf8));

  DeinitRuntime(state);

  const char *cString = utf8.c_str();
  char *returnMessage = (char *)malloc(strlen(cString) * sizeof(char*));
  strcpy(returnMessage, cString);

  return returnMessage;
}

I had to add some boilerplate for handling string marshalling between Kotlin and C, but other than that code is fairly straightforward and doesn't really differ much from the original implementation. Please spare me all the comments about how terrible the C++ code is. C++ is my second least favorite programing language and you don't even want to know what's taking the first spot in this race.

That's a very nice wheel you have written here, but how does it compute?

Since now I had all of this code that should work, I just needed a way to actually compile and link it. Easy, right? I started first by printing every llvm/clang command that is being executed during both compilation of the compiler itself as well as when compiling Kotlin/Native code. This lead me to both the clang parameters needed to compile launcher and llvm-lto parameters used for linking.

By default Kotlin/Native distribution ships with both launchers, runtime and standard library compiled into LLVM bytecode, then during the compilation process, your own code is also compiled into bytecode and then linked together with the rest of the object files to produce an executable.

With that knowledge we can tape together a simple build script that will do all of that for us, except instead of an executable we will get a static library in the end. We just need to make sure that we export the symbols that we want to access from Swift:

#!/usr/bin/env bash
set -e
set -o pipefail

KOTLIN_HOME=../KotlinNative
DIR=.
PATH=$KOTLIN_HOME/dist/bin:$PATH
PATH=$KOTLIN_HOME/bin:$PATH
PATH=$KOTLIN_HOME/dist/dependencies/clang-llvm-3.9.0-darwin-macos/bin/:$PATH
TARGET=iphone

# Compile Kotlin code to LLVM bytecode
# -nomain will allow us to have non-executable binary
# -produce will make sure we output LLVM bytecode
echo "Compiling Kotlin Code"
konanc \
    -target $TARGET \
    -nomain \
    -produce bitcode \
    $DIR/kotlin.kt -o kotlin.bc

# Compile C++ Launcher to LLVM bytecode
# -emit-llvm is the argument used to achieve that.
echo "Compiling C++ Code"
clang++ \
    -std=c++11 \
    -stdlib=libc++ \
    -miphoneos-version-min=8.0.0 \
    -arch arm64 \
    -I $KOTLIN_HOME/common/src/hash/headers \
    -I $KOTLIN_HOME/runtime/src/main/cpp/ \
    -isysroot $KOTLIN_HOME/dist/dependencies/target-sysroot-2-darwin-ios \
    -c \
    -emit-llvm \
    launcher.cpp

# Link everything together
# We use -exported-symbol argument to tell the linker about the functions we want to be able to call later
echo "Linking"
llvm-lto \
    -o kotlin.o \
    -exported-symbol=_kotlin_wrapper \
    -exported-symbol=_kotlin_main \
    -O1 \
    $KOTLIN_HOME/dist/klib/stdlib/targets/iphone/kotlin/program.kt.bc \
    $KOTLIN_HOME/dist/klib/stdlib/targets/iphone/native/runtime.bc \
    kotlin.bc launcher.bc 

# Convert the object file to a static library
echo "Bundling as a library"
libtool -static kotlin.o -o kotlin.a

# Remove Build Artifacts
rm -rf *.bc
rm -rf *.o

# Dump build product information
otool -hv kotlin.a
echo "Library built successfully"

For the above script to work, you need to checkout Kotlin/Native repository at version 0.3, build the compiler for cross distribution, and update the $KOTLIN_HOME variable with correct location.

Xcode? Yeah, sure, why not.

At this point there's really not much stopping us from finally being able to call Kotlin code from Swift. There's still few things that we have to take care of, but compared to previous steps, this hits much closer to my cozy iOS home.

First of all we have to add the built library to the project. We could wrap it into a .framework package but for simplicity I will use the good ol' plain .a file. Just add it as a dependency to the project and after some configuration work you should be ready to go:

  • We need to disable bitcode as our library doesn't have it baked in, so Xcode will complain and refuse to build our app until we disable it altogether.
  • Since we just built the binary for one architecture (arm64), let's remove the unsupported ones from the project. Just in case.
  • And most importantly: we need to change CLANG_CXX_LANGUAGE_STANDARD to c++0x or C++ Language Dialect to C++11 [-std=c++11]

Strangely, for those two last options to actually influence anything, we need to have an (empty) C++ file present in the target. After it's there the project should compile just fine (granted you target a 64-bit device and not the simulator).

What do you mean you lied?

After all of the configuration it's finally time to call the Kotlin function from the Swift side. The thing, though, is that we're not going to be calling Kotlin code. After all, the topmost layer of the Kotlin/Native is C++, and that's what we need to specify in our bridging header.

extern const char* kotlin_wrapper();

And then finally in Swift:

if let retVal = kotlin_wrapper() {
    let string = String(cString: retVal)
    print(string)
}

There it is, Kotlin code, wrapped in C++, wrapped in Objective-C++, wrapped in Swift. It's glorious.

Ship it!

Please don't.

Next steps

Obviously being able to pass a string of bytes along three layers of interoperability mechanisms is not something you'd need in a real world scenario. Unfortunately, there's little more that can be done right now without going great lengths of writing code for marshalling Foundation types to Kotlin. It can certainly be done though, and I'm sure that's the way the Kotlin/Native team is going to pursue when they finally start working on a proper way of doing what I've just hacked together. For now though Kotlin/Native on iOS is just a neat idea and a fun project to play with.