Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Proper way to load WASM module in React for big files (more than 4kb)

I'm stuck with how to load a C++ function in a React code, using wasm compiler.

My C++ is composed of two files, which result after compilation in a 160kb wasm file. Here is the command I currently use for compilation (running on macOS).

em++ ../main.cpp ../stringFormat.cpp -s WASM=1 -s EXPORT_ALL=1 -s MODULARIZE=1 -O3 --closure 1 -o lss.js -std=c++11

I then copy the lss and the wasm file together in my React code, in the same folder.

src
  - utils
    - wasm
      lss.js
      lss.wasm

But then, whenever I try to import lss.js in another file, my app crashes with a bunch of undefined expressions.

My js file

import * as lss from '../wasm/lss'
./src/utils/wasm/lss.js
  Line 10:6:     Expected an assignment or function call and instead saw an expression  no-unused-expressions
  Line 11:69:    Expected an assignment or function call and instead saw an expression  no-unused-expressions
  Line 11:117:   'read' is not defined                                                  no-undef
  Line 11:197:   'readbuffer' is not defined                                            no-undef
  Line 11:214:   'read' is not defined                                                  no-undef
  Line 11:336:   'quit' is not defined                                                  no-undef
  Line 11:367:   Unexpected use of 'print'                                              no-restricted-globals
  Line 11:430:   Unexpected use of 'print'                                              no-restricted-globals
  Line 11:493:   'printErr' is not defined                                              no-undef
  Line 12:1:     Unexpected use of 'print'                                              no-restricted-globals
  Line 12:22:    Expected an assignment or function call and instead saw an expression  no-unused-expressions
  Line 12:26:    Unexpected use of 'self'                                               no-restricted-globals
  Line 14:307:   Expected an assignment or function call and instead saw an expression  no-unused-expressions
  Line 23:174:   Expected an assignment or function call and instead saw an expression  no-unused-expressions
  Line 29:10:    Expected an assignment or function call and instead saw an expression  no-unused-expressions
  Line 29:152:   'readline' is not defined                                              no-undef
  Line 29:260:   Expected an assignment or function call and instead saw an expression  no-unused-expressions
  Line 29:350:   Expected an assignment or function call and instead saw an expression  no-unused-expressions
  Line 29:433:   Expected an assignment or function call and instead saw an expression  no-unused-expressions
  Line 30:19:    Expected an assignment or function call and instead saw an expression  no-unused-expressions
...

I also tried to generate standalone wasm file by adding a SIDE_MODULE=1 flag on compilation.

// My util function to load wasm file to js
const loadWebAssembly = (filename, imports = {}) =>
    WebAssembly
        .instantiateStreaming(fetch(filename), imports)
        .then(({instance}) => instance.exports);

// wasm file is now in my static folder, in public/
const lss = loadWebAssembly('wasm/lss.wasm')
    // splitIntoCommonSequences is the C++ function I try to call
    .then(module => Promise.resolve(module.splitIntoCommonSequences));

export {lss};

But then I got another error.

WebAssembly.instantiate(): Import #0 module="env" error: module is not an object or function

I tried to figure out how to declare a proper import object for my situation without any success. Is their any better solution ? How do I know what to put inside my import object ?

Thanks !

like image 444
KawaLo Avatar asked Feb 23 '20 14:02

KawaLo


People also ask

How do I load Wasm in React?

Use file: notation to import the wasm package that we created using wasm-pack in the package. json. After adding the package, you just need to run npm install to install the packages. One additional thing you need to take care of is changing the scripts from react-scripts to craco .

How do I load a Wasm file in HTML?

wasm : Create a new XMLHttpRequest() instance, and use its open() method to open a request, setting the request method to GET , and declaring the path to the file we want to fetch. The key part of this is to set the response type to 'arraybuffer' using the responseType property.

Can WebAssembly access file system?

The WebAssembly VM provides a sandbox to ensure application safety. However, this sandbox is also a very limited “computer” that has no concept of file system, network, or even a clock or timer. That is very limiting for the Rust programs running inside WebAssembly.


1 Answers

So I figured out their were more steps to do to get WASM binary working with React, especially if you want to import it in React es6 modules (not from public folder).

I don't claim this as a general solution, just the one that worked for me (and seems to work in most cases).

COMPILER FLAGS

Here is the em++ build command I now use :

em++ ../main.cpp ../stringFormat.cpp \
      -Os -g1 \
      -s WASM=1 \
      -s MALLOC=emmalloc \
      -s ALLOW_MEMORY_GROWTH=1 \
      -s EXPORT_ES6=1 \
      -s MODULARIZE=1 \
      -s 'EXPORT_NAME="LongerSubSequence"' \
      -s 'ENVIRONMENT="web"' \
      --bind \
      -o lss.mjs \
      -std=c++11 || exit 1

Not sure about all the options, they may be some optimization to do here. The important ones are :

  • Bind: Apparently necessary for compiler to override C++ name mangling when naming js modules (by default, C++ compiler will change all variable names kind of like react does with css modules).
  • EXPORT_ES6: Will slightly change the generated glue js syntax so it can be imported in es6 way.
  • MODULARIZE: Export all functions under a common Module (named with EXPORT_NAME flag) so you can just import js as Module and call Module.My_CPP_Function in your js code.

  • g1: Keep generated glue code readable enough to manage next step.

ADDING FILES TO REACT

The process generates two files : lss.mjs and lss.wasm. They go like this in the project tree of React :

My_React_Project
  |_public
  | |_/path/to/lss.wasm
  |
  |_src
    |_/path/to/lss.mjs

/path/to/ can be any path inside the folder, even root.

ADAPT GLUE JS

Finally, to fix errors, I edited the generated lss.mjs file :

  1. Add /* eslint-disable */ at top of the file, to avoid React syntax errors.
  2. Replace var _scriptDir = import.meta.url; with var _scriptDir = '/path/to/lss.wasm';, relative to public folder. Not sure if it is useful regarding the following steps, but React will just crash with the import.meta syntax.
  3. Replace scriptDirectory = self.location.href; with scriptDirectory = window.self.location.href;, since React es6 functions aren't binded to window.
  4. Remove the following block:
var dataURIPrefix = "data:application/octet-stream;base64,";

function isDataURI(filename) {
 return String.prototype.startsWith ? filename.startsWith(dataURIPrefix) : filename.indexOf(dataURIPrefix) === 0;
}

It is used by the compiler to automatically locate the binary file, but I handled it by myself instead.

  1. replace var wasmBinaryFile = "lss.wasm"; with const wasmBinaryFile = '/path/to/lss.wasm'; ('/' will point to public folder).
  2. Remove:
if (!isDataURI(wasmBinaryFile)) {
 wasmBinaryFile = locateFile(wasmBinaryFile);
}
  1. Remove:
function getBinary() {
 try {
  if (wasmBinary) {
   return new Uint8Array(wasmBinary);
  }
  if (readBinary) {
   return readBinary(wasmBinaryFile);
  } else {
   throw "both async and sync fetching of the wasm failed";
  }
 } catch (err) {
  abort(err);
 }
}
  1. Replace the getBinaryPromise() function with the following one:
const getBinaryPromise = () => new Promise((resolve, reject) => {
 fetch(wasmBinaryFile, { credentials: 'same-origin' })
   .then(
     response => {
      if (!response['ok']) {
       throw "failed to load wasm binary file at '" + wasmBinaryFile + "'";
      }
      return response['arrayBuffer']();
     }
   )
   .then(resolve)
   .catch(reject);
});
  1. A few lines below, replace
if (!wasmBinary && typeof WebAssembly.instantiateStreaming === "function" && !isDataURI(wasmBinaryFile) && typeof fetch === "function")

with

if (!wasmBinary && typeof WebAssembly.instantiateStreaming === "function" && typeof fetch === "function")

This just remove the isDataURI condition, since we 'trust' the path we gave to our binary file.

NOW USE YOUR BINARY CODE

And that's all it took for me to manage it. Now, in any of my react files, I can just use my c++ function like this :

anyfile.js

import LongerSubSequenceGlue from 'path/to/lss.mjs';
// Avoid main function running if you just want to use another function
const lssModule = LongerSubSequenceGlue({
    noInitialRun: true,
    noExitRuntime: true
});

//...

lssModule.my_cpp_function(...my_params);

Worked like a charm on Chrome, Firefox and Safari, will make more test later.

like image 193
KawaLo Avatar answered Nov 15 '22 20:11

KawaLo