Domain Track
Difficulty 3/5WebAssembly 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 EmbindHello 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 browserExposing 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" \
--bindjavascript
// 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"]' \
--bindFile 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@/assetsCMake 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-wasmOptimization 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=1Calling 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); // 7Performance Tips
- Use
WASM_BIGINTfor 64-bit integer interop - Prefer
Int32Array/Float32Arraytyped arrays for bulk data exchange - Use
--closure 1for JS minification - Enable
FILESYSTEM=0if not using file I/O (-70KB) - Use
MALLOC=emmallocfor smaller binary when performance isn't critical - Profile with browser DevTools — WASM shows in the Performance panel