Skip to content
davthecoder
Rust on Android: How to Use It, How to Debug It, and Why It's Worth It
tech

Rust on Android: How to Use It, How to Debug It, and Why It's Worth It

By David Cruz Anaya, Independent Senior Mobile Engineer (Android, Kotlin Multiplatform, Rust)

Updated 23 July 2026
AndroidRustNDKJNIKotlinNative DevelopmentMemory Safety
Share:

Google now runs roughly five million lines of Rust inside the Android platform. In all the years that code has been shipping, exactly one potential memory safety vulnerability has been found in it, and that one was fixed before it ever reached a release (Google, Rust in Android: move fast and fix things, 2025). Google’s own history with C and C++ runs closer to a thousand memory safety bugs per million lines. A thousand versus 0.2 is the kind of gap that changes engineering decisions.

This post is the practical half of that story for app developers: how to wire Rust into a normal Android app with cargo-ndk and JNI, how to debug it when it misbehaves, and how to decide whether it deserves a place in your stack at all. It’s the companion to my C++ on Android guide, because the two languages compete for the same slice of your app.

Key Takeaways

  • In 2025, Google measured 0.2 memory safety vulnerabilities per million lines of Rust in Android, against roughly 1,000 for its C and C++ code (Google Security Blog, 2025).
  • You don’t need Google’s blessing to ship it. rustup targets, cargo-ndk and the jni crate (or UniFFI) put Rust in an ordinary APK today.
  • Debugging reuses the C++ toolbox: logcat via android_logger, tombstones through ndk-stack, LLDB in Android Studio. The one Rust twist is that panics abort at the JNI boundary.
  • Rust competes for the same narrow slice as C++: real-time audio, heavy compute, cross-platform cores. Kotlin stays the default for everything else.

This post is the hub of the Rust + NDK on Android series (a new part every Friday):

  1. The Complete Rust Toolchain Setup for Android: rustup, cargo-ndk, and Your First .so in an APK
  2. Calling Rust from Kotlin: JNI Basics for Android Developers (coming soon)
  3. UniFFI: Kotlin Bindings for Rust Without Writing JNI by Hand (coming soon)
  4. Project Structure: Gradle + Cargo in One Build, ABI Splits, Debug/Release Flavors (coming soon)
  5. Fast Image Processing on Android with Rust: Bitmaps, Filters, and the JNI Fast Path (coming soon)
  6. Passing Data Between Kotlin and Rust: Strings, Arrays, ByteBuffers, and Zero-Copy (coming soon)
  7. Memory Management Across JNI: Ownership, Box::into_raw, Global Refs, and Leaks (coming soon)
  8. Error Handling and Panics: Keeping Rust Crashes Out of Your Android App (coming soon)
  9. Threading and Async: Tokio, Kotlin Coroutines, and Calling Back Into Kotlin from Rust (coming soon)
  10. Low-Latency Audio DSP with Rust: AAudio and Oboe from a Rust Core (coming soon)
  11. Heavy Parsing with Serde: Offloading JSON and CSV Crunching to Rust (coming soon)
  12. Cryptography with Rust on Android: ring and RustCrypto Instead of Rolling Your Own (coming soon)
  13. Binary Size and the 16KB Page Requirement: Optimizing Rust .so Files for Release (coming soon)
  14. CI/CD for Rust + Android: GitHub Actions, Cross-Compilation Caching, Reproducible Builds (coming soon)
  15. Rust in Kotlin Multiplatform: One Native Core for Android and iOS, and When to Skip Rust Entirely (coming soon)

What does “Rust on Android” actually mean?

In the first three quarters of 2025, net new Rust code in the Android platform passed net new C++ for the first time (Google, Rust in Android: move fast and fix things, 2025). So when someone says “Rust on Android”, they can mean two quite different things: the Rust that Google ships inside the operating system, and the Rust you ship inside your own app.

The platform side is further along than most people realise. Since around Android 12, new components such as Keystore2, the Ultra-wideband stack, DNS-over-HTTP/3 and the Android Virtualization Framework have been written in Rust, and AOSP documents Rust as a first-class platform language. In October 2025 the Rust rewrite of Binder, the IPC mechanism every Android app touches on every screen, was merged upstream into Linux kernel 6.18 (Rust for Linux, Android Binder Driver, 2025). By 2022 AOSP carried about 1.5 million lines of Rust; by late 2025 it was roughly 5 million (Google Security Blog, 2022 and 2025).

Rust inside the Android platformRust inside the Android platformLines of Rust code in AOSP, per Google1.5M5M2022 (Android 13)2025In 2025, net new Rust in Android passed net new C++ for the first time.
Source: Google Security Blog, 2022 and 2025.

The app side is the part this post is about, and its architecture is identical to the C++ story. Your Rust compiles to a cdylib, a .so shared library per CPU architecture, packaged into the APK. Kotlin owns the UI, the lifecycle and the glue, and calls across the JNI border into your Rust core. If you’ve read my C++ on Android guide, the mental model transfers one to one.

One piece of honesty before the tooling: Google has no first-party NDK toolchain support for Rust in apps. The NDK’s tech lead has kept that position on the open roadmap issue since 2022: it’s not planned, and the recommended path is getting the Rust toolchain from rustup. In practice that costs you very little, because the community toolchain is mature. The compiler is official, the linker comes from the NDK you already have, and cargo-ndk joins the two.

Why use Rust instead of C++ for the native layer?

Because the safety data is no longer arguable, and the velocity data now points the same way. In 2019, memory safety bugs were 76 percent of Android’s vulnerabilities. By 2022 they were down to 35 percent, the first year they were not the majority. In 2024 they hit 24 percent, against an industry norm of about 70 percent, and in 2025 they fell below 20 percent for the first time (Google Security Blog, 2022 to 2025). Google credits one policy: write new native code in memory safe languages, and let the old unsafe code age.

Memory safety share of Android vulnerabilitiesMemory safety share of Android vulnsShare of total Android vulnerabilities, per Google~70% industry norm76%35%24%<20%2019202220242025
Source: Google Security Blog, “Memory Safe Languages in Android 13” (2022), “Eliminating Memory Safety Vulnerabilities at the Source” (2024), “Rust in Android: move fast and fix things” (2025).

The surprise in the 2025 numbers is that safety didn’t come at the cost of speed. For medium and large changes, Rust changes in Android have a roughly four times lower rollback rate than C++ changes, they spend about 25 percent less time in code review, and similar-sized changes need about 20 percent fewer revisions (Google, Rust in Android: move fast and fix things, 2025).

Lars Bergstrom, a director of engineering on Android, put the broader claim on stage at Rust Nation UK in 2024: every time Google rewrote a C++ service in Rust, the effort to build and maintain it dropped by more than half (The Register, 2024). His talk, Beyond Safety and Speed: How Rust Fuels Team Productivity, is worth an hour of your time. Developers seem to agree with the data: in 2025, Rust was the most admired language in the Stack Overflow survey at 72.4 percent (Stack Overflow Developer Survey, 2025).

Rust velocity in Android, per GoogleThe safer path is the faster oneRust vs C++ in the Android platform, 20251000xlower vuln density4xlower rollback rate25%less time in reviewRollback figure covers medium and large changes.
Source: Google, “Rust in Android: move fast and fix things”, 2025.

From experience: I came to Rust from C++ on Android, mostly through audio work like Klarinet. The bug I fear most on an audio thread isn’t the crash, it’s the data race that corrupts a buffer once a week on one device model. In C++ that class of bug lives in code review and in my head. In Rust the borrow checker turns it into a compile error, and an entire category of 2am crash reports stops existing before the app is even installed.

C++ hasn’t lost every argument, though. If your native layer exists to wrap FFmpeg, OpenCV or a vendor’s C++ SDK, writing the wrapper in C++ is still the path of least friction, and I say so in detail in the C++ guide. Rust’s advantage compounds when the native code is yours, the fresh DSP or engine core you’ll still be maintaining in five years.

How do you add Rust to an Android app?

You need four pieces: a Rust toolchain with Android targets, the NDK for its linker and sysroot, cargo-ndk to glue them together, and a JNI bridge on both sides. Rust treats Android as a Tier 2 platform with six supported targets and tracks the latest LTS NDK, which is r27d as of mid-2026 (rustc book; Android Developers). Since NDK r26 the floor is API level 21, so Rust runs on effectively every device you still support.

Developer typing on a laptop with a code editor open on the screen.

Install the targets you ship, plus cargo-ndk, which points cargo at the right NDK linker per architecture:

rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
cargo install cargo-ndk

Create a library crate that builds a shared library, with the jni crate for the bridge:

[package]
name = "rustcore"
version = "0.1.0"
edition = "2021"

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

[dependencies]
jni = "0.21"

I’m pinning jni 0.21 on purpose. The 0.22 release shipped a redesigned entry-point API built around EnvUnowned and closures instead of a bare JNIEnv, and almost every example, answer and production codebase you’ll meet is still written in the 0.21 style below. Both are current; don’t mix them in one crate.

The exported function name is not decoration. JNI resolves it by pattern, Java_ plus the package, class and method with underscores, so the Rust side of a greet method on com.davthecoder.rustdemo.RustBridge looks like this:

use jni::objects::{JObject, JString};
use jni::sys::jstring;
use jni::JNIEnv;

#[no_mangle]
pub extern "system" fn Java_com_davthecoder_rustdemo_RustBridge_greet<'local>(
    mut env: JNIEnv<'local>,
    _this: JObject<'local>,
    name: JString<'local>,
) -> jstring {
    let name: String = env
        .get_string(&name)
        .map(Into::into)
        .unwrap_or_default();
    let reply = format!("Hello from Rust, {name}");
    env.new_string(reply)
        .expect("valid UTF-8")
        .into_raw()
}

The Kotlin side is a plain external declaration and a library load:

object RustBridge {
    init {
        System.loadLibrary("rustcore")
    }

    external fun greet(name: String): String
}

One command builds every architecture and drops the .so files where the Android Gradle Plugin already looks:

cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 \
  -o app/src/main/jniLibs build --release

Run that before Gradle packages the APK, either by hand while you iterate or as a Gradle exec task in CI. Mozilla also maintains a rust-android-gradle plugin if you prefer the build wired into Gradle itself, though it moves slower than cargo-ndk these days. And if your team is coming from Kotlin or C++, Google’s own training course for its Android engineers, Comprehensive Rust, is free; there is a good talk on how they run it, Rust Training at Scale.

Skip the hand-written JNI with UniFFI

UniFFI is the way out of JNI boilerplate. Mozilla’s binding generator reached version 0.32 in June 2026 and generates the Kotlin for you from annotated Rust, including data classes, enums, interfaces and typed errors (mozilla/uniffi-rs). Mozilla uses it across Firefox for Android and iOS, and Element X, the current Matrix client, ships its entire engine, the 99.9 percent Rust matrix-rust-sdk, through UniFFI-generated Kotlin and Swift bindings.

Instead of hand-matching mangled function names, you annotate the Rust and let the generator do the border paperwork:

#[derive(uniffi::Record)]
pub struct Totals {
    pub count: u32,
    pub cents: i64,
}

#[uniffi::export]
pub fn summarise(amounts: Vec<i64>) -> Totals {
    Totals {
        count: amounts.len() as u32,
        cents: amounts.iter().sum(),
    }
}

uniffi::setup_scaffolding!();

The generated Kotlin gives you summarise(listOf(1200L, 350L)) returning a real data class, with strings, collections and errors marshalled for you. For any API surface bigger than a handful of functions, this is the difference between a weekend and a month.

A Rust core under a native UI shows up in more shipping apps than you’d think. I only kept apps I could verify against a primary source: each name below links to Google Play, each description to the engineering source that documents the Rust.

App What’s written in Rust
Signal The Signal Protocol core, libsignal, reaching Kotlin over JNI
Element X The entire Matrix engine, matrix-rust-sdk
Wire MLS and Proteus encryption via core-crypto and UniFFI
Threema The libthreema protocol library
Delta Chat Networking and encryption in the chatmail core
1Password The shared 1Password Core, written primarily in Rust
Bitwarden Vault crypto and client logic via a UniFFI Rust SDK
Proton Pass The shared password and TOTP core, proton-pass-common
1.1.1.1 + WARP The WireGuard tunnel itself, BoringTun
Mullvad VPN The same Rust daemon that powers its desktop apps
ExpressVPN The Lightway protocol, rewritten from C in Rust in 2025
NymVPN The whole VPN engine, nym-vpn-core
Firefox Accounts, sync and logins via application-services
Brave The default adblock engine
Google Chrome Font rendering, the Fontations stack, for all web fonts since Chrome 133
Tuta Mail The tuta-sdk encryption core
Bitkey Bitcoin and key management, Rust inside a Kotlin Multiplatform app
Zodl Shielded-transaction logic via the Zcash Android SDK
AppFlowy The data and persistence layer under a Flutter UI, per their tech design

That table has a theme: messengers, password managers, VPNs, wallets. When cryptographic correctness is the product, the case for a memory safe core writes itself. And Typeshare, 1Password’s own tool, is worth a look even if you never touch their app: it generates matching Kotlin, Swift and TypeScript types from annotated Rust, and it’s open source.

Stuart Harris’s talk on sharing a single Rust core across iOS, Android and web is a good tour of the architecture behind most of these. It slots naturally under a Kotlin Multiplatform app too: KMP owns the business logic, Rust owns the native core, the same layering I describe in my KMP clean architecture blueprint, and the same shape Block ships in Bitkey.

How do you debug Rust on Android?

Mostly with the same toolbox as C++, plus one Rust-specific rule you need to know up front. Since Rust 1.81 in September 2024, a panic that unwinds out of a non-unwinding ABI, which includes the extern "system" functions JNI calls, aborts the process instead of being undefined behaviour (Rust Blog, Announcing Rust 1.81.0, 2024). On Android that means an unhandled panic inside a JNI call kills your app as a native crash. So the debugging story starts with making panics visible before they take the process down.

Get logs and panics into logcat

The android_logger crate, at 0.15 as of 2025, routes the standard log macros to logcat, and a panic hook makes every panic log its message and location before the abort:

use android_logger::Config;
use log::LevelFilter;

pub fn init_diagnostics() {
    android_logger::init_once(
        Config::default()
            .with_max_level(LevelFilter::Debug)
            .with_tag("rustcore"),
    );
    std::panic::set_hook(Box::new(|info| {
        log::error!("rust panic: {info}");
    }));
    log::info!("rustcore diagnostics up");
}

Call it once from a JNI init function when the library loads, then watch it with adb logcat -s rustcore. For boundaries that must never crash the app, wrap the body in std::panic::catch_unwind and translate the panic into an error return or a thrown Kotlin exception instead.

Close-up of source code in a dark editor on a laptop screen.

Read the native crash like a C++ one

When the process does die, Android writes a tombstone, and ndk-stack turns its raw addresses back into file and line numbers using your unstripped libraries:

adb logcat | $ANDROID_NDK_HOME/ndk-stack \
  -sym target/aarch64-linux-android/release

One catch: since Rust 1.77, Cargo strips debug info from release builds by default, and ndk-stack needs it. Set debug = true and strip = "none" under [profile.release] in Cargo.toml, keep the fat .so cargo produces for symbolication, and let APK packaging strip the copy you actually ship. A production crash then symbolicates down to the exact .rs line.

Breakpoints and profiling in Android Studio

Android Studio debugs Rust the way it debugs C++: it attaches LLDB for native code alongside the JVM debugger, with four debug types (Detect Automatically, Java Only, Native Only and Dual). With native code in the project, automatic detection attaches both debuggers, so you can step from a Kotlin call site into the Rust implementation in one session. Rust compiles to the same DWARF debug format as C++, so breakpoints, watch expressions and stack traces work without ceremony. Profiling is the same story: Simpleperf, the NDK’s CPU profiler, and the Android Studio profiler that fronts it see your Rust .so as ordinary native code.

What I actually do: I keep the crate pure Rust with a thin JNI or UniFFI skin, and debug almost everything with cargo test on my laptop. The core never touches an emulator until the logic is proven, and once it is, nearly every remaining bug lives in the thin bridge layer: a wrong signature, a lifetime crossing the boundary, a thread without a JNIEnv. Structure the code so the device is only ever debugging the border, not the country.

When should you say no to Rust on Android?

More often than the enthusiasm suggests, and the retraining data says why. Google found about a third of C++ developers feel as productive in Rust within two months, and about half within four (The Register, 2024). That’s a real cost for a small team, and it buys nothing if you didn’t need native code in the first place.

The decision rule from the C++ post transfers unchanged: most apps need zero native code, and you go native only after profiling shows the JVM itself is the wall. Past that gate, the choice usually falls out of a short table:

Your situation Reach for
Wrapping FFmpeg, OpenCV or a vendor C++ SDK C++
New DSP, engine or core you’ll own for years Rust
Sharing business logic across Android and iOS Kotlin Multiplatform
No profiled bottleneck, no native experience on the team Kotlin, and stop there
Fresh real-time audio or render path Rust first, C++ if an existing ecosystem pulls you

If your goal is sharing business logic across platforms, Kotlin Multiplatform remains the right default, and Rust sits underneath it for the genuinely native slice, not instead of it. And if your native layer is mostly a window onto an existing C++ ecosystem, staying in C++ avoids a bilingual FFI sandwich that nobody wants to maintain.

What’s changed is the default for new native code. Five years ago, choosing Rust for an Android audio engine or crypto core was a bet on tooling that might mature. In 2026, with Google landing more new platform code in Rust than C++ and the app-side toolchain stable, writing fresh native code in C++ is the choice that needs defending.

If you’re weighing this call for a real codebase, an audio pipeline, say, or a core you want to share with iOS, it’s the kind of decision I help teams make in architecture reviews and mentoring, and the kind of native work I take on as an independent engineer.

Frequently Asked Questions

Do I need the NDK to use Rust on Android? Yes. The Rust compiler comes from rustup, but linking needs the NDK’s linker and sysroot, which cargo-ndk wires up per target. Rust supports six Android targets at Tier 2 and tracks the latest LTS NDK, r27d as of mid-2026, with a minimum of API level 21 since NDK r26.

Is Rust officially supported for Android app development? The platform side is official: AOSP documents Rust as a first-class language for OS components. For apps, Google’s NDK team has said first-party toolchain support is not planned, and recommends the rustup toolchain. In practice cargo-ndk plus the NDK covers the gap, and production apps have shipped this way for years.

How do I see Rust panics and crashes in logcat? Route logs with android_logger, then install a panic hook so panics log before the process dies. Since Rust 1.81 in 2024, a panic escaping a JNI entry point aborts the app. For the crash itself, feed the tombstone through ndk-stack with your unstripped libraries to get exact file and line numbers.

Can I use Rust together with Kotlin Multiplatform? Yes, and it layers cleanly: KMP shares your Kotlin business logic, while the Rust core builds as a cdylib for Android and a static library for iOS. Element X ships this shape at scale, driving both apps from the matrix-rust-sdk through UniFFI-generated Kotlin and Swift bindings.

Which production apps already use Rust on Android? The list is longer than most expect. Signal, Element X, Wire, Threema and Delta Chat run Rust protocol cores. 1Password, Bitwarden and Proton Pass do their vault crypto in Rust. Cloudflare’s 1.1.1.1, Mullvad, ExpressVPN and NymVPN tunnel through Rust, and Chrome has rendered all web fonts with Rust since version 133. The linked table above has all nineteen.

Is Rust faster than Kotlin on Android? For the narrow slice that matters, yes: Rust compiles to native code with no garbage collector, so it holds real-time deadlines the JVM can’t guarantee, same as C++. For ordinary app code the JVM is plenty fast, which is why the honest answer is that most of your app should stay in Kotlin.

The short version

Use Kotlin until profiling proves you need native code, same as before. What’s changed is what you write on the day that proof arrives. Google didn’t move Android’s native development to Rust out of fashion; it measured its way there over six years, and the app-side toolchain has quietly caught up. My default for new native code on Android is now Rust. The C++ I still write is there to talk to C++ that already exists.

If you want a second pair of senior eyes on that decision for your own codebase, that’s what my architecture reviews are for.

Sources

Share:

Comments

Loading comments…