C/C++ code in React using WebAssembly
Igor Proskurin
Posted on September 13, 2023
I started looking into WebAssembly (WASM) recently having a small project in mind. The project is to make a web interface for an open-source numeric C++17 library. I was thinking about a web application using a popular framework such as React. So here we go. This post is about my first experience with using C code compiled to WebAssembly in React.
While it was relatively easy to compile and call C-code from JavaScript which I described in the first and second posts, making it work with React required much more time and patience. I did my homework, and I knew about difficulty with loading WASM modules. I read about solutions based on wasm-loader
withreact-app-rewired
as discussed here. And I tried fetch
WASM directly as described in MDN Docs. But each time the result was the same -- my WASM module refused to load at runtime complaining about "incorrect MIME types" or something like that.
Finally, the only solution that really worked for me was to compile C code in Emscripten with -sSINGLE_FILE=1
. What it does? It embeds WebAssembly binary directly into JavaScript glue code, which is then used to call byte code at runtime. This solved my problems with loading WASM modules.
Here is what I did step by step...
Setting up a React App
If you are new to React (like myself), there is a good introduction at MDN Docs. I used Node.js v16.20.0, which is bundled with Emscripten compiler. Just type in the terminal (I used PowerShell this time)
$npx create-react-app react-wasm
$cd react-wasm
$npm start
And we have a demo React project in the directory react-wasm
up and running at localhost:3000
. We will need to install some more stuff for our demo, but it can be done later. There is no rush...
Preparing a toy C library
I am going a prepare a toy C library. One function will be only for side effects, and another one will be a "numeric algorithm" -- it will take data from an input buffer, transform it, and put it into an output buffer. The input buffer will be filled in from JavaScript and the output data should be rendered by some React component (I will just use react-plotly.js
to make some visuals). So here is our C file:
// hello_react.c
#include <assert.h>
#include <stdio.h>
void hello_react() {
printf("Hello, React!\n");
}
void process_data(double* input, double* output, int size) {
int i;
assert(size > 0 && "size must be positive");
assert(input && output && "must be valid pointers");
for (i = 0; i < size; i++) {
output[i] = input[i] * input[i];
}
}
If you like to compile this file as C++, just wrap the function into extern "C" {}
to prevent function names being mangled (in a WASM binary our functions will be translated into symbols _hello_react
and _process_data
). I saved this file as react-wasm/src/hello_react.c
into src
folder of the React project.
The next step is to compile this file into WASM binary and JavaScript glue code to interface it with React. I use Emscripten compiler that wraps around LLVM. There are two compiler options that make our life easier. One is -sMODULARIZE
that helps to run WASM code in Node.js. Another one is -sSINGLE_FILE=1
.
As I mentioned above, without -sSINGLE_FILE=1
the emcc
compiler produces a separate *.wasm
file that has to be loaded into browser at runtime. In my case (I mostly followed a method described here with some variations), whenever I used a separate file, it refused to load, despite the fact that I could build a package with npm
. According to Emscripten FAQ: "another option than a local webserver is to bundle everything into a single file, using -sSINGLE_FILE (as then no XHRs will be made to file:// URLs)." This worked for me.
Okay, the final command to compile out toy C "library" with Emscripten looks like this.
$emcc hello_react.c -o hello_react.js -sMODULARIZE -sSINGLE_FILE=1 -sEXPORTED_FUNCTIONS=_hello_react,_process_data,_malloc,_free,getValue -sEXPORTED_RUNTIME_METHODS=ccall
Now, we have hello_react.js
that contains JavaScript glue code and embedded WASM code (and we don't have a separate hello_react.wasm
). We also asked the compiler to export some functions for us including _hello_react
and _process_data
.
Interfacing with React components
Now comes the fun stuff. How can we integrate our compiled file hello_react.js
into a React component? Let's move step by step...
To call a JavaScript module with WASM code, I will mostly follow the method for interoperability with Node.js from Emscripten Docs. The basics idea is to to call require('./hello_wasm.js')
which returns a promise.
var factory = require('./hello_react.js');
factory().then((instance) => {
instance._hello_react(); // direct calling
instance.ccall("hello_react", null, null, null);
// more code...
});
Here, we can call our C function directly via the exported symbol _hello_react
or using a runtime method ccall
.
Now index.js
inside my src
folder looks like this.
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
var factory = require('./hello_react.js');
factory().then((instance) => {
instance._hello_react(); // direct calling
instance.ccall("hello_react", null, null, null);
});
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
At this point, we can run our project with npm start
... And see that it doesn't work.
The first problem is specific to webpack, and there is a standard workaround, which is called react-app-rewired
. So our next step is to install this package.
$npm install react-app-rewired
And set up config-overrides.js
in the root directory of the project with the following content.
module.exports = function override(config, env) {
config.resolve.fallback = {
fs: false
};
return config;
};
After that I just modify the script
section of the package.json
to call react-app-rewired
at startup.
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
},
Next step is npm install path
so we can silence the following.
Module not found: Error: Can't resolve 'path' in 'C:\Users\Igor\Devel\react-wasm\src'
This almost works. The only remaining thing is the annoying eslint
complaints, which I silenced using a temporary solution simply by adding the following lines on top of hello_react.js
.
/* eslint-disable no-undef */
/* eslint-disable no-restricted-globals */
/* eslint-disable import/no-amd */
Now, we open the console, and see that our function has been called.
Okay, this part works. We can even built package with npm run build
at this point. Now let's go a little bit further and try to connect WASM code with a React component.
Mocking interface between C code and a React component
I will use Plotly.js as a simple graphical react component.
$npm install plotly-react.js plotly.js
Since I am using low-level C code here, I will need to make an input buffer using _malloc
to pass data to the WASM module. We already exported it when compiled our C file with emcc
. And I will use a separate output buffer to get data out. Let me skip some little steps here. More details can be found in my previous post.
Out index.js
now looks like this.
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
var factory = require('./hello_react.js');
factory().then((instance) => {
const inputArr = new Float64Array([0, 1, 2, 3, 5]);
const inputBuff = instance._malloc(inputArr.length * inputArr.BYTES_PER_ELEMENT);
instance.HEAPF64.set(inputArr, inputBuff / inputArr.BYTES_PER_ELEMENT);
let outputArr = new Float64Array(inputArr.length);
const outputBuff = instance._malloc(inputArr.length * inputArr.BYTES_PER_ELEMENT);
instance.ccall('process_data', 'number', ['number', 'number', 'number'], [inputBuff, outputBuff, inputArr.length]);
for (let i = 0; i < outputArr.length; i++) {
outputArr[i] = instance.getValue(outputBuff + i * outputArr.BYTES_PER_ELEMENT, 'double');
}
console.log(inputArr);
console.log(outputArr);
instance._free(outputBuff);
instance._free(inputBuff);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
});
To get data out of the output buffer, I used a low-level operation getValue
(there is no HEAPF64.get
in the API).
for (let i = 0; i < outputArr.length; i++) {
outputArr[i] = instance.getValue(outputBuff + i * outputArr.BYTES_PER_ELEMENT, 'double');
}
Let's check console logs and find correct input and output array values.
Let's now use this data somewhere...
Let's visualize what we get
We can add a simple script into src
folder to plot input and output using a Plotly.js component <Plot />
(sprinkle it with some CSS if you like).
// MyPlot.js
import React from 'react';
import Plot from 'react-plotly.js';
import './MyPlot.css'
function MyPlot(props) {
const xs = props.xs;
const ys = props.ys;
const plots = [{
x: xs,
y: ys,
type: 'scatter',
mode: 'lines+markers',
marker: {color: 'red'}
}];
return (
<div className='MyPlot'>
<Plot
data={ plots }
layout={ {width: 640, height: 480, title: 'Plotly React'} }
/>
</div>
);
}
export default MyPlot;
Now the component is ready to use from index.js
. Just add import MyPlot from './MyPlot'
and update the rendering
root.render(
<React.StrictMode>
<App />
<MyPlot xs={inputArr} ys={outputArr} />
</React.StrictMode>
);
Voila!
Posted on September 13, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.