When your logic lives on the phone
You spent weeks building a fast parser in Rust. It handles edge cases that made your Python script crash. It runs in milliseconds where the Go service took seconds. Now your team wants that parser in the mobile app. The iOS team writes Swift. The Android team writes Kotlin. You could rewrite the parser twice. You could write a C wrapper and pray the memory management holds up. Or you can use neon to compile your Rust crate into a binary that both platforms load directly.
neon bridges the gap between Rust and the host runtime. It compiles your code and generates the bindings so the app can call your functions without you writing JNI boilerplate or Objective-C glue. You write Rust. neon handles the messy interop.
The bridge between Rust and the host
Rust doesn't speak Swift or Kotlin by default. It speaks C ABI. To call Rust from a mobile app, you need a Foreign Function Interface. FFI is the handshake between languages. It defines how arguments pass across the boundary and how memory gets managed.
neon automates this handshake. It wraps your Rust functions in a layer that the host runtime understands. For Android, that means generating the JNI structures. For iOS, it produces the headers and binaries that link into the Xcode project. neon also manages the garbage collection hooks. When the host language creates a value that Rust holds, neon ensures the value stays alive as long as Rust needs it.
Think of neon like a universal adapter for power plugs. Your Rust code is the appliance. The phone OS is the wall socket. Android and iOS have different sockets. neon gives you an adapter that fits both, so you don't have to redesign the appliance for each country.
Your first neon library
Start by installing the CLI tool. This tool manages the project structure and build commands.
cargo install neon-cli
Create a new library project. The CLI scaffolds the directory, sets up Cargo.toml, and adds the neon dependency.
neon create mylib
cd mylib
Open src/lib.rs. This file defines the functions the host can call. Every exported function takes a context object and returns a JsResult. The context gives you access to arguments and lets you construct return values.
use neon::prelude::*;
/// Adds two numbers and returns the sum.
fn add(mut cx: FunctionContext) -> JsResult<JsNumber> {
// Extract arguments from the host context.
// neon handles the type checking and conversion.
let a = cx.argument::<JsNumber>(0)?.value(&cx);
let b = cx.argument::<JsNumber>(1)?.value(&cx);
// Perform the calculation in Rust.
let sum = a + b;
// Return the result back to the host.
Ok(cx.number(sum))
}
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
// Export the function so the host can find it.
cx.export_function("add", add)?;
Ok(())
}
The #[neon::main] function registers your exports. The host looks for this entry point when loading the library. The export_function call maps the Rust function to a name the host can use.
Build the library for your current platform.
neon build --release
This command invokes cargo under the hood. It compiles your Rust code, links it with the neon runtime, and produces a shared library in target/release. The exact filename depends on the platform. On Linux or Android, you get a .so file. On macOS or iOS, you get a .dylib or .a file.
What the build actually does
neon build does more than compile Rust. It detects the target architecture and configures the linker. For mobile development, you need to add the cross-compilation targets to your Rust toolchain. neon can trigger these installs, or you can run them manually.
Add the Android target for 64-bit ARM devices.
rustup target add aarch64-linux-android
Add the iOS target for 64-bit ARM devices.
rustup target add aarch64-apple-ios
Once the targets are installed, neon build can compile for those platforms. You still need the SDKs. Android requires the Android SDK and NDK. iOS requires Xcode. neon looks for these in standard locations. If the paths are non-standard, you can set environment variables like ANDROID_NDK_HOME or IOS_SDK_PATH.
The build produces artifacts in target/release. The structure mirrors the Rust target directories. You'll find the compiled library alongside debug symbols and metadata. Copy the library into your mobile project. On Android, place it in src/main/jniLibs/. On iOS, add it to the Xcode build phases.
Convention aside: neon projects often keep a package.json file even for mobile libraries. This file stores configuration for the CLI, like the library name and version. It's a holdover from neon's Node.js roots. The community uses it because neon-cli reads it for build settings. You can ignore it if you prefer Cargo.toml only, but the CLI expects it for some commands.
A realistic data processor
Real libraries do more than add numbers. They process data and return results. Here's a function that validates an email address and returns a boolean. It shows how to handle strings and errors.
use neon::prelude::*;
/// Validates an email address format.
/// Returns true if the email contains '@' and a dot after the '@'.
fn validate_email(mut cx: FunctionContext) -> JsResult<JsBoolean> {
// Get the email string from the host.
let email = cx.argument::<JsString>(0)?;
let email_str = email.value(&cx);
// Perform validation in Rust.
let is_valid = email_str.contains('@') && {
let parts: Vec<&str> = email_str.split('@').collect();
parts.len() == 2 && parts[1].contains('.')
};
// Return the boolean result.
Ok(cx.boolean(is_valid))
}
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
cx.export_function("validateEmail", validate_email)?;
Ok(())
}
The value method on JsString borrows the string data. You can use it while the function runs. You cannot return a reference to this data. The host might drop the string after the function returns. If you try to return a &str, the compiler rejects you with E0515 (cannot return value referencing local variable). Always return owned types or use neon's garbage collection helpers to keep data alive.
Pitfalls and compiler traps
Building shared libraries introduces specific failure modes. Watch for these.
ABI stability. Rust does not guarantee ABI stability by default. If you change the Rust code, the binary layout might change. The host app must link against the new binary. You cannot update the Rust code and expect the old binary to work. Rebuild the library every time you change the Rust code. The binary is not magic.
Memory management. neon manages the boundary, but you can still leak memory. If you allocate data in Rust and pass a pointer to the host, you must tell neon how to free it. Use Gc or Root to manage lifetimes. If you forget, the host might free memory while Rust still holds a reference. The result is a crash or corruption. Trust the GC hooks. They keep your pointers valid.
Cross-compilation errors. If you miss a target, the build fails. You'll see an error like "failed to run custom build command for neon-build". Check that you added the target with rustup target add. Check that the SDK paths are correct. The error message usually points to the missing toolchain component.
Type mismatches. neon enforces types at the boundary. If the host passes a string where you expect a number, the function returns an error. The host receives a runtime error. You can catch this in the host code. If you mix up the argument index, you get a panic. The compiler won't catch index errors. Test the bindings thoroughly.
Unsafe blocks. neon uses unsafe internally to manage the FFI. You don't need unsafe in your business logic. If you find yourself writing unsafe, you're likely doing something wrong. Keep unsafe out of your business logic. neon handles the unsafe boundary.
Choosing your tool
neon is not the only way to share Rust code with mobile apps. The ecosystem offers alternatives. Pick the tool that matches your needs.
Use neon when you want a unified build command and automatic binding generation for runtimes that support neon. Use neon when you prefer writing Rust and letting the tool handle the host-side glue. Use neon when you need garbage collection integration with the host runtime.
Use cargo-ndk when you need a raw static library for Android and want full control over the linking step. Use cargo-ndk when you are building a library that links into a C++ Android project. Use cargo-ndk when you want to avoid the neon runtime overhead.
Use bindgen when you are wrapping an existing C library in Rust. Use bindgen when you have a C header file and want to generate Rust bindings automatically. Use bindgen when you need to call C functions from Rust without writing manual FFI code.
Use plain cargo build --target when you want maximum flexibility and don't mind managing the build scripts yourself. Use this when you are integrating Rust into a complex build system that doesn't fit neon's model. Use this when you need to produce multiple artifacts with different flags.