Ibiyemi Abiodun

« main page

jsi-rs

Enabling high-performance native code in React Native GitHub repository

October 2022 to August 2023

rustcppreact-native

In winter 2022, I created jsi-rs as part of another project. I was building implement a custom Vulkan renderer for advanced graphics inside of a React Native app, and I wanted to take advantage of wgpu and my existing background in Rust rather than write it in C++.

In React Native, the best way to interoperate between native code and JavaScript code is currently JSI (JavaScript Interfaces). It gives your native code a direct reference to the JavaScript runtime (and vice versa) that eliminates the slow marshalling that took place with older React Native APIs.

jsi-rs is a library that makes it possible to write these JSI modules in Rust and take advantage of all of the benefits that Rust brings, including a simpler type system, a more consistent build system, a robust package manager, and excellent cross-platform portability.

An example

Here are the steps that I took to create a simple JSI module using jsi-rs, referencing the code here:

  1. Create a blank React Native project: npx react-native@latest init example

  2. Create a blank Cargo project inside: cd example && cargo init --lib example-jsi-module

  3. Add jsi, jni, and cxx as dependencies of example-jsi-module

    • jsi will allow us to create a React Native module
    • jni is needed for interop with Java code on Android
    • cxx is needed to initialize jsi because JSI is implemented in C++ with smart pointers
  4. Write the code in example-jsi-module/src

    • android.rs: provides an entrypoint to our Rust library that can be called from Java
    • lib.rs: the general entrypoint of our library that is shared between Android and iOS
  5. Update android/build.gradle to add the following plugin under buildscript.dependencies:

    buildscript {
        repositories {
          // don't remove the existing repositories, just add maven b/c the Rust plugin is hosted there
          maven {
            url "https://plugins.gradle.org/m2/"
          }
        }
        dependencies {
          // don't remove the existing dependencies, just add this one
          classpath("io.github.MatrixDev.android-rust:plugin:0.3.2")
        }
    }
    

    This plugin will compile example-jsi-module using the Android NDK as part of building our application for Android

  6. Make sure the Android NDK is installed. Version 23 should work

  7. Update android/app/build.gradle to add the following lines:

    apply plugin: "io.github.MatrixDev.android-rust"
    androidRust {
        module("example-jsi-module") {
            it.path = file("../example-jsi-module")
    
            // default abi targets are arm and arm64; if you want to run on Android
            // Emulator then you may need to add x86_64
            
            // targets = ["arm", "arm64", "x86", "x86_64"]
        }
    }
    

    Note: when you compile the program, this Gradle plugin will install all of the Rust Android targets if they are not already installed

  8. Write the code in android/app/src/main/java/com/example/ExampleJsiModule.java. This will be called when the application starts, and it gives us a pointer to the React Native runtime so we can initialize our Rust code

  9. Write the code in android/app/src/main/java/com/example/ExampleJsiPackage.java. This lets React Native discover our module

  10. In android/app/src/main/java/com/example/MainApplication.java, add the following line to getPackages():

    packages.add(new ExampleJsiPackage());
    
  11. Add a couple of lines to App.tsx to call our native module (I added them near the top):

    // call our Rust module
    const {ExampleJsiModule} = NativeModules;
    ExampleJsiModule.install();
    
    // call our global object injected by Rust
    console.log('the current time is: ' + ExampleGlobal.time());
    
  12. Connect your Android device or start an emulator

  13. Run npm run start and press A to deploy to Android

  14. You should see hello from Rust in the terminal after the app loads!

The potential

In this example, I added an object to the JavaScript global namespace. This object is a host object, which is special because when you interact with it from JavaScript, you are calling directly into your Rust code! The functions and properties on host objects can return any JS value including other host objects, allowing you to construct any API you can imagine with minimal overhead.

In my other project, I used them to create a custom React Native control that renders a scene with hardware acceleration using wgpu.