WebAssembly with Emscripten
Purpose
Guide agents through compiling C/C++ to WebAssembly using Emscripten: emcc flag selection, function exports, memory model configuration, Asyncify for asynchronous C code, debugging WASM binaries, and targeting WASI vs browser.
Triggers
-
"How do I compile C to WebAssembly with Emscripten?"
-
"How do I export a C function to JavaScript?"
-
"How does WebAssembly memory work with Emscripten?"
-
"How do I debug a .wasm file?"
-
"How do I use Asyncify to make synchronous C code async?"
-
"What's the difference between WASI and Emscripten browser target?"
Workflow
- Setup and first build
Install Emscripten SDK
git clone https://github.com/emscripten-core/emsdk.git cd emsdk ./emsdk install latest ./emsdk activate latest source ./emsdk_env.sh # add emcc to PATH
Verify
emcc --version
Compile C to WASM (browser target)
emcc hello.c -o hello.html # generates hello.html + hello.js + hello.wasm emcc hello.c -o hello.js # just JS + WASM (no HTML shell)
Serve locally (WASM requires HTTP, not file://)
python3 -m http.server 8080
Open: http://localhost:8080/hello.html
- Exporting functions to JavaScript
// math.c #include <emscripten.h>
// EMSCRIPTEN_KEEPALIVE prevents dead-code elimination EMSCRIPTEN_KEEPALIVE int add(int a, int b) { return a + b; }
EMSCRIPTEN_KEEPALIVE double sqrt_approx(double x) { return x * 0.5 + 1.0; }
Export specific functions
emcc math.c -o math.js
-s EXPORTED_FUNCTIONS='["_add","_sqrt_approx"]'
-s EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]'
-s MODULARIZE=1
-s EXPORT_NAME=MathModule
The leading underscore is required for C functions
// Using exported functions in JS const Module = await MathModule();
// Direct call const result = Module._add(3, 4);
// Via ccall (type-safe) const result2 = Module.ccall('add', 'number', ['number', 'number'], [3, 4]);
// Via cwrap (creates a callable JS function) const add = Module.cwrap('add', 'number', ['number', 'number']); console.log(add(3, 4)); // 7
- Memory model
Emscripten provides a linear memory heap accessible from both C and JS:
Configure initial and maximum heap
emcc prog.c -o prog.js
-s INITIAL_MEMORY=16MB
-s MAXIMUM_MEMORY=256MB
-s ALLOW_MEMORY_GROWTH=1 # allow dynamic growth
Stack size (default 64KB)
emcc prog.c -o prog.js -s STACK_SIZE=1MB
Shared memory (for SharedArrayBuffer / threads)
emcc prog.c -o prog.js -s SHARED_MEMORY=1 -s USE_PTHREADS=1
// Accessing C memory from JS const ptr = Module._malloc(1024); // allocate Module.HEAPU8.set([1, 2, 3], ptr); // write bytes Module._free(ptr); // free
// Read a C string const strPtr = Module.ccall('get_message', 'number', [], []); const str = Module.UTF8ToString(strPtr);
// Write a string to C const jsStr = "hello"; const cStr = Module.stringToNewUTF8(jsStr); // malloc + copy Module._process_string(cStr); Module._free(cStr);
- Asyncify — synchronous C in async environments
Asyncify lets synchronous C code suspend and resume for async operations (like fetch() , sleep, etc.):
// async.c #include <emscripten.h>
// Synchronous sleep in C (blocks C, but yields to JS event loop) EM_JS(void, do_fetch, (const char *url), { // Emscripten generates wrappers to suspend C while JS runs Asyncify.handleAsync(async () => { const resp = await fetch(UTF8ToString(url)); const text = await resp.text(); console.log(text); }); });
void process_url(const char *url) { do_fetch(url); // looks synchronous in C printf("fetch complete\n"); }
Enable Asyncify
emcc async.c -o async.js
-s ASYNCIFY
-s ASYNCIFY_STACK_SIZE=16384
-O2 # Asyncify works better with optimization
- Optimization and wasm-opt
Optimization levels
emcc prog.c -O0 -o prog.js # no optimization (fastest build) emcc prog.c -O2 -o prog.js # balanced emcc prog.c -O3 -o prog.js # aggressive emcc prog.c -Os -o prog.js # optimize for size emcc prog.c -Oz -o prog.js # aggressive size (Emscripten's smallest)
Post-process with wasm-opt (Binaryen)
wasm-opt -Oz -o prog.opt.wasm prog.wasm # optimize for size wasm-opt -O4 -o prog.opt.wasm prog.wasm # optimize for speed
Compare sizes
ls -lh prog.wasm prog.opt.wasm
- Debugging WASM
Build with debug info
emcc prog.c -g -O0 -o prog.html
-s ASSERTIONS=1
-s SAFE_HEAP=1 # catch misaligned accesses
In Chrome DevTools:
Sources → prog.wasm → line-by-line C source debugging
(requires -g and browser with WASM debugging support)
LLDB with WASM (wasmtime)
See skills/runtimes/wasm-wasmtime for CLI WASM debugging
Emscripten debug helpers
emcc prog.c -o prog.js
-s ASSERTIONS=2 # extensive runtime checks
-s SAFE_HEAP=1 # sanitize heap accesses
-s STACK_OVERFLOW_CHECK=1
Print generated JS
emcc prog.c -o prog.js && cat prog.js | head -100
- WASI vs browser target
Feature Browser (Emscripten) WASI
Host APIs Web APIs (fetch, WebGL, etc.) POSIX subset (files, stdin/stdout)
Runtime Browser JS engine wasmtime, wasmer, WAMR, Node.js
Threads SharedArrayBuffer + pthreads wasi-threads (limited)
Networking fetch(), WebSocket wasi-http (preview2)
Use case Web applications Server-side, CLI tools, edge
Build for WASI (no browser JS, pure WASM)
emcc prog.c -o prog.wasm --target=wasi
Or use wasi-sdk (better WASI support than Emscripten)
/opt/wasi-sdk/bin/clang --sysroot=/opt/wasi-sdk/share/wasi-sysroot
prog.c -o prog.wasm
wasmtime prog.wasm
For Emscripten linker flags reference, see references/emscripten-linker-flags.md.
Related skills
-
Use skills/runtimes/wasm-wasmtime for server-side WASM with wasmtime CLI and Rust embedding
-
Use skills/compilers/clang for Clang-based WASM compilation with WASI SDK
-
Use skills/binaries/elf-inspection for inspecting WASM binary structure