# rust-mobile/ndk: raw and safe Rust bindings for the Android NDK

> The rust-mobile/ndk repository publishes two crates, ndk-sys for the raw FFI surface of the Android NDK and ndk for a safe abstraction over it, with different minimum Rust versions. It is a small, focused binding project whose scope is the NDK and nothing more, and the README is short enough to say exactly that.

**rust-mobile/ndk** —  Rust bindings to the Android NDK

- Repository: https://github.com/rust-mobile/ndk
- Stars: 1,321 · Forks: 128
- Language: Rust
- License: Apache-2.0
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/rust-mobile-ndk

## Two crates, and the split is the design

The repository is described as Rust bindings to the Android NDK, and the whole thing is two crates.

ndk-sys is the raw FFI bindings to the NDK. ndk is a safe abstraction of the bindings. That split is the convention the Rust ecosystem uses for platform bindings, and it is worth understanding why it is there rather than treating it as two ways of doing the same thing.

The raw crate is what a bindgen-style generator produces: the C declarations of the NDK turned into extern functions, with the types the C API uses. It gives you everything the platform offers and nothing more. It also gives you the whole C API surface: pointers that can be null, integers that have to be cast, error codes that have to be checked, and ownership rules that are only documented in a header comment somewhere in the NDK.

The safe crate is the layer that takes the raw surface and gives it Rust's type system. That is where the lifetimes appear, where the nullability becomes an Option, where the ownership of an Android object becomes a type that says who has to release it. The cost is that the safe crate covers what its author decided to wrap, and anything not wrapped is not available through it.

The two crates therefore have different minimum supported Rust versions, and the README states them explicitly. ndk-sys carries an MSRV badge of rustc 1.60.0+, and ndk carries one of rustc 1.64.0+. The safe layer needs the newer compiler because the abstractions it builds use language features the raw layer does not. For a project with an old pinned toolchain, that is the number to look at first, because the safe crate is the one you probably want.

The repository top level reflects the two-crate structure precisely. There is a Cargo.toml at the root declaring a workspace with resolver = "2" and two members, ndk and ndk-sys, and then a directory for each. The root also carries LICENSE-APACHE and LICENSE-MIT, a rustfmt.toml, and a .github/ directory with the CI workflow. There is no build script, no code generation directory and no examples directory, which is itself a statement about the project's scope: it publishes bindings, and the examples live elsewhere.

## The FFI boundary, and why raw bindings are a separate crate

The reason the raw crate exists as a separate published artifact rather than being an internal detail of the safe crate is a matter of layering, and it has practical consequences for a consumer.

A safe wrapper over an FFI surface needs a way to reach the underlying functions. The usual pattern is for the safe crate to depend on the raw crate and to re-export it, so that a user who finds an API the safe layer has not wrapped can drop down to the raw one and call it directly, keeping the same types and the same ABI. Splitting the crates at the published level makes that drop-down possible without the safe crate having to wrap every NDK function itself, and it means the raw layer can be regenerated and released independently of the safe layer's ergonomics work.

That is the design rationale. The practical consequence for you is that ndk-sys is the crate that has to stay in step with the NDK, because it mirrors the platform's C headers, while ndk is the crate that changes when the author's API design changes. A new NDK revision with new headers does not necessarily produce a new ndk release, and a new ndk release does not necessarily require a new NDK.

The release list shows the two moving at their own pace. ndk-sys 0.6.0 and ndk 0.9.0 were both published on 2024-04-26, seconds apart, which is the coordinated release you would expect for a wrapper and its base. Before that, ndk-sys 0.5.0 was published on 2023-10-18, with no matching ndk release in the list at all. So the raw crate is released more often, and the safe crate's releases track the raw ones when the safe layer's own work lands.

The licence arrangement is also layered. The README carries two licence badges, MIT and APACHE2, and the repository has LICENSE-APACHE and LICENSE-MIT files. That is the standard Rust dual licence, and it is the same arrangement the generated bindings inherit, since the bindings describe an interface rather than copying implementation.

For a reader who wants to know what is inside the surface, the honest answer is that the README does not enumerate the API. The type definitions live in the ndk-sys crate and its documentation on docs.rs, and the README's job is to point at the two crates and the examples.

## Where the examples live, and what the README actually tells you

The README is short, and the most useful thing in it is the pointer to the examples.

See these ndk-examples and these rust-android-examples for examples using the NDK. The first is a directory in the rust-mobile/cargo-apk repository, and the second is a separate rust-mobile/rust-android-examples repository.

That split is informative. cargo-apk is the tool for building Android applications from Rust, and it carries the NDK examples with it because that is where the build integration lives. The rust-android-examples repository is the broader set, and it is where a Rust project that is not being built with cargo-apk would look. For anyone starting with this, the examples are the documentation, and the README says so by pointing rather than by describing.

The CI setup is described by a single badge. There is a rust.yml workflow triggered on push to master, which is the build and test workflow for the two crates. The topic list is short, android, android-ndk and rust, which is consistent with a project whose entire surface is one platform's C API.

What the README does not contain is worth naming, because a reader arriving at the repository from a search result will notice the gaps. There is no installation section, so the route to the crates is the crates.io badges and the workspace Cargo.toml. There is no version compatibility table mapping ndk releases to NDK revisions, so the pairing between ndk-sys 0.6.0 and a particular NDK is something a consumer has to establish for themselves. There is no changelog file in the tree, so the release notes on the GitHub releases are the record. And there is no statement of which NDK APIs the safe ndk crate covers.

None of that is fatal for a binding crate, whose users are usually the people who will read its docs.rs page anyway. But it does mean the evaluation has to be done in docs.rs and in the examples rather than in the README, and for a library that is a well-trodden path: the ndk-sys docs are the generated API reference, and the ndk crate's documentation is the safe surface.

## The alternative: binding the NDK yourself

The comparison to make is not with another Rust binding crate, since ndk-sys and ndk are the reference implementations in this ecosystem, but with the alternative of not using a crate at all.

The first alternative is to write the extern declarations yourself. The NDK ships its headers, and bindgen will turn them into Rust for you. For a small set of functions that is entirely reasonable, and it has the advantage that you get exactly the API surface you need and nothing else. The cost is that you own the generated code, the build integration and the correctness of the declarations, and bindgen has to run as part of your build or be checked in and regenerated on every NDK bump. The ndk-sys crate is precisely that work, done once and published.

The second alternative is to avoid the NDK entirely. Most Rust code that needs to talk to Android does not need the NDK at all, because the interesting operations cross the JNI boundary: calling into the JVM, or being called from it. The tooling around that is a different set of crates, and the direction of the call determines what you need. If you are writing a library that a Kotlin or Java app calls, you need a JNI surface, not the NDK's windowing and input APIs. If you are writing a Rust application that owns the surface, you need the NDK.

The third alternative is a different framework. Game engines and cross-platform UI toolkits that target Android ship their own NDK bindings or their own C++ shims, and if you are building a game with one of those, you are already holding an NDK surface and a second binding on top of it is a liability rather than an asset.

So the question is narrow and worth answering directly. Do you need the Android NDK's own C API, as opposed to the JVM, and are you writing the Android-facing layer in Rust rather than reaching it through an engine? If both answers are yes, these two crates are the shortest path, and the raw/safe split lets you start safe and drop down only where you have to. If either answer is no, you are looking at the wrong crate for the job, and the JNI-shaped alternatives are the right ones.

## Conclusion

Adopt ndk-sys and ndk if you are writing Rust that has to call the Android NDK directly, for JNI, for the native windowing and input APIs, or for anything else in the platform's C surface, and you want the FFI split to be done for you. Do not adopt it if you only need to call into your own Java code, since that is JNI from the other direction and there are other bindings for that. Verify first that your minimum supported Rust version satisfies both crates, that the ndk-sys 0.6.0 and ndk 0.9.0 releases are recent enough for the NDK revision you build against, and that the safe ndk crate covers the specific API you need before you commit to it instead of ndk-sys.

## FAQ

### What is the difference between ndk-sys and ndk?

ndk-sys is the raw FFI bindings to the NDK, and ndk is a safe abstraction of those bindings. The README lists their minimum supported Rust versions separately, rustc 1.60.0+ for ndk-sys and rustc 1.64.0+ for ndk.

### Which minimum Rust version do the rust-mobile NDK bindings need?

The MSRV badges in the README show rustc 1.60.0+ for ndk-sys and rustc 1.64.0+ for ndk, so the safe abstraction is the one with the higher floor. The repository is a Cargo workspace with resolver = "2" and the two crates as members.

### Where can I find examples of using these bindings?

The README points to the ndk-examples directory in the rust-mobile/cargo-apk repository and to the separate rust-mobile/rust-android-examples repository. The examples live outside this repository, which contains only the two crates.

### What licence applies to the rust-mobile NDK bindings?

The README carries both MIT and APACHE2 licence badges, and the repository root has LICENSE-APACHE and LICENSE-MIT files, which is the standard Rust dual licence arrangement. The most recent releases listed are ndk-sys 0.6.0 and ndk 0.9.0, both published on 2024-04-26.

## Sources

- [Issues](https://github.com/rust-mobile/ndk/issues)
- [License: Apache-2.0](https://github.com/rust-mobile/ndk/blob/master/LICENSE)
- [README](https://github.com/rust-mobile/ndk/blob/master/README.md)
- [Releases](https://github.com/rust-mobile/ndk/releases)
- [rust-mobile/ndk on GitHub](https://github.com/rust-mobile/ndk)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/rust-mobile-ndk
