Building a Rust Native Module for React Native on iOS and Android
by Daniel Boros
Sep 8
7 min read
142 views
React Native is fantastic for building mobile apps using JavaScript, but sometimes you need extra performance, or maybe you want to leverage existing native code written in languages like Rust. Rust, known for its performance and memory safety, can be a great fit for critical mobile tasks. In this tutorial, we’ll walk through creating a native module using Rust for both iOS and Android. By the end, you’ll have a working Rust module that integrates seamlessly with your React Native project, offering all the performance benefits of Rust while still being accessible from JavaScript.
Why Rust?
Rust is becoming the go-to language for performance-critical applications because it provides memory safety without a garbage collector, making it a great choice for mobile apps. By using Rust, we can write a function once and reuse it across both iOS and Android, saving time and effort. If you’re new to this, here's a helpful video tutorial that covers some of the core concepts of working with native code in React Native. It goes into detail on some of the things we’ll cover here, so feel free to give it a watch.
Prerequisites
Before diving in, you should have the following: Familiarity with React Native A working development environment for iOS and Android Basic understanding of Rust Node.js and npm installed Cargo (Rust’s build tool and package manager) Xcode for iOS development Android Studio for Android development Homebrew (for macOS users to install certain dependencies) Now that you're set up, let’s dive into creating a native module!
Step 1: Create a Native Module
Let’s start by setting up the project. We’ll use Expo’s create-expo-module
command to scaffold a basic native module.
Run the following in your terminal:
npx create-expo-module rust-module --local
This command initializes a new Expo native module that we’ll integrate with Rust. Once it’s created, be sure to run pod install
in the iOS directory to install necessary dependencies:
cd ios
pod install
Why this step is crucial: iOS uses CocoaPods to manage native dependencies. Without running pod install
, you’ll encounter a native module not found error when running the app.
Step 2: Initialize a Rust Library
Now, we need to create a Rust library that will contain the native code we’ll call from JavaScript. Start by navigating to your backend directory (or create one) and initialize a new Rust library:
cargo new rust_backend --lib
Once the library is initialized, open your Cargo.toml
file and configure it to generate static libraries for iOS and dynamic libraries for Android:
[lib]
crate-type = [
"staticlib", # for iOS
"cdylib" # for Android
]
Why this step is important: We need to tell Cargo (Rust’s package manager) to compile the library into different formats depending on the target platform. iOS uses static libraries, while Android uses dynamic ones.
Step 3: Set Up the Rust Toolchain for iOS
To build Rust code for iOS, we need to install the appropriate toolchains for both the device and the simulator:
rustup target add aarch64-apple-ios aarch64-apple-ios-sim
What does this do? This command adds the targets required to build Rust code that runs on both real iOS devices (aarch64-apple-ios
) and iOS simulators (aarch64-apple-ios-sim
).
Step 4: Build the Rust Module for iOS
Now, let’s build our Rust module for iOS devices and simulators. Run the following commands:
cargo build --release --target aarch64-apple-ios
cargo build --release --target aarch64-apple-ios-sim
This will create the necessary .a
files that can be linked into our iOS app.
Why this is critical: We need to build the Rust code in a format that iOS can use. The .a
files are static libraries that will be included in the iOS app.
Step 5: Building for Android
On Android, we need to build for multiple architectures (e.g., ARM and x86) to ensure compatibility with a wide range of devices. First, install the necessary Rust toolchains:
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android
Then, build the Rust module for each target:
cargo build --release --target aarch64-linux-android
cargo build --release --target armv7-linux-androideabi
cargo build --release --target x86_64-linux-android
cargo build --release --target i686-linux-android
Why multiple builds? Android devices come in different CPU architectures, so we need to provide native libraries for all major platforms to ensure compatibility.
Step 6: Integrating with iOS
Once you’ve built the Rust module, you need to add the .a
binary to your iOS project. Copy the generated .a
file and header file into the ios/rust
directory.
Your folder structure should look like this:
ios
└── rust
├── librust_connect_backend.a
└── rust_connect_backend.h
Next, modify your Podspec file to tell CocoaPods about the Rust binary:
s.vendored_libraries = 'rust/rust_connect_backend.a'
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
Step 7: Integrating with Android
For Android, we need to add the .so
binaries to the project. Copy them into the jniLibs
directory:
android
└── app
└── src
└── main
└── jniLibs
├── arm64-v8a
│ └── librust_connect_backend.so
├── armeabi-v7a
│ └── librust_connect_backend.so
├── x86
│ └── librust_connect_backend.so
└── x86_64
└── librust_connect_backend.so
Step 8: Adding JNI Bindings for Android
For Android, we need to expose our Rust functions using Java Native Interface (JNI). Here’s how you can create JNI bindings in your Rust code:
#[cfg(target_os = "android")]
pub mod android {
use crate::rust_add;
use jni::objects::JClass;
use jni::sys::jint;
use jni::JNIEnv;
#[no_mangle]
pub unsafe extern "C" fn Java_expo_modules_rustbackend_RustBackendModule_rustAdd(
_env: JNIEnv,
_class: JClass,
a: jint,
b: jint,
) -> jint {
rust_add(a, b)
}
}
Why JNI? JNI allows Java and Kotlin code to call native code written in languages like Rust. This step is essential for Android integration.
Step 9: Defining the Function in Kotlin
In your Kotlin code, define the Rust function:
class RustBackendModule : Module() {
companion object {
init {
// Load the Rust library
System.loadLibrary("rustbackend")
}
}
external fun rustAdd(a: Int, b: Int): Int
}
Step 10: Adding the Function in Swift and JavaScript
For iOS, define the function in Swift:
@objc(MyModule)
class MyModule: NSObject {
@objc
func rustAdd(_ a: Int32, b: Int32, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) -> Void {
resolver(rust_add(a, b))
}
}
In index.ts
, define the JavaScript function that calls the native code:
export async function rustAdd(a: number, b: number): Promise<number> {
return RustBackendModule.rustAdd(a, b);
}
Step 11: Importing and Using the Rust Function in Your App
Finally, import the Rust function into your React Native component:
import { rustAdd } from "../../../modules/rust-module/index";
rustAdd(5, 10).then(result => {
console.log(result); // Output: 15
});
Conclusion
As we conclude this exploration of integrating Rust into the React Native ecosystem, it's important to reflect on the ongoing evolution in mobile development frameworks. Popular libraries like react-reanimated, react-native-gesture-handler, skia, and wgpu are predominantly written in C++ to leverage its performance optimizations and extensive ecosystem. However, despite C++'s dominance in these areas, there's a compelling case to be made for Rust and its growing influence in systems programming.
Rust offers distinct advantages over C++, particularly in terms of memory safety, concurrency, and developer ergonomics. These benefits make Rust an attractive alternative for developing native modules and even entire libraries that could be used with React Native. The type safety and memory management features of Rust not only reduce the risk of common bugs found in C++ development but also enhance maintainability and scalability of the code.
Given the rapid development and adoption of Rust in various domains, it's conceivable that libraries akin to those mentioned could be rewritten or newly created in Rust with potentially greater efficiency and reliability. For developers who are adept in Rust, this represents a unique opportunity to pioneer the use of Rust in areas traditionally dominated by C++. By integrating Rust-based native modules in React Native, developers can push the boundaries of what's possible in mobile app development, bringing high performance, safety, and a modern programming environment to their applications.
Moreover, as the React Native architecture evolves to potentially incorporate more features like TurboModules and Fabric, the integration of Rust could further optimize these new capabilities, aligning well with the goals of improving performance and smooth interoperability between JavaScript and native code.
Despite these possibilities, it is crucial to acknowledge that React Native itself remains an extremely robust and performant framework, especially notable for its ability to deliver native UI/UX across platforms, a feat not matched by frameworks like Flutter and current Rust-based alternatives. With the recent advancements in the JavaScript Interface (JSI), React Native has almost eliminated the overhead caused by the old asynchronous bridge, enhancing its performance closer to true native execution.
This tutorial not only guides you through setting up Rust with React Native but also positions you at the cutting edge of a shift towards more robust and safe mobile applications. As the community and technology continue to evolve, Rust may well become a key player in the future of mobile app development, offering a sustainable and powerful alternative to traditional C++ approaches, while React Native continues to excel as a leading choice for cross-platform mobile development.