Be Faster, let’s adopt WebAssembly

Frank Chung
Dec 19, 2019 · 5 min read

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
  • Insert the codes into generated JavaScript wrapper
  • Bundle the .wasm binary by base64 into JavaScript wrapper to avoid fetching the .wasm file.
  • Pre-fetch the assets before running the WebAssembly
  • Adjust memory size (default 16 MB)

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

DeepQ Research Engineering Blog

Technical sharing by HTC research engineering team.

Frank Chung

Written by

DeepQ Research Engineering Blog

Technical sharing by HTC research engineering team.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade