DEV Community

Igor Proskurin
Igor Proskurin

Posted on

C/C++ code in React using WebAssembly

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
Enter fullscreen mode Exit fullscreen mode

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];
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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...
});
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

At this point, we can run our project with npm start... And see that it doesn't work.

Image description

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
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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"
  },
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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 */
Enter fullscreen mode Exit fullscreen mode

Now, we open the console, and see that our function has been called.

Image description

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
Enter fullscreen mode Exit fullscreen mode

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>
  );

});
Enter fullscreen mode Exit fullscreen mode

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');
  }
Enter fullscreen mode Exit fullscreen mode

Let's check console logs and find correct input and output array values.
Image description

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;
Enter fullscreen mode Exit fullscreen mode

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>
  );
Enter fullscreen mode Exit fullscreen mode

Voila!

Image description

Top comments (1)

Collapse
 
joyhughes profile image
Joy Hughes

I got the first part working! Now I just need to drop in my complex C++ program on one end and my React interface on the other and I should be good to go...