Be Faster, let’s adopt WebAssembly
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.