Running Swift code on Android Wednesday October 14, 2015

Apple's Swift has been available for over a year now, and Apple has promised it will be made available under an Open-Source license by the end of 2015.

That's great, but can I run Swift code on an Android device today?

The Swift Compiler

Given they were both designed by the awesome Chris Lattner, it is not a surprise to discover that Swift's compiler is built on top of LLVM. LLVM is a compiler infrastructure that leverages the very interesting concept of a retargetable compiler.

LLVM architecture

Namely, instead of generating machine code targetting a specific architecture, LLVM generates assembly code for an imaginary machine, and then converts that intermediate representation to actual code for whichever architecture we're interested in.

That modular design is very smart since it allows for high code reuse (sharing optimizations and backends among frontends). There are some excellent resources on the net if you want to know more about LLVM.

Targetting a different machine

At that point, you're likely wondering:

But wait, if LLVM is so modular, couldn't we use a different backend and generate binary code for something that is not OS X or iOS? Android maybe?

Well, turns out you can! Let's see how.

Building Swift code manually

Of course Xcode won't let you just point'n'click your way through this. So let's start by compiling and linking a simple Swift "Hello world" manually:

// hello.swift
print("Hello, world!");

Building the object file it isn't very complicated:

$ $SDK/usr/bin/swiftc -emit-object hello.swift

Let's have a look at what's inside hello.o, the object file we just compiled:

$ nm hello_swift.o
                 U __TFSSCfMSSFT21_builtinStringLiteralBp8byteSizeBw7isASCIIBi1__SS
                 U __TFSs27_allocateUninitializedArrayurFBwTGSaq__Bp_
                 U __TFSs5printFTGSaP__9separatorSS10terminatorSS_T_
                 U __TIFSs5printFTGSaP__9separatorSS10terminatorSS_T_A0_
                 U __TIFSs5printFTGSaP__9separatorSS10terminatorSS_T_A1_
0000000000000140 S __TMLP_
0000000000000100 S __TMaP_
                 U __TMdSS
                 U __TZvOSs7Process11_unsafeArgvGVSs20UnsafeMutablePointerGS0_VSs4Int8__
                 U __TZvOSs7Process5_argcVSs5Int32
                 U _globalinit_33_1BDF70FFC18749BAB495A73B459ED2F0_func6
                 U _globalinit_33_1BDF70FFC18749BAB495A73B459ED2F0_token6
0000000000000000 T _main
                 U _swift_getExistentialTypeMetadata
                 U _swift_once

Ha, that's interesting! So apparently Swift mangles symbols kind of like C++. Indeed, the print function wasn't resolved to a _print symbol but to a much more convoluted __TFSs5printFTGSaP__9separatorSS10terminatorSS_T_ list of symbols.

There's also a bunch of other symbols required too, that seem to deal with string conversion and memory handling.

Anyway, all those symbols are defined in libswiftCore.dylib, which is somewhere in the $SDK. Let's give that info to the linker:

$ ld -arch x86_64 -o hello hello.o
     -L$SDK/usr/lib/swift/macosx
     -lSystem -lswiftCore

$ DYLD_LIBRARY_PATH=$SDK/usr/lib/swift/macosx ./hello
Hello, world!

Yay, it's working!

Targeting Android

The biggest issue here is going to be a missing SwiftCore library. Right now Apple is shipping one for iOS, OS X and Watch OS. But that's it - and obviously they don't ship an Android version.

However, not all Swift code requires the SwiftCore library, just like not all C++ code requires the STL. So as long as we use the subset of Swift that doesn't hit SwiftCore, we should be ok.

For the sake of the demo, let's start with a simple addition:

// add.swift
func addTwoNumbers(first: UInt8, second: UInt8) -> UInt8 {
  return first + second
}

So basically the process it going to be threefold:

  1. Ask the Swift compiler to generate some LLVM-IR
  2. Use LLVM to generate ARM ELF from the intermediate representation
  3. Use the Android NDK to generate a binary that links against the generated object file

1. Ask the Swift compiler to generate some LLVM-IR

In the previous step, when running swiftc hello.swift, the Swift compiler actually did two things:

  1. Generate LLVM Intermediate Representation from the Swift code
  2. Translate that IR to some x86_64 machine code, packaged as a Mach-O file

This is indeed the most common case so it makes sense for the compiler to do both at once. But we want to generate some an ARM ELF file (this is the binary format used on Android).

$SDK/usr/bin/swiftc
  -parse-as-library # We don't need a "main" function
  -target armv7-apple-ios9.0
  -emit-ir
  add.swift
  | grep -v "^!" # Filter-out iOS metadata
  > add.ll

Note: We have to add a "grep" filter to remove some iOS-specific metadata added by the Swift compiler.

2. Generate an object file from the LLVM-IR

From this point on we'll need the Android NDK. Luckily it ships with an LLVM toolchain, whose llc (LLVM static compiler) we can leverage:

$NDK/toolchains/llvm-3.5/prebuilt/darwin-x86_64/bin/llc
  -mtriple=armv7-none-linux-androideabi
  -filetype=obj
  add.ll

Great, so now we have just built an ARM ELF object file!

3. Package that object file in an Android app

We'll want to call this from Java, so we need a JNI bridge. It'll be easier to write it in C, here goes:

// jni-bridge.c

// Let's work around Swift symbol mangling
#define SWIFT_ADD _TF3add13addTwoNumbersFTVSs5UInt86secondS0__S0_

uint8_t SWIFT_ADD(uint8_t, uint8_t);

jstring jni_bridge(JNIEnv * env, jobject thiz ) {
  uint8_t a = 123;
  uint8_t b = 45;
  uint8_t c = SWIFT_ADD(a,b);

  char result[255];
  sprintf(result, "The result is %d", c);

  return (*env)->NewStringUTF(env, result);
}

And finaly, we just have to pack all this in a shared library:

$NDK_GCC/bin/arm-linux-androideabi-ld
  add.o
  jni_bridge.o
  -shared # Build a shared library
  -lc # We'll need the libc
  -L$NDK/platforms/android-13/arch-arm/usr/lib

And that's it! We just need to package that shared object file in an Android app and run it:

We are computing the addition of 123 and 45. The result is 168. This computation was written in Swift and is running on this Android device!"

Conclusion

That was fun, but is of course useless:

Last but not least, I've made published a working demo on GitHub if you guys want to experiment with this!