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 (4)

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...

Collapse
 
aloth_naveen_4db184e14452 profile image
Aloth Naveen

how did you do for multiple c codes, if i have dependencies on libcurl.so openssl.so then how to resolve it

Collapse
 
iprosk profile image
Igor Proskurin

I would say you'll need those libraries compiled to target wasm architecture, and then link them statically into your binaries. There might be a way to do it with shared libraries as well, but you'll still need a version compiled for wasm. I never tried it.

Thread Thread
 
aloth_naveen_4db184e14452 profile image
Aloth Naveen

cmake_minimum_required(VERSION 3.13)
project((*********)

Set C standard to C99 and enforce its usage

set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)

Add required compiler flags

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DWASM=1 -Wall -Wimplicit-function-declaration")

Include directories

include_directories(
${CMAKE_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/libs/openssl/arm64-v8a/include
${CMAKE_SOURCE_DIR}/libs/curl/arm64-v8a/include
)

Add library dependencies

file(GLOB OPENSSL_LIBS "${CMAKE_SOURCE_DIR}/libs/openssl/arm64-v8a/lib/.a")
file(GLOB CURL_LIBS "${CMAKE_SOURCE_DIR}/libs/curl/arm64-v8a/lib/
.a")

message(STATUS "OPENSSL_LIBS: ${OPENSSL_LIBS}")
message(STATUS "CURL_LIBS: ${CURL_LIBS}")

Add source files

add_executable((*********

***.c
*****.c
******.c
******.c
********.c
Enter fullscreen mode Exit fullscreen mode

)
target_link_libraries((*********
${OPENSSL_LIBS}
${CURL_LIBS}
)

Link libraries

target_link_libraries((*********

${OPENSSL_LIBS}

${CURL_LIBS}

)

Add definitions to avoid implicit declarations

target_compile_definitions((********* PRIVATE
-D_POSIX_C_SOURCE=200809L
-D_GNU_SOURCE
)

Include headers globally

target_include_directories(********* PRIVATE

# ${CMAKE_SOURCE_DIR}/libs/openssl/arm64-v8a/include
# ${CMAKE_SOURCE_DIR}/libs/curl/arm64-v8a/include

)

Debug output for diagnostics

message(STATUS "CMAKE_C_FLAGS: ${CMAKE_C_FLAGS}")
message(STATUS "CMAKE_C_STANDARD: ${CMAKE_C_STANDARD}")

wasm-ld: warning: libs/curl/arm64-v8a/lib/libcurl.a: archive member 'libcurl_la-libssh2.o' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: libs/curl/arm64-v8a/lib/libcurl.a: archive member 'libcurl_la-wolfssh.o' is neither Wasm object file nor LLVM bitcode

i given static library of opensll and libcurl... but final wasm out out is 4kb only is it library are included or not
can you help me to link those libraries