Skip to content
C++
Domain Track
Difficulty 3/5

WebAssembly with C++ (Emscripten)

Compiling C++ to WebAssembly with Emscripten — toolchain setup, JS bindings with Embind, file system, async/Asyncify, and optimization flags.

TL;DR

Emscripten compiles C++ to WebAssembly. Use Embind to expose C++ classes and functions to JavaScript. Use -O3 -flto for release builds and ASYNCIFY for async interop.

bash
# Install Emscripten SDK
git clone https://github.com/emscripten-core/emsdk.git
./emsdk install latest && ./emsdk activate latest
source ./emsdk_env.sh

# Compile
em++ -O2 -std=c++20 main.cpp -o app.js \
     -s WASM=1 \
     --bind  # enable Embind

Hello World

cpp
// hello.cpp
#include <iostream>
#include <emscripten/emscripten.h>

int main() {
    std::cout << "Hello from WebAssembly!\n";
    return 0;
}
bash
em++ hello.cpp -o hello.html -s WASM=1
# Generates hello.html, hello.js, hello.wasm
# Open hello.html in browser

Exposing C++ to JavaScript with Embind

cpp
// math_lib.cpp
#include <emscripten/bind.h>
#include <cmath>

float lerp(float a, float b, float t) {
    return a + t * (b - a);
}

class Vector2 {
public:
    float x, y;
    Vector2(float x = 0, float y = 0) : x{x}, y{y} {}
    float length() const { return std::sqrt(x*x + y*y); }
    Vector2 normalized() const {
        float l = length();
        return {x/l, y/l};
    }
    Vector2 operator+(const Vector2& r) const { return {x+r.x, y+r.y}; }
};

EMSCRIPTEN_BINDINGS(math_module) {
    emscripten::function("lerp", &lerp);

    emscripten::class_<Vector2>("Vector2")
        .constructor<float, float>()
        .property("x", &Vector2::x)
        .property("y", &Vector2::y)
        .function("length",     &Vector2::length)
        .function("normalized", &Vector2::normalized)
        ;
}
bash
em++ math_lib.cpp -o math.js \
    -s WASM=1 \
    -s MODULARIZE=1 \
    -s EXPORT_NAME="MathLib" \
    --bind
javascript
// JavaScript usage
const MathLib = await require('./math.js')();
const v = new MathLib.Vector2(3, 4);
console.log(v.length());    // 5
const n = v.normalized();
console.log(n.x, n.y);      // 0.6, 0.8
v.delete();  // free C++ object
n.delete();

Memory Management

cpp
// Allocate in C++ heap, share with JS
#include <emscripten/emscripten.h>

extern "C" {

EMSCRIPTEN_KEEPALIVE
uint8_t* allocate_buffer(int size) {
    return new uint8_t[size];
}

EMSCRIPTEN_KEEPALIVE
void free_buffer(uint8_t* buf) {
    delete[] buf;
}

EMSCRIPTEN_KEEPALIVE
void process_image(uint8_t* pixels, int width, int height) {
    for (int i = 0; i < width * height * 4; i += 4) {
        // Convert to grayscale
        uint8_t gray = (pixels[i] + pixels[i+1] + pixels[i+2]) / 3;
        pixels[i] = pixels[i+1] = pixels[i+2] = gray;
    }
}

}
javascript
// Access C++ memory from JS
const ptr = Module._allocate_buffer(width * height * 4);
const view = new Uint8ClampedArray(Module.HEAPU8.buffer, ptr, width * height * 4);
view.set(imageData.data);  // copy in
Module._process_image(ptr, width, height);
imageData.data.set(view);  // copy out
Module._free_buffer(ptr);

Asyncify — Async/Await in C++

cpp
// Calling async JS functions from C++
#include <emscripten/emscripten.h>

// Declare JS function
EM_JS(void, js_fetch_url, (const char* url), {
    // This JS runs asynchronously
    fetch(UTF8ToString(url))
        .then(r => r.text())
        .then(text => {
            // Store result for C++ to read
            Module.fetchResult = text;
        });
});

EM_ASYNC_JS(char*, fetch_sync, (const char* url), {
    const response = await fetch(UTF8ToString(url));
    const text = await response.text();
    const bytes = lengthBytesUTF8(text) + 1;
    const ptr = _malloc(bytes);
    stringToUTF8(text, ptr, bytes);
    return ptr;
});

// With ASYNCIFY, C++ can "block" on JS promises
#include <emscripten/fiber.h>

void download_and_process(const char* url) {
    char* data = fetch_sync(url);  // suspends C++ until JS resolves
    process(data);
    free(data);
}
bash
em++ app.cpp -o app.js \
    -s ASYNCIFY \
    -s ASYNCIFY_IMPORTS='["fetch_sync"]' \
    --bind

File System

cpp
#include <fstream>
#include <emscripten/emscripten.h>

// Emscripten provides an in-memory filesystem
void read_file_example() {
    std::ifstream f("/data/config.json");  // virtual filesystem
    std::string content((std::istreambuf_iterator<char>(f)),
                          std::istreambuf_iterator<char>());
}
bash
# Embed files at compile time
em++ app.cpp -o app.js \
    --embed-file data/config.json@/data/config.json

# Or preload (lazy load from network)
em++ app.cpp -o app.js \
    --preload-file assets@/assets

CMake with Emscripten

cmake
# CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(MyWasmApp)

set(CMAKE_CXX_STANDARD 20)

add_executable(app src/main.cpp src/lib.cpp)

if(EMSCRIPTEN)
    target_link_options(app PRIVATE
        -sWASM=1
        -sMODULARIZE=1
        -sEXPORT_NAME="MyApp"
        --bind
        -sALLOW_MEMORY_GROWTH=1
    )
    set_target_properties(app PROPERTIES
        SUFFIX ".js"
    )
endif()
bash
emcmake cmake -B build-wasm -DCMAKE_BUILD_TYPE=Release
cmake --build build-wasm

Optimization Flags

bash
# Debug
em++ -O0 -g -s ASSERTIONS=2 app.cpp -o app.js

# Release
em++ -O3 -flto --closure 1 app.cpp -o app.js \
    -s WASM=1 \
    -s ENVIRONMENT=web \
    -s FILESYSTEM=0   # disable if not needed (saves ~70KB)

# Size optimized
em++ -Oz --closure 1 app.cpp -o app.js \
    -s WASM=1 \
    -s MINIMAL_RUNTIME=1

Calling C++ from JavaScript (without Embind)

cpp
// Use extern "C" to avoid name mangling
extern "C" {
    EMSCRIPTEN_KEEPALIVE
    int add(int a, int b) { return a + b; }
}
javascript
// Direct call via ccall/cwrap
const result = Module.ccall('add', 'number', ['number', 'number'], [3, 4]);

const addFn = Module.cwrap('add', 'number', ['number', 'number']);
addFn(3, 4);  // 7

Performance Tips

  • Use WASM_BIGINT for 64-bit integer interop
  • Prefer Int32Array/Float32Array typed arrays for bulk data exchange
  • Use --closure 1 for JS minification
  • Enable FILESYSTEM=0 if not using file I/O (-70KB)
  • Use MALLOC=emmalloc for smaller binary when performance isn't critical
  • Profile with browser DevTools — WASM shows in the Performance panel