You don’t want rust in your android, but you might want Rust in your Android.
Background
I like Kotlin, and I’m very impressed with the content being written in Rust. I knew it should be possible to call Rust from my Android app. Because I love fighting with the compiler I wanted to see if I could get it working for fun. (I got it working!) I wrote this blog post so others could try it out, and so I could refer back when I try to do something again in the future.
The star of the show is Mozilla’s UniFFI library that does a lot of the hard work. A high level view is that it generates Rust and Kotlin1 that are made for each other. That way your Kotlin code can invoke the Rust methods without worrying about Foreign Function Interface (FFI) for talking cross-language.
The rest of this post will walk through
- configuring your development environment
- creating a basic Rust library with UniFFI-generated scaffolding
- generating Kotlin using UniFFI
- integrating the Rust and Kotlin in an Android app
I’ll assume you have a basic Rust (via cargo) and Android (via Android Studio) environment installed.
Step 1 - Configure your Rust + NDK environment
This was (I believe) the most annoying part to get right. You can either manually configure the Android Native Development Kit (NDK) or you can use cross
that downloads a Docker image that’s ready to go. I’d recommend setting up the NDK locally (builds faster2), but falling back on cross
(easier default setup) if you get stuck.
Option A - Use Docker-based cross
- Install Docker Desktop, OrbStack, Rancher Desktop, or your favorite tool. If you can run
docker run --rm hello-world
, then you’re good. - Install cross.
- If you’re happy with the
minSdkVersion
oncross
(seen here), you’re done. Otherwise, you’ll need to build new Docker images with the desired Android version (instructions here) - That’s it! Go to “Step 2 - Make a Rust library”.
Option B - Configure Android NDK locally
Open Android Studio, and navigate to SDK Manager > SDK Tools > NDK (Side by Side) as laid out on the Android Developer site.
Locate which NDK version you have…
… and set it to your NDK_PATH
environment variable.
<⚠️> Android replaced libgcc
with libuwind
in NDK 23 which breaks the compilation step. Fortunately there’s a workaround3 that I’ll summarize. If you’re using NDK 23.x or higher, you’ll either need to use a nightly
version of Rust or run the following from your terminal.
</⚠️>
You’ll be able to see the C libraries for each of the architecture-Android version combinations. I’ve modified the output to be more readable.
I’m going to build for an Android minSdkVersion
of 24, so these are the four libraries I’ll use.
Open (or create) your $HOME/.cargo/config
file. Add each of the target linkers. Please note:
- The path has to be absolute.
armv7a
’s target name and clang name are different and it is “androideabi” as opposed to “android”.
Finally, add the targets to your Rust environment.
Step 2 - Make a Rust library
For our example, we’re going to make a simple library that has two methods: reverse a string (“hello” -> “olleh”) and reverse an integer (123 -> 321).
Let’s start by making the library using cargo
.
Inside the generated src/lib.rs
file, I throw in some (ChatGPT-assisted) Rust code to reverse a string and integer as well as some tests.
From the reverse-rs/
folder, run cargo test
and make sure everything looks good.
Step 3 - Prepare the Rust for Android
Here’s where the UniFFI magic comes in! We’re going to define our reverse string and integer methods in UniFFI’s special language which we’ll then use to generate both the Rust and Kotlin code.
Update dependencies
Update the Cargo.toml
file to look like this.
This snippet does three key things.
- Make the library a cdylib crate. I dropped the
-rs
from the name because hyphens aren’t allowed. - Add
uniffi
as a dependency. - Add
uniffi
as a build dependency.
Write the UDL file
UniFFI uses it’s own special UniFFI Definition Language (UDL) for describing interfaces. I made src/reverse.udl
.
Write the Rust generator
Create a build file in the the top level folder (i.e. reverse-rs/build.rs
) and have it point to the UDL file.
Add the uniffi::include_scaffolding
macro on the top of the lib.rs
file, to generate the Rust scaffolding.
Step 4 - Compile the Rust library
If on step 1 you setup cross
use that, or if you went through all the NDK-related steps, use cargo build ...
.
The end result will be a .so
file in your corresponding target/
folder!
To get these ready for the Android app you’ll need to:
- move everything to the appropriate Android ABI directory in a
jniLibs/
folder - rename
libreverse.so
tolibuniffi_reverse.so
Here’s a command that will do all of it for you.
Here’s where you’ll be at the end.
Step 5 - Generate the Kotlin methods
Add the following to the bottom of your Cargo.toml
file.
Make the reverse-rs/uniffi-bindgen.rs
file.
Then generate the Kotlin code!
This creates a new file reverse-rs/src/uniffi/reverse/reverse.kt
with a ton of boilerplate but also our methods!
Step 6 - Create the Android app
For demonstration purposes, I’m going to make a new project via Android Studio > File > New Project… and use the “Empty Activity” template, but I’m assuming you’re familiar with Android development and can make your own choices.
Add the JNA dependency
The UniFFI library depends on Java Native Access (JNA), so add the @aar
dependency.
Make sure to sync your Gradle files.
Copy over generated files
- Move the
reverse-rs/jniLibs/
folder intoapp/src/main/
. - Move the
reverse-rs/src/uniffi/
folder intoapp/src/main/java/
.
Use the generate Kotlin library
Your IDE will now autocomplete, and you’ll have access to uniffi.reverse.reverseString
and uniffi.reverse.reverseInteger
. Here’s what my class looks like.
Run it and 🤞🏼 that you don’t have any errors!
Congratulations! You’re running Rust in Android!
Bonus - Suggestions and Resources
There are a few tweaks that you can do and other things I came across that you might find interesting/helpful.
Optimize with --release
When you cross build
or cargo build
, adding the --release
flag really cuts down on size (but it ~doubles the build time).
Move uniffi-bindgen
to its own crate
If you want to iterate faster on your Rust + Kotlin, you’ll need to have the uniffi-bindgen
logic in it’s own crate. Otherwise, you’ll hit this error.
Helpful Docker guide
Guillaume Endignoux’s very thorough blog post, Compiling Rust libraries for Android apps: a deep dive, was super helpful for me. It is much more comprehensive that my post.
More than just UniFFI
There is a neat alternative to UniFFI called Diplomat for which Mark Hammond(from Mozilla) wrote a nice comparison, Comparing UniFFI with Diplomat.
I’m personally excited for uniffi-kotlin-multiplatform-bindings which is still new-ish but could really move the Kotlin ecosystem forward.
2023-07-05 Update
Lammert Westerhoff helpfully pointed out that if you run cargo build
with the --lib
flag (in step 4), the subsequent bin
additions to the Cargo.toml
(in step 5) won’t break future attempts at cargo build
ing. I’ve updated the code block in step 4 to include the --lib
flag.
heinrich5991 also mentioned something similar earlier, but I did not apply their feedback to my blog post. 🤦
Thank you to my friends
Special thanks to my friends who helped me with this post.
- Richard Moot - workshopping the title and hook
- Gary Guo - correcting my poor grammar and helping with the flow
- Ray Ryan - trying the recipe out, finding quite a few issues, and letting me know about them before I embarrassed myself
Let me know what you think!
Please feel free to reach out on email, the Fediverse @[email protected], or Twitter @SalTesta14.
-
… and other languages! ↩
-
On my 2016 MacBook Pro, the
cargo build
took ~1.5 minutes while thecross build
took ~6 minutes. ↩ -
Thank you to Tilmann Meyer in this GitHub thread for laying out the problem! Thank you to ssrlive and Caleb James DeLisle for the fixes. ↩