Rust for Android App Developers: When It Makes Sense (And When It Doesn't)

ExtensionBooster Team · · 14 min read
Close-up of code editor showing Rust programming language syntax with Android project structure

Rust android development has a perception problem. Most of the tutorials out there are written for OS contributors patching AOSP, not for the developer building a real app on the Play Store. The result is a sea of low-level systems content that leaves app developers with one burning question: is any of this actually useful to me?

Short answer: yes, but in a narrower set of situations than the hype suggests.

This guide is written for Android app developers. Not kernel hackers. Not AOSP contributors. If you’re building apps in Kotlin and wondering whether Rust belongs anywhere in your stack, this is the practical breakdown you haven’t found yet.


TL;DR

  • Rust makes sense for one hot-path library (image processing, crypto, compression) where you need C/C++ speed without the memory bugs.
  • You won’t rewrite your app in Rust. Nobody does. The goal is a narrow Rust library called from Kotlin via JNI.
  • The FFI chain (Rust FFI to Android NDK to JVM JNI) is the real pain point. Plan for it.
  • Google is already betting on Rust in AOSP itself, which means toolchain support is only getting better.
  • For most apps, Kotlin all the way is still the right call. Don’t add Rust complexity without a measured reason.

Why Rust Is Gaining Ground in Android

Google started adding Rust to AOSP in 2021. By 2023, there were over a million lines of Rust in the Android codebase. By 2026, new memory-safety-critical components in Android are being written in Rust by default.

The reason is simple: Android’s most serious security vulnerabilities have historically been memory safety bugs. Buffer overflows, use-after-free, data races. C and C++ are capable of all of them. Rust’s ownership model makes the entire class of bugs impossible to compile.

That’s the Google story. For app developers, the calculus is different but the core benefit is the same. When you need native code performance, you used to have two options: take the C++ risk or accept the performance ceiling of the JVM. Rust gives you a third path.

One commenter in a recent r/androiddev discussion put it plainly: most app devs need Rust for exactly one thing, a single hot-path library, not a full rewrite. That framing is the right one to carry through this guide.


Does Google Actually Use Rust in Android?

Yes, and significantly.

The Android Open Source Project (AOSP) now includes Rust in the Bluetooth stack, DNS resolver, keystore, and various media components. Google has published comprehensive tooling and documentation for Rust in AOSP and even released a free Comprehensive Rust course used internally to train engineers.

The organizational commitment is real. That matters for app developers because it means:

  • The Android NDK team is motivated to keep Rust toolchain support working.
  • cargo-ndk and related tooling get maintained against new NDK releases.
  • Rust’s standard library support for Android targets gets better over time.

You’re not betting on a niche. You’re betting alongside Google’s own infrastructure team.


Can Rust Replace C++ in Android NDK?

Technically yes. Practically, it depends on what you’re replacing and why.

Where Rust wins over C++:

  • Memory safety without a garbage collector. No use-after-free, no buffer overflows, no data races, enforced at compile time.
  • Modern tooling. cargo is a dramatically better build experience than CMake or Bazel for native Android code.
  • Better FFI ergonomics when using tools like cbindgen to generate C headers automatically.
  • Growing ecosystem of pure-Rust crates for cryptography, compression, parsing, and media.

Where C++ still wins:

  • Existing codebase. If you have 200,000 lines of working C++, you’re not rewriting it in Rust without a compelling reason.
  • NDK-specific APIs. The Android NDK exposes C/C++ APIs directly. Rust calls them through the FFI boundary, which adds a thin layer of boilerplate.
  • Established team knowledge. C++ expertise is easier to hire for right now than Rust expertise.

The honest answer: for new native Android code where you have a choice, Rust is increasingly the better default. For existing C++ code, the migration cost rarely justifies itself unless you have specific memory safety incidents driving the decision.


The Double-Binding Problem: What Nobody Warns You About

Here’s the part that trips up every developer who tries Rust for Android without reading enough first.

When you call Rust from an Android app, your call goes through three layers:

  1. JVM (Kotlin/Java) calls a native function via JNI (Java Native Interface).
  2. JNI lands in a C-compatible function that’s part of the Android NDK.
  3. That C function calls into your Rust library via FFI (Foreign Function Interface).

So your Kotlin code doesn’t call Rust directly. It calls JNI. JNI calls C. C calls Rust. Three layers, three potential failure modes.

The community calls this the double-binding problem: you need a Rust FFI binding for your native Rust types, and you need a JNI binding to expose those types to the JVM. Keep those two layers clean and well-typed, and it’s manageable. Let them blur together, and you end up with unsafe code scattered everywhere.

The simplest working approach is a clear three-file structure:

android-app/
  app/
    src/main/
      java/com/yourapp/
        NativeLib.kt       ← Kotlin interface
  rust-lib/
    src/
      lib.rs               ← Rust implementation
      jni_bridge.rs        ← JNI boundary, unsafe lives here

Keep all your unsafe in jni_bridge.rs. Your lib.rs stays clean idiomatic Rust. Your Kotlin stays clean idiomatic Kotlin. The bridge is the controlled mess.


How to Call Rust from Kotlin via JNI

Here’s a working minimal example. This is a Rust image processing function exposed to Kotlin.

Step 1: Write the Rust library

// rust-lib/src/lib.rs

/// Converts raw RGBA bytes to grayscale in-place.
/// Pure safe Rust, no JNI knowledge here.
pub fn rgba_to_grayscale(data: &mut [u8]) {
    for chunk in data.chunks_mut(4) {
        let gray = (0.299 * chunk[0] as f32
            + 0.587 * chunk[1] as f32
            + 0.114 * chunk[2] as f32) as u8;
        chunk[0] = gray;
        chunk[1] = gray;
        chunk[2] = gray;
        // chunk[3] is alpha, leave it alone
    }
}

Step 2: Write the JNI bridge

// rust-lib/src/jni_bridge.rs

use jni::JNIEnv;
use jni::objects::JByteArray;
use jni::sys::jint;

// This function name must match: Java_<package>_<class>_<method>
// com.yourapp.NativeLib → Java_com_yourapp_NativeLib_rgbaToGrayscale
#[no_mangle]
pub extern "C" fn Java_com_yourapp_NativeLib_rgbaToGrayscale(
    env: JNIEnv,
    _class: jni::objects::JClass,
    pixel_data: JByteArray,
) -> jint {
    let result = std::panic::catch_unwind(|| {
        let mut data = env
            .convert_byte_array(&pixel_data)
            .expect("failed to read byte array");

        crate::rgba_to_grayscale(&mut data);

        env.set_byte_array_region(&pixel_data, 0, unsafe {
            // Safe: we're writing back the same length we read
            std::slice::from_raw_parts(data.as_ptr() as *const i8, data.len())
        })
        .expect("failed to write back byte array");
    });

    if result.is_err() { -1 } else { 0 }
}

Step 3: Declare the Kotlin interface

// app/src/main/java/com/yourapp/NativeLib.kt

object NativeLib {
    init {
        System.loadLibrary("yourlib")
    }

    // Return value: 0 = success, -1 = error
    external fun rgbaToGrayscale(pixelData: ByteArray): Int
}

Step 4: Call it from your Android code

val bitmap = BitmapFactory.decodeResource(resources, R.drawable.photo)
val pixels = ByteArray(bitmap.byteCount)
bitmap.copyPixelsToBuffer(ByteBuffer.wrap(pixels))

val result = NativeLib.rgbaToGrayscale(pixels)
if (result == 0) {
    bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(pixels))
    imageView.setImageBitmap(bitmap)
}

That’s the core pattern. Rust does the computation, JNI is the controlled bridge, Kotlin stays readable.

Developer writing Rust and Kotlin code on a dual-monitor setup


cargo-ndk Setup and the Version Mismatch Problem

cargo-ndk is the tool that makes building Rust for Android targets tolerable. It wraps cargo build with the right NDK environment variables and cross-compilation targets.

Basic setup:

# Install cargo-ndk
cargo install cargo-ndk

# Add Android targets
rustup target add \
  aarch64-linux-android \
  armv7-linux-androideabi \
  x86_64-linux-android \
  i686-linux-android

# Build for all targets
cargo ndk -t armeabi-v7a -t arm64-v8a -t x86 -t x86_64 \
  -o ../app/src/main/jniLibs build --release

That -o flag tells cargo-ndk to drop the compiled .so files directly into Android’s jniLibs directory, organized by ABI. Gradle picks them up automatically on the next build.

The version mismatch problem is real. Multiple developers in the r/androiddev thread flagged it: cargo-ndk version, NDK version, and rustup target version need to be compatible with each other. The combination that breaks most often is a new NDK release + an old cargo-ndk that doesn’t know about it.

Practical advice:

  • Pin your NDK version in local.properties (ndk.version=26.3.11579264).
  • Pin your cargo-ndk version in CI (cargo install cargo-ndk --version X.Y.Z).
  • Check cargo-ndk’s GitHub releases when you upgrade the NDK. They’re usually in sync within a few days of NDK releases.

Don’t let these be floating dependencies. Reproducible builds are worth the explicit version pinning.


Is Rust Faster Than C++ on Android?

Performance-wise, Rust and C++ compile to essentially equivalent native code. Both compile to LLVM IR, which means the optimizer sees the same picture. Micro-benchmarks show results within single-digit percentage differences, favoring either language depending on the specific operation.

What Rust does differently isn’t raw speed, it’s what it avoids. Memory bugs in C++ don’t just cause crashes. They cause unpredictable memory access patterns that defeat CPU cache prefetchers, introduce branch mispredictions, and cause sporadic slowdowns that are nearly impossible to reproduce in benchmarks. Rust’s ownership model makes those patterns impossible.

The Zed editor’s Android port is a real-world proof point. Zed uses Rust with Vulkan for rendering, targeting native Android UI at full GPU speed. The result is an editor that runs at the same frame rate on Android as it does on desktop, without a JVM in the rendering path. That’s not possible in pure Kotlin, and it’s evidence that Rust plus the NDK can genuinely match C++ for GPU-adjacent workloads.

For typical app hotpath work (image processing, cryptography, data compression, audio processing), expect:

  • 5x to 15x faster than equivalent Kotlin code on the JVM.
  • Comparable to C++ for the same algorithm, within measurement noise.
  • Safer than C++ with no runtime cost for that safety.

When Should You Actually Use Rust?

Be honest with yourself before reaching for Rust. The FFI chain has a real cost in developer time, toolchain complexity, and debugging difficulty. It’s not a cost you should pay without a measured reason.

Rust makes sense when:

  • You have a specific function that’s measured as the bottleneck in a profiler, not assumed.
  • That function does pure computation: pixel manipulation, cryptographic operations, compression, audio processing, parser-heavy work.
  • You want memory safety guarantees that matter, for example, processing untrusted user-uploaded images.
  • You’re starting a new native component and don’t have C++ to migrate from.

Rust doesn’t make sense when:

  • Your bottleneck is network I/O, database queries, or UI rendering. Rust won’t help with any of those.
  • Your team has no Rust experience and your deadline is in two months.
  • You’re looking for a way to share business logic between Android and iOS. Flutter, KMP, or a React Native approach will serve you better.
  • You want to rewrite your whole app. You don’t. Nobody does this successfully.

The single-library pattern is the right mental model. Identify one function. Profile it. Write that function in Rust. Ship it. Measure. That’s the entire playbook for most apps.


FAQ

Can Rust replace C++ in Android NDK development?

For new code, yes, Rust is a viable replacement for C++ in Android NDK work. It offers equivalent performance with stronger memory safety guarantees and a better build experience via Cargo. For existing C++ codebases, full replacement is rarely practical. The more useful framing is: write new native Android components in Rust instead of C++ when you have a choice.

How do I call Rust from Kotlin?

You call Rust from Kotlin using JNI (Java Native Interface). The pattern is: declare an external fun in Kotlin, implement a C-compatible function in Rust with a name matching the JNI naming convention (Java_<package>_<class>_<method>), compile the Rust to a shared library (.so) using cargo-ndk, and load it at runtime with System.loadLibrary(). The code examples above show the full working pattern.

Is Rust faster than C++ on Android?

In raw throughput, Rust and C++ perform equivalently on Android because both compile to native code via LLVM. The performance advantage of Rust over C++ comes from the elimination of memory safety bugs that can cause unpredictable cache misses and branch mispredictions. Compared to Kotlin on the JVM, Rust is typically 5-15x faster for CPU-bound work.

Does Google use Rust in Android?

Yes. Google began integrating Rust into AOSP in 2021 and by 2026 there are multiple millions of lines of Rust in Android’s codebase, including in the Bluetooth stack, DNS resolver, keystore, and media components. Google also provides tooling documentation and a free Comprehensive Rust course used to train Android engineers internally.

What is cargo-ndk and how do I use it?

cargo-ndk is a Cargo subcommand that simplifies cross-compiling Rust for Android’s supported CPU architectures (arm64-v8a, armeabi-v7a, x86, x86_64). It handles the NDK environment configuration that would otherwise require manual setup. Install it with cargo install cargo-ndk, then use cargo ndk -t arm64-v8a build --release to compile for a specific ABI. Pin both the cargo-ndk version and NDK version to avoid the common version mismatch problems discussed above.

What is the double-binding problem in Rust Android development?

The double-binding problem refers to the two-layer translation required to call Rust from a Kotlin Android app. First, you need a JNI binding that exposes a C-compatible function to the JVM. Second, you need an FFI binding from that C function into your Rust code. Managing these as separate, clean layers (keeping all unsafe code isolated in a bridge file) is the key to making it maintainable.


Share this article

Build better extensions with free tools

Icon generator, MV3 converter, review exporter, and more — no signup needed.

Related Articles