Use Rust in React Native through WebAssembly

This post shows how to use Rust functions in a React Native project through WebAssembly. I struggled quite a bit to find an easy way to make it work, so I thought I’d share my findings. You can find the code for this post here.

Here’s the TLDR:

  1. Create a wasm-pack project exposing the Rust functions you want to export.
  2. Serve a web page exposing these functions through message events.
  3. Use a React Native WebView of this web page.
  4. Call Rust functions by sending messages to the WebView.

Pros and Cons

Pros

  • One alternative is to use native modules but it is a pain to setup with Rust and React Native, and requires lots of different configurations for Android and iOS.
  • Another alternative is to serve wasm files locally on the app and use them in a WebView. This is also a pain to setup since local files need to be put in different places in Android and iOS, and you will get a bunch of permission errors along the way.
  • The solution shown here is very easy to setup, works out of the box, and allows for a clear separation between the Rust and React Native development.

Cons

  • The device needs an Internet connection to download the web page.
  • Mild privacy issue since one can track requests to the web server. But all computations using WebAssembly are done locally on the device. In particular, the parameters to the Rust functions will not be sent to the server.
  • Probably quite slower than using the native modules.

Prototype

This gives an example of this structure with a simple Rust function that adds two numbers.

Rust

Create a wasm-pack project following the instructions given here. The src/lib.rs exposes the functions you want to export. Here’s ours:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn add(a: u32, b: u32) -> u32 {
	a + b
}

Then, build the package with wasm-pack build and create a web directory with package.json like:

/* -------- SNIP -------- */
"devDependencies": {
    "rust": "file:../pkg",
    "webpack": "^4.29.3",
    "webpack-cli": "^3.1.0",
    "webpack-dev-server": "^3.1.5",
    "copy-webpack-plugin": "^5.0.0"
  }
/* -------- SNIP -------- */

The WebAssembly functions can be called from Javascript using message events. Here’s our index.js:

import * as wasm from "rust"; // Import the wasm package

let sum;
document.addEventListener("message", function(event) { // Receive parameters in a message
    let {a: s1, b: s2} = JSON.parse(event.data); // Parse the parameters
    sum = wasm.add(s1, s2); // Call the wasm function
    window.ReactNativeWebView.postMessage(JSON.stringify({sum})); // Send a message to React Native with the result of the wasm function.
}, false);

Now, serve this web directory on a web server. For this prototype, we serve it on localhost:8080.

React Native

Create a React Native project with react-native-webview linked. Here is our App.js that shows the basic way to call the WebAssembly functions served by our web server:

import React, { Component } from 'react';
import { View, Button, TextInput, Text } from 'react-native';
import { WebView } from 'react-native-webview';



export default class App extends Component {

    webView = null; // Holds the reference to the WebView
    state = { s1: null, s2: null, sum: null };
	
	// Sends a message to the WebView with the parameters of the Rust function `add`.
    sendMessage = () => {
        this.webView.postMessage(JSON.stringify({ a: parseInt(this.state.s1), b: parseInt(this.state.s2) }));
    }
	
    // Listen for messages from the WebView containing the result of the wasm function.
    onMessage = (event) => {
        this.setState({sum: JSON.parse(event.nativeEvent.data).sum}, () => console.log(this.state));
    }
    
    render() {
        return (
            <View style={{ flex: 1 }}>
                <Button
                    title="Press me"
                    onPress={this.sendMessage} // Sends a message on button press.
                />
                <TextInput
                    placeholder="First summand"
                    onChangeText={(text) => this.setState({ s1: text })}
                    value={this.state.s1}
                />
                <TextInput
                    placeholder="Second summand"
                    onChangeText={(text) => this.setState({ s2: text })}
                    value={this.state.s2}
                />
                <WebView
                    style={{ height: 0 }}
                    useWebkit={true}
                    originWhitelist={['*']}
                    javaScriptEnabled={true}
                    source={{ uri: 'http://10.0.2.2:8080/' }} // Change to your webserver.
                    allowFileAccess={true}
                    cacheEnabled={false}
                    ref={(webView) => this.webView = webView} // Set ref.
                    onMessage={this.onMessage} // Listens for messages.
                />
                {this.state.sum && 
                <Text>The sum is {this.state.sum}</Text>}
            </View>
        )
    }
}

This component gives a way to call the Rust function add from React Native, through WebAssembly.

Conclusion

Although this solution may seem a bit hacky, I found that it works well for my needs and gives me the least complexity to use the speed and safety of Rust and WebAssembly in my React Native projects.

If you find a problem in the code or a better way to achieve this, don’t hesitate to create an issue on the repository.