Building Pure Rust Apps for Android

Author

Marijn Suijten

Date

January 28, 2026

Reading time

8 min

Table of contents
Share article

This blog post was written in 2022 and recently migrated here. It no longer reflects our state of the art for building Android apps with Rust.

Rust is an amazing language. It has allowed us at Traverse Research to develop with much greater speed and courage thanks to additional safety guarantees (bar the few Foreign Function Interface crates that we maintain!) and tooling, but does that hold true for Android applications as well?

There are many ways to write Android apps. Not to mention the plethora of choices you get when also combining them with Rust code. A common use-case is embedding some Rust code — say a parser algorithm or networking infrastructure — inside an existing app. Assuming most apps are built in Android Studio and defined in gradle, Mozilla’s rust-android-gradle plugin is the perfect choice for this scenario: after some configuration it compiles your Rust crate to a Shared Object that is subsequently embedded in an APK (installable Android app Package) and ready to be invoked from Java or Kotlin.

1. App compilation internals

Going forward this story will cover pure Rust apps, which is how we build them at Traverse Research. Such apps contain no Java/Kotlin code and have to rely on whatever the Android system provides for so-called “native apps”.

Note that this story doesn’t intend to be a guide to using Rust on Android — rather serves to discuss the various options available for building Android binaries and applications, discuss technical details of their implementations, lay out caveats and finally figure out where to go next.

Let’s start by uncovering how a typical Android build environment is constructed, before diving into the (Rust!) tools that automate this for you.

First and foremost is Rust’s tier 2 compiler support for Android on popular architectures, most notably aarch64 and arm(v7) as used on most mobile phones and x86_64/i686 for desktop (emulator) usage. Without this you wouldn’t be able to compile Rust code for Android, but we unfortunately have to go beyond the Rust compiler and set up more.

Enter the linker: cargo invokes the host linker which isn’t able to compile applications for Android and doesn’t typically have support for the architectures mentioned above. In a traditional setup one would download Android’s official NDK [and utilize the binaries in there].

Unfortunately most crates pull in a non-Rust dependency somewhere. Those dependencies compile C or C++ code ad-hoc (instead of bundling pre-compiled libraries for a limited set of architectures, platforms and optimization flags), and typically use cc-rs to make compiler invocation painless. This crate flawlessly supports per platform and architecture compiler configuration.

Now that our Rust library is squared away, it is time to look at the remaining tools necessary to turn this into an installable Android app package. We start with aapt which is basically a glorified zip compressor that inserts and compiles XML files to a binary representation, and packages up other assets and resources that constitute an APK. In here goes your AndroidManifest.xml together with native libraries and — for “normal apps” — Java code.

This package subsequently has to be aligned and signed. A debug key, in the form of a Java Keystore is generated using keytool and used to sign an application. Alternatively a release key can be provided via crate metadata.

The resulting APK is now ready to be installed to an Android device of choice. During development adb — short for Android Debug Bridge — is typically used for quickly installing and relaunching an application, and it is the tool of choice to extract live logs from a running device.
The same APK file can be distributed to clients, but can unfortunately no longer be published to Google's Play Store which requires a newer AAB format.

2. Rust tools for compiling native apps

The Rust community provides multiple tools to build binaries and apps (APKs) for Android (disclaimer: I maintain and contribute to cargo-apk and xbuild). While different in approach and design, these tools have a few responsibilities in common:

  • Detect an Android NDK installation
  • Provide its linker to cargo for creating libraries and executables
  • Provide its compiler(s) to cc-rs for crates that need to compile native (C/C++) code
  • Make Android libraries available to the linker
  • Invoke Rust’s compiler (generally through the cargo build system) to compile the crate into an executable or shared object that runs on Android on the desired architecture

And that’s “all there is” to compiling Rust crates to runnable binaries on Android, and what our first tool for today, cargo-ndk, provides. This tool compiles libraries and command-line executable for Android, allowing them to be embedded in applications or invoked from adb shell.

That is only part of the effort to create an app for distribution and perhaps publishing on the Android App Store (Google Play), and where cargo-apk comes into play as part of the bigger rust-mobile ecosystem. We'll cover the contents of true Android apps in the next chapter.
cargo-apk does much the same as cargo-ndk, and builds an APK on top. This requires various extra steps, typically performed through utilities from Android’s NDK and SDK:

  • Parse extra metadata about a package from Cargo.toml (Rust’s way of describing a crate to the build system)
  • aapt: Package compiled Rust libraries and any other support libraries into an APK
  • Generate AndroidManifest.xml to explain to Android how to treat and expose the app to the user
  • zipalign: Align zip contents to make them mappable on Android
  • keytool: Generate a debug.keystore to sign the app, if it doesn’t exist yet
  • apksigner: Sign the application using debug.keystore
  • adb: Install the generated APK on a connected phone or emulator

This utility, while simple in design, has proven cumbersome to deploy and use. Its large set of dependencies on Android’s NDK and other SDK packages make it nontrivial to use out-of-the-box, especially on Windows. Parsing crate metadata hasn’t been optimal or complete either. Recent efforts from David Craven resulted in xbuild, a build-tool that abstracts most of this setup away while relying on more standard tools:

  • Houses cross-compilation for multiple platforms in one utility and manifest format: it supports Windows, Android, MacOS and iOS
  • Supports native Rust code as well as Flutter
  • LLVM / clang include support for aforementioned platforms out of the box: by invoking the host compiler with a cross-compilation --target it isn’t necessary to download an install a toolchain for every individual platform
  • Repackages a slimmed-down NDK containing only libraries and headers
  • Has built-in support for assets and icons
  • Provides native implementations of aapt, keytool, apksigner, and soon adb
  • Also in progress: support for Android App Bundles
  • Automatically follows and prints phone logs after installing the app through x run --device adb:<id>
  • And probably more that I have yet to discover 🥳

The tool works well enough for our purposes, but still misses some critical infrastructure around library detection and packaging that’s currently being fleshed out, but the community goal is to eventually supersede and replace cargo-apk.

A honourable mention goes out to cargo-mobile : this tool creates XCode and Android Studio projects that embed Rust crates. It is also actively maintained but different from the other approaches: we don’t use it and as such can’t comment on it.

In the end these tools only bring us so far. Our hopes and dreams are to embed these configuration and packaging stages directly into cargo, or provide them as a cargo extension. This rids us of fragile manifest parsing, incomplete commandline-argument hijacking and trying very hard to replicate every cargo intricacy when it comes to parsing and handling targets in crates, but is a much bigger endeavour that requires carefully defining how and what cargo communicates with such extensions. This expands beyond Android APK to support the likes of packages and platforms also provided by xbuild.

Share article

3. Activity lifecycle and “UI”

Native Android apps don’t look like your average (commandline) executable, but will always be associated with a physical window — a so-called Activity in Android terminology — on the screen. The Android framework contains a NativeActivity subclass of said Activity that loads a native library in which your (Rust) code resides, allowing apps to be created without any Java code/classes whatsoever. It serves as a bridge between Android’s app lifecycle and native code, forwarding events through callbacks. This lifecycle details how an app starts and moves to back- and foreground while users navigate between apps on their phones.

ndk-glue from rust-mobile provides a Rust abstraction over this NativeActivity and can be seen as a partial implementation of Android’s Native App Glue library in pure Rust. One of its consumers is winit, a cross-platform window creation and management crate. It provides the user an event loop to process window and input events, and a place to redraw screen contents.

In terms of “user interface”, NativeActivity provides access to a SurfaceView (more specific: its Surface / NativeWindow producer-counterpart) that spans the entire Activity “window”. One can blit pixels to it directly or create an OpenGL or Vulkan surface and use the respective graphics API to draw to it from the GPU.

4. Improving on the status-quo

Without diving too much into the inner workings of ndk-glue, it suffices to say that there are multiple active issues and attempts for improvement around this crate and the way it integrates with winit. Some have recently been solved, others require more careful planning and ecosystem-wide migrations to newer systems.

Firstly support for Android’s new GameActivity class. Or rather, part of the Android Game Development Kit. Unlike NativeActivity this class is distributed as an Android Archive (AAR) and must be bundled with the APK, containing all the support code that makes this Activity backwards-compatible down until SDK level 16 (Android 4.1).

Binding said class and using it in winit will come with a larger design shift for ndk-glue. Most of its issues stem from the fact that it only provides a thin API on top of NDK primitives, and requires “downstream crates” like winit to carefully follow its documentation (or as much as I’ve been able to write over the past year or so) to keep things working; especially around object synchronization. Redesigning ndk-glue and a GameActivity wrapper to sit behind a shared API that makes them interchangeable will automatically address some of those issues.

At the same time storage of “globals” in static variables continue to be a pain-point. For one because having multiple versions of ndk-glue (containing said static globals) in a dependency graph results in the variables being duplicated: if a user initializes ndk-glue 0.6 , but winit or app_dirs2 subsequently tries to pull one of those values out of ndk-glue 0.5, nothing will be available. We’ve solved this for some variables through a new ndk-context crate that won’t ever see a breaking release version.

However, there are important reasons for eliminating use of statics altogether. As a refresher for those aware of Android’s architecture: an app can have multiple Activity instances, and they all run within the same process. This means that any global reference to a single activity eventually end up referencing the wrong one. An API change has already been proposed that provides NativeActivity handles in the “main” callback only, and put the burden on the user to pass it on to winit — for example in a window constructor. This was found to be too disruptive of a change back in the day, but now seems to be the only way forward to defeat the issue.

Finally there are concerns that neitherNativeActivity nor GameActivity provide bindings for everything Android has to offer. Intents, onNewIntent and onActivityResult being the most commonly reported or asked about. It is possible to interop with Intents through JNI, yet it is not possible to insert the aforementioned callbacks into Activity classes unless they are subclassed, which requires some form of Java support in the build chain or precompiled Java classes provided by the crate.

All in all there’s still a lot to do to make Rust apps on Android truly viable in a professional context. Our main desire is to embed at least the necessary tooling to compile Rust crates into APKs directly in cargo. Stay tuned, and if you find this deeper dive in some of Android + Rust intricacies and issues interesting, feel free to reach and help out!