DevelopmentRust on AndroidWritten by Emil Sjölander on Sun Jan 27 2019

You may have heard of rust, it’s a systems programming language designed for memory safety and speed. Built by Mozilla to power the next-generation high-performance cross-platform software. If you haven’t already I suggest having a look at the great learning material, but keep in mind that it can take a while to get into and appreciate so I suggest writing more than just a hello world.

If you’re an Android developer you may be asking yourself how and why you would make use of rust on Android. This article will mostly cover the how. As to why, the most compelling reason for us at Visly is that it enables us to share code between Android and iOS in a performant and safe manner, in a language much easier to work with than C++.

(We wrote a similar article for iOS)

Getting set up

Before we get started we need to make sure we have the rust toolchain set up. We will assume you already have a working Android toolchain, if not you should download Android Studio and set it up according to any other Android guide. One thing to make sure is that you have set up your $ANDROID_HOME environment variable. With a typical install on macOS this should be set to ~/Library/Android/sdk.

Next, we have to make sure rust is installed on our system. Rustup makes this a simple one-liner.

curl https://sh.rustup.rs -sSf | sh

You can validate rust was successfully installed and located in your PATH by running rustc --version. Once rust is installed on your system we need to make rust aware of how to build for the supported Android architectures. Rust can build binaries for all sorts of architectures but not by default. To add the appropriate architectures run the following command.

rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android

Next we have to set up some standalone toolchains to build rust for Android’s various supported architectures. These only need to be installed once for your system and not multiple times for each project, so instead of installing them to the project folder we will install them in our home directory.

mkdir ~/.NDK

$(ANDROID_HOME)/ndk-bundle/build/tools/make_standalone_toolchain.py --api 26 --arch arm64 --install-dir ~/.NDK/arm64;
$(ANDROID_HOME)/ndk-bundle/build/tools/make_standalone_toolchain.py --api 26 --arch arm --install-dir ~/.NDK/arm;
$(ANDROID_HOME)/ndk-bundle/build/tools/make_standalone_toolchain.py --api 26 --arch x86 --install-dir ~/.NDK/x86;

Finally we have to tell rust about these toolchains. Append the following to ~/.cargo/config, creating the file if it doesn’t already exist.

[target.aarch64-linux-android]
ar = ".NDK/arm64/bin/aarch64-linux-android-ar"
linker = ".NDK/arm64/bin/aarch64-linux-android-clang"

[target.armv7-linux-androideabi]
ar = ".NDK/arm/bin/arm-linux-androideabi-ar"
linker = ".NDK/arm/bin/arm-linux-androideabi-clang"

[target.i686-linux-android]
ar = ".NDK/x86/bin/i686-linux-android-ar"
linker = ".NDK/x86/bin/i686-linux-android-clang"

Hello World

Let’s build a small hello world app using rust! We’ll start by creating our rust library and later move on to creating our Android Studio project.

mkdir rust-android-example
cd rust-android-example

cargo new rust --lib
cd rust

This will create a basic rust library managed by cargo, the rust equivalent to gradle, which we will later make use of in our Android Studio project. The --lib flag instructs carg that we want to create a library, not an executable binary. Within the newly created project folder you will find Cargo.toml which much like a build.gradle file defines metadata for your library as well as any dependencies. You’ll also find a src folder which contains our rust source code. The src folder only contains lib.rs which itself only contains a sample test function. We can start by removing everything in this file and replacing it with the following.

#![cfg(target_os="android")]
#![allow(non_snake_case)]

use std::ffi::{CString, CStr};
use jni::JNIEnv;
use jni::objects::{JObject, JString};
use jni::sys::{jstring};

#[no_mangle]
pub unsafe extern fn Java_com_example_android_MainActivity_hello(env: JNIEnv, _: JObject, j_recipient: JString) -> jstring {
    let recipient = CString::from(
        CStr::from_ptr(
            env.get_string(j_recipient).unwrap().as_ptr()
        )
    );

    let output = env.new_string("Hello ".to_owned() + recipient.to_str().unwrap()).unwrap();
    output.into_inner()
}

We start off with telling rust that this file will only be used when targeting Android with #[cfg(target_os=”android”)] and because JNI required CamelCase function names which are not standard on Rust we also allow that with #[allow(non_snake_case)]. There are a couple other things to notice here, because we are interfacing with Kotlin we have to make use of C calling conventions and JNI, this means we have to tell rust not to mangle any names (with #[no_mangle]).

We then define a basic function which constructs a new string given an input string. A lot is happening there as we need to transform the string from a jni string to a C string to a rust string and back. The rust jni and ffi crates make this fairly safe though and later on we will link to some patterns we use in Visly to make this easier. In a larger application this is not much of an issue as the glue code between Kotlin and Rust can be kept fairly small.

We also need to update our Cargo.toml to add a dependency on the jni library as well as define the name of the final binary and how to compile it.

[dependencies]
jni = { version = "0.10.2", default-features = false }

[profile.release]
lto = true

[lib]
name = "rust"
crate-type = ["cdylib"]

Now the last thing to do before we are ready to move onto our Android Studio project is build our library for our supported targets.

cargo build --target aarch64-linux-android --release
cargo build --target armv7-linux-androideabi --release
cargo build --target i686-linux-android --release

Android Studio

Time to start a new Android Studio project and test this out in a simulator. Start by going through the standard project setup, we’ll be using Kotlin but you can use Java if you want as well. We will name the project android saving it next to our rust library at the root of rust-android-example.

Open up MainActivity.kt and paste the following code. We declare an external function hello which tells Android to look for a native library function named Java_example_com_android_MainActivity_hello. Before we can call this function we have to load our library using System.loadLibrary.

package com.example.android

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.util.Log

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        System.loadLibrary("rust")
        Log.d("rust", hello("World"))
    }

    external fun hello(to: String): String
}

At this point the app should compile however it will crash as soon as we run it. That’s because we haven’t yet included the native library in our project. Let’s copy it in.

cd rust-android-example

mkdir android/app/src/main/jniLibs
mkdir android/app/src/main/jniLibs/arm64-v8a
mkdir android/app/src/main/jniLibs/armeabi-v7a
mkdir android/app/src/main/jniLibs/x86

cp rust/target/aarch64-linux-android/release/librust.so android/app/src/main/jniLibs/arm64-v8a/librust.so
cp rust/target/armv7-linux-androideabi/release/librust.so android/app/src/main/jniLibs/armeabi-v7a/librust.so
cp rust/target/i686-linux-android/release/librust.so android/app/src/main/jniLibs/x86/librust.so

Now we can re-build and re-run our app and we should see “Hello World” written out in Logcat. Congrats! You have manage to compile and run Rust code on Android.

Automating the process

Automating this process of copying over binaries is actually pretty easy with a simple bash script.

#!/bin/sh
JNI_LIBS=../android/app/src/main/jniLibs

cd rust
cargo build --target aarch64-linux-android --release
cargo build --target armv7-linux-androideabi --release
cargo build --target i686-linux-android --release

rm -rf $JNI_LIBS
mkdir $JNI_LIBS
mkdir $JNI_LIBS/arm64-v8a
mkdir $JNI_LIBS/armeabi-v7a
mkdir $JNI_LIBS/x86

cp target/aarch64-linux-android/release/librust.so $JNI_LIBS/arm64-v8a/librust.so
cp target/armv7-linux-androideabi/release/librust.so $JNI_LIBS/armeabi-v7a/librust.so
cp target/i686-linux-android/release/librust.so $JNI_LIBS/x86/librust.so

Now save this as rust-android-example/install.sh and just run the script after any updates to your rust code to compile and install it into your Android Studio project. If you want to get fancy you can add this as a build step in your gradle file so it is automatically run any time you build your Android Studio project.

Next steps

While the code above works, it isn’t super easy to work with. In a larger project, we want a way to encapsulate the ugly bits of interacting with Rust from Kotlin and vice versa. In a later post, I’ll cover the patterns we adopted when building Visly to simplify this.

The final code for this tutorial and a great starting point if you plan on using rust on Android can be found on GitHub. If you have any questions or comments feel free to tweet at me.