Be Faster, let’s adopt WebAssembly

Frank Chung
DeepQ Research Engineering Blog
5 min readDec 19, 2019

This article tells what is WebAssembly, when and how to utilize it.

What is WebAssembly (WASM)?

WebAssembly is a IR language that can be performed in browser or server with JavaScript which across different OS architecture. Emscripten is a toolchain to compile the C/C++ or Rust into wasm bytecode for speeding-up the performance of web programming.

Installation

Follow the tutorial to install the emscripten sdk

# Get the emsdk repo
$ git clone https://github.com/emscripten-core/emsdk.git

# Enter that directory
$ cd emsdk
# Download and install the latest SDK tools.
$ ./emsdk install latest

# Make the "latest" SDK "active" for the current user.
$ ./emsdk activate latest

# Activate PATH and other environment variables.
$ source ./emsdk_env.sh

Build Hello World Example

Write the following hello.c code:

#include <stdio.h>

int main() {
printf("hello, world!\n");
return 0;
}

Build the c code into wasm and js:

# compile the c source.
$ emcc hello.c
# show the output files.
$ tree
.
├── a.out.js
├── a.out.wasm
└── hello.c

Run with Node.js:

$ node a.out.js
hello, world

Run with browser:

# compile the c source.
$ emcc hello.c -o index.html
# show the output files.
$ tree
.
├── hello.c
├── index.html
├── index.js
└── index.wasm
# serve the files.
$ emrun .

How It Works?

Let’s trace the generated js code:

fetch(wasmBinaryFile, { credentials: 'same-origin' })
.then(function (response) {
var result = WebAssembly.instantiateStreaming(response, info);
return ...
}

In browser runtime, the JavaScript helper fetches the .wasm file and compile the bytecode with WebAssembly.instantiateStreaming method.

function doRun() {  if (calledRun) return;  calledRun = true;  if (ABORT) return;  initRuntime();  preMain();  if (Module["onRuntimeInitialized"]) {    Module["onRuntimeInitialized"]();  }  postRun();}

Then the main function will be called after the runtime is initialized, and a callback function onRuntimeInitialized is provided.

Function Binding

However, in most cases main function is not required. For example, let’s write a customized function that can be called many times. Let’s write a math.c:

int add(int x, int y) {  return x + y;}

Since all c functions will be renamed with prefix _ , we can build the above source with exported function _add, and primitive type can be directly passed into add function:

# compile the c source with exported function
$ emcc math.c -s EXPORTED_FUNCTIONS='["_add"]' -o index.js
# show the output files.
$ tree
.
├── index.js
├── index.wasm
└── math.c

Let’s call the add function in Node.js

> const { _add } = require("./index.js");
undefined
> _add(3,5);
8

Yeah, everything seems well. Let’s try again in browser:

<html>  <body>    <script src="index.js"></script>    <script>      // wait for download and compile for .wasm      Module.onRuntimeInitialized = () => {        console.log(_add(3, 5));      };    </script>  </body></html>

Until now, we can call a WebAssembly function in both browser or Node runtime.

Function Parameters

Let’s see the following example which has non-primitive function parameters in string.c:

#include <stdlib.h>#include <stdio.h>#include <string.h>char *concat(uint8_t numbers[], int length)  {    char *buffer = malloc(sizeof(char) * 1024);    for (int i = 0; i < length; i++)    {      char numstr[10];      sprintf(numstr, "%d ", numbers[i]);      strcat(buffer, numstr);    }  return buffer;}

Then build the function, we need to use a wrapper cwrap defined in JavaScript, add it into EXTRA_EXPORTED_RUNTIME_METHODS :

# compile the c source with exported function
$ emcc string.c -s EXPORTED_FUNCTIONS='["_concat"]' -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' -o index.js
# show the output files.
$ tree
.
├── index.js
├── index.wasm
└── string.c

Let’s try in Node.js:

> const { cwrap, _free } = require("./index.js");
undefined
> const concat = cwrap("concat", "string", ["array", "number"]);
undefined
> const str = concat([3,5,7], 3);
undefined
> console.log(str);
3 5 7
undefined
> _free(str); // don't forget to free the allocated memory.
undefined

Be careful that the types are number (for a JavaScript number corresponding to a C integer, float, or general pointer), string (for a JavaScript string that corresponds to a C char* that represents a string) or array (for a JavaScript array or typed array that corresponds to a C array; for typed arrays, it must be a Uint8Array or Int8Array).

The same, in browser:

<html>  <body>    <script src="index.js"></script>    <script>      // wait for download and compile for .wasm      Module.onRuntimeInitialized = () => {        const concat = Module.cwrap("concat", "string", ["array", "number"]);        const str = concat([3, 5, 7], 3);       console.log(str);        // don't forget to free the allocated memory.        Module._free(str);      };    </script>  </body></html>

Some Tricks

Here lists some useful options when compiling the WebAssembly:

  • WebAssembly runtime is stateful, static variables are kept alive even main function returned.
  • Reduce the size by optimizing the codes including Minify and Uglify
-O0 | -O1 | -O2 | -O3 | -Oz
  • Insert the codes into generated JavaScript wrapper
--pre-js <file> | --post-js <file>
  • Bundle the .wasm binary by base64 into JavaScript wrapper to avoid fetching the .wasm file.
-s "SINGLE_FILE=1"
  • Pre-fetch the assets before running the WebAssembly
--preload-file <name>
  • Adjust memory size (default 16 MB)
-s "TOTAL_MEMORY=16777216"

Case Study

Potrace is a useful algorithm for tracing a bitmap and produce a vector-based image (svg, pdf, … etc). The author Peter Selinger released the c implementation as a CLI tool.

Even there is some porting kilobyte/potrace and tooolbox/node-potrace, there still exists a significant difference in performance and functionality.

After studying the WebAssembly, my first practice is porting the c source to WebAssembly and finally it works as a charm, the source is located at IguteChung/potrace-wasm.

Reference

--

--