How to Use WebAssembly with Node.js

The reason WebAssembly appealing to me, besides the performance, is that I can run WebAssembly either on client-side or server-side. In this post, I will share how to build WebAssembly file and run it with Node.js.

Environment

  • Node.js v8.11.3
  • IIS v10.0.15063.0

WebAssembly Standalone

When compiling C++ code to WebAssembly, by default, we will get a .js file and a .wasm file. The JavaScript code loads the WebAssembly. However, if your code only contains pure computational code, you can just build a .wasm file without the JS glue. Inspired by StackOverflow, we can build standalone WebAssembly file with the compiling parameter SIDE_MODULE.

Create test.c:

int add(int a, int b) {
  return a + b;
}

Build and output test.wasm:

emcc test.c -O2 -s WASM=1 -s SIDE_MODULE=1 -o test.wasm

Node.js

Load the test.wasm file to buffer.

const fs = require('fs');
var source = fs.readFileSync('./test.wasm');

Convert buffer to typed array:

var typedArray = new Uint8Array(source);

Instantiate the WebAssembly module.

const env = {
    memoryBase: 0,
    tableBase: 0,
    memory: new WebAssembly.Memory({
      initial: 256
    }),
    table: new WebAssembly.Table({
      initial: 0,
      element: 'anyfunc'
    })
  }

WebAssembly.instantiate(typedArray, {
  env: env
}).then(result => {
  console.log(util.inspect(result, true, 0));
  console.log(result.instance.exports._add(9, 9));
}).catch(e => {
  // error caught
  console.log(e);
});

Note: if you do not use the second parameter of instantiate() correctly, you will get the following error:

TypeError: WebAssembly Instantiation: Imports argument must be present and must be an object
    at <anonymous>
    at process._tickCallback (internal/process/next_tick.js:188:7)
    at Function.Module.runMain (module.js:695:11)
    at startup (bootstrap_node.js:191:16)
at bootstrap_node.js:612:3

Run the code:

node index.js

WebAssembly Node.js

Web

The JavaScript code for web is similar.

<!DOCTYPE html>
<html>

<head>
    <title>WebAssembly Standalone</title>
</head>

<body>
    <h1>WebAssembly Standalone</h1>
    <img src="loading.gif" style="margin-top:10px" id="anim-loading">
    <br>
    <input type="number" id="int1" name="int1" value="0" />
    <input type="number" id="int2" name="int2" value="0" />
    <input type="button" value="Scan" onclick="add();" />
    <div id='result'></div>

    <script type="text/javascript">
        var wa_add;

        function add() {
            let int1 = document.getElementById('int1').value;
            let int2 = document.getElementById('int2').value;
            if (wa_add) {
                document.getElementById('result').innerText = wa_add(parseInt(int1), parseInt(int2));
            } else {
                document.getElementById('result').innerText = parseInt(int1) + parseInt(int2);
            }
        }
        const env = {
            memoryBase: 0,
            tableBase: 0,
            memory: new WebAssembly.Memory({
                initial: 256
            }),
            table: new WebAssembly.Table({
                initial: 0,
                element: 'anyfunc'
            })
        };
        fetch('test.wasm').then(response =>
            response.arrayBuffer()
        ).then(bytes =>
            WebAssembly.instantiate(bytes, {
                env: env
            })
        ).catch(e =>
            console.log(e)
        ).then(result => {
            document.getElementById('anim-loading').style.display = 'none';
            wa_add = result.instance.exports._add;
        }).catch(e =>
            console.log(e)
        );
    </script>
</body>

</html>

WebAssembly Web

WebAssembly with JavaScript Glue

Let’s make the C++ code more complicated for memory manipulation.

#include <stdlib.h>
#include <stdint.h>
#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
  return a + b;
}

EMSCRIPTEN_KEEPALIVE
uint8_t* create(int width, int height) {
  return malloc(width * height * 4 * sizeof(uint8_t));
}

EMSCRIPTEN_KEEPALIVE
void destroy(uint8_t* p) {
  free(p);
}

Since standalone WebAssembly does not support malloc() and free(), we need to build the code with JS glue. EMSCRIPTEN_KEEPALIVE is used to export the functions.

emcc test.c -O2 -s WASM=1 -Wall -s MODULARIZE=1 -o test.js

Node.js

The JavaScript code for Node.js is much simpler:

const Module = require('./test.js');
const wasm = Module({wasmBinaryFile: 'test.wasm'});
wasm.onRuntimeInitialized = function() {
    console.log(wasm._add(40, 40));
};

Web

Here is the JavaScrip code embedded in an HTML page:

<script type="text/javascript">
        var wa_add, wa_create, was_destroy, wasm;

        function add() {
            let int1 = document.getElementById('int1').value;
            let int2 = document.getElementById('int2').value;
            if (wa_add) {
                document.getElementById('result').innerText = wa_add(parseInt(int1), parseInt(int2));

                // API test
                if (wa_create && was_destroy) {

                    let canvas = document.getElementById('image');
                    var ctx = canvas.getContext('2d');
                    var image = ctx.getImageData(0, 0, canvas.width, canvas.height);
                    const p = wa_create(canvas.width, canvas.height);
                    wasm.HEAP8.set(image.data, p);
                    // Do something for the image buffer.
                    was_destroy(p);
                }

            } else {
                document.getElementById('result').innerText = parseInt(int1) + parseInt(int2);
            }
        }

        if (Module) {
            wasm = Module({
                wasmBinaryFile: 'test.wasm'
            });
            wasm.onRuntimeInitialized = function () {
                document.getElementById('anim-loading').style.display = 'none';
                wa_add = wasm._add;
                wa_create = wasm._create;
                was_destroy = wasm._destroy;
            };
        }
    </script>

References

Source Code

https://github.com/yushulx/webassembly-nodejs