You would want to use Rust with React Native when you need high performance and safety in your mobile app. Rust is a fast and memory-efficient language that can handle complex computations and tasks. By integrating Rust with React Native, you can write performance-critical parts of your app in Rust, while using React Native for the user interface. This combination allows you to create apps that are both responsive and reliable. Rust’s safety features also help prevent bugs and crashes, making your app more stable and secure. For example with rust and react native you can develop high performance mobile apps for the use cases mentioned below:
-
High-Performance Games: Develop mobile games where the game engine and performance-critical logic are implemented in Rust for optimal speed and efficiency, while React Native is used for the user interface and touch interactions.
-
Real-Time Messaging Apps: Create real-time messaging applications that utilize Rust for handling concurrent network connections and message processing, ensuring low latency and high performance, while React Native manages the chat UI and user experience.
-
Augmented Reality (AR) Apps: Build AR applications where Rust handles the complex calculations and data processing for rendering AR objects and scenes, while React Native is used to develop the user interface and control elements.
-
Cryptographic Wallets: Develop secure cryptocurrency wallets where Rust is used for cryptographic operations and secure key management, providing enhanced security and performance, while React Native manages the user interface and user interactions.
-
Health and Fitness Trackers: Create health and fitness tracking apps that use Rust for processing and analyzing sensor data from devices such as heart rate monitors and accelerometers, while React Native provides a responsive and engaging user interface.
Setup Toolchain & Environment
Developing a React Native Android app that utilizes Rust code involves several moving parts. Below, we will cover all these components in detail. This guide will provide all the necessary plumbing required for calling Rust code from React Native, ensuring you have a smooth and efficient development experience.
First, I will cover how to set up the prerequisite Android tools, including the SDK, NDK, and Java, which are essential for developing Android applications. Next, I will guide you through configuring Gradle for React Native Android to use Kotlin, and also ensuring that the NDK is properly set up to handle the compiled Rust library.
After that, I will walk you through setting up the Rust toolchain, which includes installing Rust and configuring the necessary build tools. Following the Rust setup, I will explain how to register custom React Native modules in Kotlin, enabling seamless integration between Rust and React Native.
Finally, I will show you how to call the Rust functions from React Native TypeScript code. By following this comprehensive guide, you will be able to harness the power and performance of Rust in your React Native Android applications, combining the best of both worlds to create high-performance, secure, and user-friendly mobile apps.
Assumptions
Let’s move ahead in this comprehensive guide with the following assumptions:
- We are going to call our app as
sample-app
- We are going to use:
- Open JDK
17.0.10
- NDK
25.2.9519653
- Gradle
8.6-all
- Rust
rustc 1.77.2
- Uniffi
v0.28.0
- Node
v21.1.7
- npm
v10.5.0
- Open JDK
Since, the technology for this setup is ever evolving, these assumptions are required for a successful build.
Android sdk
What is Android SDK and Its Significance for Developing React Native Apps
The Android Software Development Kit
(SDK) is a set of tools and libraries provided by Google for developers to create applications for the Android platform. It includes a comprehensive set of development tools, such as libraries, debugger, emulator, and documentation, which are essential for building, testing, and debugging Android applications.
For developing React Native apps, the Android SDK is crucial because React Native leverages native components and APIs of the Android platform to build applications. React Native uses the Android SDK to compile JavaScript code into native code, interact with device hardware and sensors, handle user interface elements, and access various platform-specific features. In essence, the Android SDK acts as a bridge between the JavaScript code of a React Native app and the underlying Android platform, enabling developers to create high-quality, performant mobile applications.
Common Ways to Setup Android SDK
1. Using Android Studio:
Android Studio
is the official Integrated Development Environment (IDE) for Android development, and it includes built-in support for installing and managing the Android SDK. To set up the Android SDK using Android Studio:- Download and install Android Studio from the official website.
- Launch Android Studio and follow the setup wizard to install necessary components, including the
Android SDK
. - Android Studio will automatically download and install the required SDK components based on your selected configuration.
2. Using Command Line Interface (CLI) in Linux:
- Alternatively, you can set up the Android SDK on Linux using the command line. Here’s a basic outline of the steps:
- Download the Android SDK Command Line Tools from the official Android developer website.
- Extract the downloaded archive to a suitable location on your system.
- Set the
ANDROID_HOME
environment variable to the path where you extracted the SDK. - Add the
SDK
’s tools andplatform-tools
directories to your system’s PATH variable. - Optionally, you can use the sdkmanager command-line tool to install additional SDK components as needed.
Common Environment Variables for Android SDK
When setting up the Android SDK, several environment variables play a crucial role in configuring the development environment. Some of the common environment variables include:
ANDROID_HOME
: This variable points to the root directory of the Android SDK installation. It is used by various tools and scripts to locate the SDK components.PATH
: It should include the tools and platform-tools directories within the Android SDK, allowing you to run Android development commands from the terminal.JAVA_HOME
: This variable points to the root directory of the Java Development Kit (JDK) installation. Android development requires Java, so setting this variable ensures that the Android SDK can find the necessary Java tools.ANDROID_SDK_ROOT
: Similar toANDROID_HOME
, this variable also points to the root directory of the Android SDK installation. It’s used by some tools as an alternative toANDROID_HOME
.
By correctly configuring these environment variables, developers can ensure that the Android SDK is properly integrated into their development environment, allowing for smooth and efficient development of React Native applications targeting the Android platform.
Explanation for how to setup these variables is beyond the scope of this post.
JDK
The specific JDK version used for this sample-app development is jdk-17.0.10.
Explanation for setting up JDK is beyond the scope of this post.
NDK - Native Development Kit
In order to successfully call rust code in our react native android app, we would need NDK to allow calling rust methods in our Java/kotlin code. NDK can be installed either manually using CLI or more conveniently through Android Studio. The specific version of NDK used is 25.2.9519653
.
Explanation for how to setup NDK is beyond the scope of this post.
rustup
To install rust on the system, rustup has been used. The version used is rustc 1.77.2
.
Explanation for how to setup rust using rustup is beyond the scope of this post. Please, refer https://rustup.rs/ for guidance.
cross
- Cross is a utility used for “Zero setup” cross compilation and “cross testing” of Rust crates
- Cross can be installed using this command:
cargo install cross --git https://github.com/cross-rs/cross
Later in this post, we would be using cross
to cross compile our rust code for android specific architectures. Cross utilizes docker containers & emphasizes on isolation to cross compile rust code for different target platforms.
I say this with full responsibility, Cross
has been a super time saver for me in different situations.
Toolchain & environment checklist
Essentially, the toolchain & environment necessary for calling Rust code using react native consist of:
- Cross
ANDROID_SDK_ROOT
environment variableANDROID_HOME
environment variableANDROID_NDK_HOME
environment variableJAVA_HOME
environment variableadb
utilityrustup
with rust installedNDK
bundleJDK
Node
(v21.1.7
)npm
(v10.5.0
)yarn
Setup Project — Frontend
React Native
- Scaffold a new
react-native
app using:
npx react-native@latest init SampleApp --directory sample-app --title HelloWorld
For our android layer, we are going to use kotlin instead of java.
- Configure
android/gradle/wrapper/gradle-wrapper.properties
if you want to customize gradle for successful kotlin setup for our android app.
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
- Update
android/build.gradle
. We are using NDK version25.2.9519653
for our app.
buildscript {
ext {
buildToolsVersion = "34.0.0"
minSdkVersion = 23
compileSdkVersion = 34
targetSdkVersion = 34
ndkVersion = "25.2.9519653"
kotlinVersion = "1.9.22"
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle")
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
}
}
apply plugin: "com.facebook.react.rootproject"
- Update
android/app/build.gradle
to correctly setdefaultConfig.ndk.abiFilters
for the target devices’ architectures. We are also creating a task namedbuild_backend_for_android
at the bottom of this file for compiling our rust code usingcross
we have setup above. This task sets the working directory and executes thebuild_backend_for_android.sh
script.
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'idea'
}
apply plugin: "com.android.application"
apply plugin: "com.facebook.react"
apply plugin: "kotlin-android"
react {
}
def enableProguardInReleaseBuilds = false
def jscFlavor = 'org.webkit:android-jsc:+'
android {
ndkVersion rootProject.ext.ndkVersion
compileSdkVersion rootProject.ext.compileSdkVersion
namespace "com.sampleapp"
defaultConfig {
applicationId "com.sampleapp"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64', 'aarch'
}
}
signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
signingConfig signingConfigs.debug
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
buildFeatures {
buildConfig = true
}
ndkVersion '25.2.9519653'
}
dependencies {
implementation("com.facebook.react:react-android")
implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")
implementation "net.java.dev.jna:jna:5.13.0@aar"
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
implementation jscFlavor
}
}
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
task build_backend_for_android(type: Exec) {
def uniffiPath = "${buildDir}/generated/source/uniffi/java"
def rustBackendDir = "${project.projectDir}/../../backend"
workingDir(rustBackendDir)
commandLine './build_backend_for_android.sh'
}
preBuild.dependsOn 'build_backend_for_android'
[bindings.kotlin] package_name = “com.sampleapp.uniffi” cdylib_name = “backend”
Setup Project — Backend
create new rust lib
Now let’s finally write the rust code which will be called in our react native android sample-app
. We are going to store our rust code in the project root under the backend
directory.
# Run this in the root of the project
cargo new --lib backend
Build script
Update backend/build_backend_for_android.sh
. In this script we are targeting android aarch64
, if the device/emulator you have setup on your system is different, change them accordingly.
#!/usr/bin/bash
######################################
###### Compile & setup for ARM 64 android
###### Generate kotlin bindings from code
######################################
cross run --target=aarch64-linux-android --features=uniffi/cli --bin=uniffi-bindgen generate src/backend.udl --language kotlin --out-dir=.
cross build --target=aarch64-linux-android --release --lib
rm -rf ../android/app/src/main/jniLibs
mkdir -p ../android/app/src/main/jniLibs/arm64-v8a
cp target/aarch64-linux-android/release/libbackend.so ../android/app/src/main/jniLibs/arm64-v8a/libbackend.so
######################################
######################################
###### Generate Kotlin bindings from library
######################################
# cross run --target=aarch64-linux-android --bin uniffi-bindgen generate --library target/aarch64-linux-android/release/libbackend.so --language kotlin --out-dir .
######################################
# useful when using udl definitions
# cross run --target=aarch64-linux-android --bin=uniffi-bindgen generate src/backend.udl --language kotlin --out-dir=.
######################################
###### Cleanup
######################################
rm -rf ../android/app/src/main/java/com/sampleapp/uniffi
cp -r com/sampleapp/uniffi ../android/app/src/main/java/com/sampleapp/uniffi
rm -rf com
######################################
uniffi setup
We are going to use Mozilla’s UniFFI. UniFFI is a tool that automatically generates foreign-language bindings targeting Rust libraries. We would be using it to generate Kotlin bindings for our Rust code. Using these kotlin bindings we would be able to call the methods exposed through the rust library, in our react native android kotlin
layer.
[package]
name = "backend"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
name = "backend"
[dependencies]
uniffi = { version = "0.28.0", features = ["cli"] }
[build-dependencies]
uniffi = { version = "0.28.0", features = ["build"]}
uniffi_build = { version = "0.28.0", features = ["builtin-bindgen"] }
[[bin]]
name = "uniffi-bindgen"
path = "uniffi-bindgen.rs"
UniFFI relies on Interface Definition Language UDL
file which describes the methods we want to expose in our rust code. uniffi
build dependencies read this file and links our library methods in the final rust library that is created. Additionally, using this UDL kotlin bindings are generated.
Learn more about the schema UDL supports on the official docs for it.
namespace backend {
string rusty_hello(string text);
};
We then need to tell rust in our build.rs
to use the uniffi_build
crate to generate our kotlin bindings.
fn main() {
uniffi_build::generate_scaffolding("src/backend.udl").unwrap();
}
If you take a look at our cargo.toml
file line 17-20, for generating the kotlin bindings, we have instructed cargo to register a binary for path uniffi-bindgen.rs
. Let’s create this file for triggering the FFI bindings generation.
fn main() {
uniffi::uniffi_bindgen_main()
}
We can configure UniFFI to mark the generated kotlin bindings with specific package we want to use, and also the name of the library that will be available to the Android app using jniLibs. For that we need to create this toml file:
[bindings.kotlin]
package_name = "com.sampleapp.uniffi"
cdylib_name = "backend"
Let’s finally include the scaffolding generated using uniffi in our library, and write a hello_world
rust function.
#![allow(unused)]
uniffi::include_scaffolding!("backend");
pub fn rusty_hello(text: String) -> String {
format!("Hello {text}")
}
The name of the functions in backend/src/lib.rs
that we want to call from react native should match with their UDL namespace definitions.
kotlin setup
Finally, we need some kotlin glue code to be able to call our rust code from react native. React native provides native modules, we can register our own custom react native module for our react native app.
Learn more about React Native Modules using the official docs.
The build_backend_for_android.sh
script would move the UniFFI Kotlin FFI bindings to android/app/src/main/java/com/sampleapp/uniffi/backend.kt
. We can then create a custom module which calls these rust library methods exposed via the kotlin FFI. For reference, here’s the backend/build_backend_for_android.sh
file below:
#!/usr/bin/bash
######################################
###### Compile & setup for ARM 64 android
###### Generate kotlin bindings from code
######################################
cross run --target=aarch64-linux-android --features=uniffi/cli --bin=uniffi-bindgen generate src/backend.udl --language kotlin --out-dir=.
cross build --target=aarch64-linux-android --release --lib
rm -rf ../android/app/src/main/jniLibs
mkdir -p ../android/app/src/main/jniLibs/arm64-v8a
cp target/aarch64-linux-android/release/libbackend.so ../android/app/src/main/jniLibs/arm64-v8a/libbackend.so
######################################
######################################
###### Generate Kotlin bindings from library
######################################
# cross run --target=aarch64-linux-android --bin uniffi-bindgen generate --library target/aarch64-linux-android/release/libbackend.so --language kotlin --out-dir .
######################################
# useful when using udl definitions
# cross run --target=aarch64-linux-android --bin=uniffi-bindgen generate src/backend.udl --language kotlin --out-dir=.
######################################
###### Cleanup
######################################
rm -rf ../android/app/src/main/java/com/sampleapp/uniffi
cp -r com/sampleapp/uniffi ../android/app/src/main/java/com/sampleapp/uniffi
rm -rf com
######################################
Observe, using cross we are running the uniffi-bindgen utility for generating the Kotlin FFIs. Also, using cross we are generating libbackend.so
which is being copied to android/app/src/main/jniLibs/arm64-v8a/
. The way this works is, depending on the platform architecture, we need to copy the library .so
files to their respective jniLibs
directory. So, pay attention to the platform architecture you are targeting and accordingly modify the script as necessary.
There are several ways through which JVM can call external libraries: JNI, JNA & JNR. The terms JNA, JNI, and JNR refer to different ways of interfacing Java with native code (c++, rust compiled code).
In layman terms, JNA is really easy to use since it doesn’t require the developers to write extra JNI code. But, generally speaking, it’s slower than JNI. Then, we have JNR which sits in between JNA and JNI in terms of performance and ease of use. To go in depth for these is out of scope for this post. You can read more about these here 1 2.
Coming back to the glue code we talked about above, we need to do the following:
- Create a new React Native Module, let’s call it
BackendModule
. - Call the automatically generated FFI method
helloWorld
(this is generated automatically using our UDL definition). - Create a new kotlin package.
- Register our
BackendModule
in our customMyAppPackage
. - Register
MyAppPackage
with React Native Modules in kotlin.
package com.sampleapp
import com.facebook.react.bridge.Promise
import android.util.Log
import com.sampleapp.uniffi.rustyHello
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
class BackendModule internal constructor(context: ReactApplicationContext?) : ReactContextBaseJavaModule(context) {
override fun getName(): String {
return "BackendModule"
}
@ReactMethod
fun hello(text: String, promise: Promise) {
var response = rustyHello(text)
Log.d("Kotlin BackendModule says:", "${response}")
promise.resolve(response)
}
}
In the file mentioned above android/app/src/main/java/com/sampleapp/BackendModule.kt
, we are exposing the hello
kotlin function. Part of this function signature, we are calling the helloWorld
function which is being automatically mapped to our rust library hello_world
method by UniFFI
. If you think about it, it’s awesome how UniFFI
is doing all this FFI heavy-lifting for us in a clean abstracted away! To understand how it does it, just take a look at the android/app/src/main/java/com/sampleapp/uniffi/backend.kt
file, it’s using JNA!
We now need to create a new package. Using this package we will be able to register our newly created custom BackendModule
with react native modules, and then it will be available in our react native app to consume.
Notice, we are using Promise
as part of fn hello
signature. There are several ways for interfacing react native java/kotlin layer and JS layer. You can read about other possibilities here.
package com.sampleapp
import android.view.View
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ReactShadowNode
import com.facebook.react.uimanager.ViewManager
class MyAppPackage : ReactPackage {
override fun createViewManagers(
reactContext: ReactApplicationContext
): MutableList<ViewManager<View, ReactShadowNode<*>>> = mutableListOf()
override fun createNativeModules(
reactContext: ReactApplicationContext
): MutableList<NativeModule> = listOf(BackendModule(reactContext)).toMutableList()
}
Now, we need to register the package MyAppPackage
we created above.
package com.sampleapp
import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.soloader.SoLoader
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
add(MyAppPackage())
}
override fun getJSMainModuleName(): String = "index"
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}
override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
SoLoader.init(this, false)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
load()
}
}
}
Frontend - Final pieces coming together
Finally, all that’s left is to consume our very own BackendModule
React Native Module in our typescript react native layer.
package json
This is how the package.json file looks for our project sample-app
.
{
"name": "sampleapp",
"version": "0.0.1",
"private": true,
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
"test": "jest"
},
"dependencies": {
"react": "18.2.0",
"react-native": "0.74.1"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0",
"@react-native/babel-preset": "0.74.83",
"@react-native/eslint-config": "0.74.83",
"@react-native/metro-config": "0.74.83",
"@react-native/typescript-config": "0.74.83",
"@types/react": "^18.2.6",
"@types/react-test-renderer": "^18.0.0",
"babel-jest": "^29.6.3",
"eslint": "^8.19.0",
"jest": "^29.6.3",
"prettier": "2.8.8",
"react-test-renderer": "18.2.0",
"typescript": "5.0.4"
},
"engines": {
"node": ">=18"
},
"packageManager": "yarn@3.6.4"
}
React native Frontend Typescript code
Now, in our frontend code we can call these custom react native modules methods (added by our package MyAppPackage
). To do so we need to import NativeModules
from react-native
package. And, then we can obtain our custom module by using TS destructuring for convenience const {BackendModule} = NativeModules;
. Finally, we can now call our Kotlin hello
method to return response from Rust’s hello_world
function.
import React, {useState} from 'react';
import {Button, NativeModules, SafeAreaView, Text} from 'react-native';
const {BackendModule} = NativeModules;
const HelloFromRust = () => {
const [value, setValue] = useState('');
const onPress = async () => {
let helloFromRust = await BackendModule.hello('from rust!');
setValue(helloFromRust);
};
return (
<>
<Button
title="Click To invoke Kotlin Native Module Code"
color="#241584"
onPress={onPress}
/>
<Text style={{fontSize: 40}}>{value}</Text>
</>
);
};
function App(): JSX.Element {
return (
<SafeAreaView>
<HelloFromRust />
</SafeAreaView>
);
}
export default App;
We can use adb logcat
to view logs printed in our kotlin layer.
adb logcat | grep -e BackendModule
Additionally, we can also directly print logs in our rust layer. For that we need to import certain crates in our library, which is beyond the scope of this post.
Conclusion
Without a doubt, this has been a really long comprehensive guide. Thank you for your patience for following through all along. I hope with this guide, you can successfully and swiftly create the skeleton for a react native + rust app.
As a todo, whenever I get the time for it, I will create a mason bricks scaffold for this entire guide. Or, if you can do it, please let me know about it. I would be more than happy to consume it! 😉