Porting the Swift Runtime to Android, Part One: The How

In June 2016 something I worked on was featured in Apple's WWDC State of the Union.

Shortly after Swift was open-sourced, Zhuowei Zhang sent an email to the swift-dev mailing list, introducing his fork of Swift, which could produce code that ran on Android devices. I worked on his fork, to adapt the code so that it could be merged into the Apple Swift repository. In February 2016 I sent a pull request, entitled simply "Port to Android". It was merged in April.

What does it mean to "port" the Swift runtime to Android?

The Swift compiler is a program that simply translates Swift source code into an intermediate representation, called LLVM IR. LLVM is a program that translates LLVM IR into assembly code. Assembly code is a slightly more readable version of the 0's and 1's that are used to represent machine instructions. LLVM can generate assembly for many different platforms, including Android, PS4, and Windows.

LLVM can convert IR into executable code that runs on many different platforms, including Android.

Swift already generates LLVM IR, and LLVM can already turn IR into assembly code for Android. The pull request I sent to "port" the Swift runtime to Android merely added a few command line options, as well as some glue code to get it to build. Here's what it involved:

  1. Modifications to the compiler itself
    1. Adding an Android -target option to libDriver
    2. Adding an Android toolchain option to libDriver
    3. Adding an os(Android) platform condition to libBasic
  2. Modifications to the Swift runtime
    1. Using #if defined(__ANDROID__) to conditionally exclude unsupported features, such as stack traces
  3. Modifications to the Swift standard library
    1. Modifying the build system to link against a libicu built for Android armv7
    2. Modifying the build system to use Android's libc implementation
    3. Expanding os(Linux) checks to also include os(Android)

In a future post, I'll write about the current state of using Swift to write Android apps. In the meantime, you can read on for more technical detail on the above changes.


1. Modifications to the compiler itself

I needed to make a few changes to the compiler, the program that runs when you invoke swift on the command line, in order for it to properly inform LLVM that swift -target armv7-none-linux-androideabi should produce 0's and 1's that work on Android.

1.1. Adding an Android -target option to libDriver

The Swift compiler is an ordinary C++ program. Like any C or C++ program, it has an int main() function. That function almost immediately calls code in lib/Driver.

The lib/Driver library does two things:

  1. Parses command line arguments.
  2. Generates commands to shell out and execute. The commands usually just call swift -frontend once for each source file, then call the linker to link them together.

One of the arguments lib/Driver parses is -target, an option you can pass to the compiler so that it generates an executable that can be run on a specific machine. For example: to compile Swift source code for macOS 3.3, use swift -target x86_64-apple-macosx10.10. To compile Swift source code for iOS, use swift -target arm64-apple-ios7.0.

A few spots in the Swift codebase validated the -target parameter being passed in, raising an error if it wasn't recognized. My pull request added the swift -target armv7-none-linux-androideabi option to those spots.

1.2. Adding an Android "toolchain" option to libDriver

lib/Driver parses the -target option and chooses a "toolchain" based on that option.

As I mentioned above, lib/Driver [1] parses command line arguments, and [2] generates command line invocations for swift -frontend. In Swift driver parlance, a "toolchain" is an object that knows how to generate those invocations for a specific platform.

For example, driver::toolchains::Darwin knows which arguments to pass to swift -frontend to compile Swift source code for macOS and iOS. driver::toolchains::GenericUnix knows how to do so for Unix platforms like Linux.

My pull request added an Android toolchain, which passes the correct arguments to swift -frontend and the linker for Android. Most importantly, it specified -target armv7-none-linux-androideabi when invoking the linker.

1.3. Adding an os(Android) platform condition to libBasic

The changes above are all that's necessary for the Swift compiler to specify that Swift code that's been lowered to LLVM IR be translated to machine code for Android.

However, people writing Swift will probably want to write things differently when targeting Android, so my pull request added an os(Android) platform conditional. This allows you to write the following Swift code:

/// Foo.swift
#if os(Android)
    import Glibc
#else
    import Darwin
#endif

2. Modifications to the Swift runtime

As a Swift program is running, it calls through to underlying system libraries. Not all of these libraries work the same on Android, so I needed to modify how the Swift runtime used them.

2.1. Using #if defined(__ANDROID__) to conditionally exclude unsupported features, such as stack traces

When you write Swift code that calls fatalError(), or when Swift code crashes as it's running, the Swift runtime prints a stack trace. To do so, it uses execinfo.h, a library that exists on Linux and macOS, but not on Android. My pull request turned off stack traces on Android.

3. Modifications to the Swift standard library

The changes above allow for Swift code to be compiled for Android, and run on an Android device. However, useful Swift code uses functions like print() or String. These are defined in the Swift standard library.

Swift's String supports unicode. On macOS, Swift uses CoreFoundation to hash and compare strings. On operating systems that don't ship with CoreFoundation, the Swift standard library uses libicu.

Swift's build system assumed that, if you were compiling the Swift standard library in a Linux environment, you'd build the standard library for that machine, and using the libicu that environment provided. None of these assumptions were true when building the Swift standard library for Android, so my pull request modified the build system so that you could specify which libicu to use.

3.2. Modifying the build system to use Android's libc implementation

When you write C, you have access to the C standard library. This includes functions like printf and ceil. Most implementations of the C standard library are the same, but some have slight differences.

In addition, the Swift build system assumed that the C standard library could be located at the system root of the environment compiling the Swift standard library, in /usr.

My pull request conditionally included libc headers on Android, as well as specified where to find libc headers for Android.

3.3. Expanding os(Linux) checks to also include os(Android)

Everyone's favorite part of writing cross-platform Swift code has got to be including the following five lines in every single file:

#if os(OSX) || os(iOS) || os(watchOS) || os(tvOS)
    import Darwin
#elseif os(Linux) || os(FreeBSD) || os(PS4) || os(Android)
    import Glibc
#endif

My pull request added os(Android) to a bunch of these checks.