Skip to content
Beyond Coordinates
Go back

WebAssembly Study Notes

Updated:

WebAssembly (Wasm) is a portable binary instruction format designed for safe and efficient execution. MDN describes it as:

A compact, low-level compilation target that runs in modern browsers at near-native speed and interoperates with JavaScript.

Languages with an appropriate compiler can target Wasm and run in the browser. Mozilla, Google, Microsoft, and Apple collaborated on the design, and Firefox, Chrome, Edge, and Safari converged on support in 2017—an unusual degree of agreement around a technology that was not yet standardized.

On December 5, 2019, WebAssembly became a W3C Recommendation alongside the web’s established core technologies.

Before WebAssembly

JavaScript was created in 1995 to make HTML pages dynamic, not to serve as a high-performance systems target. As the web expanded into image processing, video, and 3D rendering, browser vendors competed to execute it faster.

The Browser Performance Race

Chrome’s 2008 release popularized JIT compilation through V8. Apple followed with JavaScriptCore/Nitro, Mozilla with TraceMonkey, and Microsoft with Chakra.

A JIT identifies hot code, compiles it to machine instructions, caches the result, and may run an optimizing compiler for the hottest paths. See A crash course in just-in-time compilers.

JavaScript performance improved rapidly:

Faster engines enabled richer media, games, and server-side JavaScript. JIT is not free: compiled code consumes memory, and optimization depends on stable type assumptions that dynamic JavaScript can violate. Consider:

function arraySum(arr) {
  var sum = 0;
  for (var i = 0; i < arr.length; i++) {
    sum += arr[i];
  }
}

Optimizing sum += arr[i] requires assumptions about whether the operation is numeric addition or string concatenation. The engine guards those assumptions and discards optimized code when a different type appears, a process called deoptimization or bailing out:

arr = [1, "hello"];

Repeated optimization and deoptimization can erase the expected benefit.

High-performance applications therefore motivated execution formats beyond ordinary JavaScript.

Google’s NaCl and PNaCl

Google open-sourced Native Client in 2008 and enabled it in Chrome 14. NaCl ran an AOT-compiled safe subset of native code inside a sandbox and communicated with JavaScript through PPAPI. Performance approached native applications, but architecture-specific binaries harmed portability.

Portable Native Client (PNaCl) instead distributed platform-independent LLVM IR, with final compilation occurring for the target machine.

PNaCl anticipated several WebAssembly ideas, but browser-native code remained controversial and the ecosystem did not converge on Google’s plugin interface.

Chrome removed PNaCl support in 2018 in favor of WebAssembly.

Mozilla’s asm.js

At Mozilla, Alon Zakai created Emscripten to compile C and C++ programs, including game engines, into JavaScript.

Unlike a conventional native compiler, the original Emscripten operated as a source-to-source compiler targeting JavaScript.

Its browser compatibility led to asm.js in 2013 and Firefox’s OdinMonkey optimization path.

asm.js is a statically typed, allocation-disciplined subset of JavaScript designed for predictable JIT optimization. For example, this C function:

int f(int i) {
  return i + 1;
}

can become:

function f(i) {
  i = i | 0;
  return (i + 1) | 0;
}

The |0 coercions make integer types explicit and let specialized engines skip much inference. Because asm.js remains valid JavaScript, it can run in any browser, albeit more slowly without dedicated optimization.

Its text format is large and slow to parse, and generated type annotations make it unreadable:

Even heavily optimized asm.js remains constrained by JavaScript syntax and execution semantics.

Other Attempts

Other attempts included Dartium, a browser with a native Dart VM. Google abandoned the browser strategy in 2015; Dart later found a major role in Flutter.

WebAssembly’s Synthesis

WebAssembly combined lessons from NaCl/PNaCl and asm.js:

The project was named WebAssembly in 2015, shipped across major browsers in 2017, and became a W3C standard in 2019.

Getting Started

Many languages compile to Wasm; the official developer guide lists available toolchains. The following example focuses on C/C++, with corresponding guides available for Rust and Go.

Compiling C/C++ to WebAssembly

Install the Emscripten SDK using its setup documentation, then verify the environment:

$ emcc --check
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.24 (68a9f990429e0bcfb63b1cde68bad792554350a5)
shared:INFO: (Emscripten: Running sanity checks)

Create hello.c:

#include <stdio.h>

int main() {
    printf("Hello World\n");
    return 0;
}

Compile it with emcc:

$ emcc hello.c -o hello.html

This generates:

The page must be served over HTTP because browser security rules prevent the required module fetches from a file:// page.

Start a local server with npm:

$ npx serve .

or Python:

$ python3 -m http.server

Open hello.html:

The C program’s output now appears in the browser.

Functions can also be exported for JavaScript. Emscripten keeps main() by default; mark another function with EMSCRIPTEN_KEEPALIVE in greet.c:

#include <stdio.h>
#include <emscripten/emscripten.h>

int main() {
    printf("Hello World\n");
    return 0;
}

#ifdef __cplusplus
#define EXTERN extern "C"
#else
#define EXTERN
#endif

EXTERN EMSCRIPTEN_KEEPALIVE void greet(char* name) {
    printf("Hello, %s!\n", name);
}

Export the ccall runtime method and keep the runtime alive:

$ emcc -o greet.html greet.c -s NO_EXIT_RUNTIME=1 -s EXPORTED_RUNTIME_METHODS=ccall

Add a button to the generated page:

<button id="mybutton">Click me!</button>

Call greet() through the exported ccall method:

document.getElementById("mybutton").addEventListener("click", () => {
  const result = Module.ccall(
    "greet", // name of C function
    null, // return type
    ["string"], // argument types
    ["WebAssembly"] // arguments
  );
});

Exporting cwrap as well creates a reusable JavaScript wrapper around a C function, whereas ccall performs one direct call.

Clicking the button prints the greeting on the page and in the console.

WebAssembly Text Format

The compact .wasm binary is not convenient to inspect. WebAssembly therefore defines both a binary format and the editable, assembly-like WebAssembly Text Format, commonly stored as .wat.

Example:

(module
  (func $add (param $lhs i32) (param $rhs i32) (result i32)
    local.get $lhs
    local.get $rhs
    i32.add)
  (export "add" (func $add))
)

A Wasm module is represented by nested S-expressions. This module defines the equivalent of i32 add(i32 lhs, i32 rhs) and exports it as add. See Understanding WebAssembly text format.

Save it as add.wat and use WABT’s wat2wasm to compile it:

$ wat2wasm add.wat -o add.wasm

Load the module and call add():

fetchAndInstantiate("add.wasm").then(function (instance) {
  console.log(instance.exports.add(1, 2)); // "3"
});

// fetchAndInstantiate() found in wasm-utils.js
function fetchAndInstantiate(url, importObject) {
  return fetch(url)
    .then(response => response.arrayBuffer())
    .then(bytes => WebAssembly.instantiate(bytes, importObject))
    .then(results => results.instance);
}

When served from an HTML page, the console prints 3. Chrome DevTools can also debug Wasm modules.

WebAssembly Outside the Browser

WebAssembly’s portability, sandboxing, and efficiency also make it useful outside browsers. WASI, the WebAssembly System Interface, defines capabilities for those environments.

Running a WASI module requires a compatible runtime such as Wasmtime, Wasmer, or WasmEdge. Their SDKs embed Wasm modules in many host languages.

References


Share this post:

Previous Post
Quadtrees and Collision Detection
Next Post
Understanding C++ Rvalue References and std::move