Compiling C to WebAssembly with Emscripten
WebAssembly lets you run C code in the browser at near-native speed. Emscripten is the tool that makes this practical—it’s a full LLVM-based compiler toolchain that handles the messy work of bridging C semantics to the web platform.
Setting Up Emscripten
Install the SDK and activate it in your shell:
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
Verify with emcc --version. The SDK bundles Clang, the Binaryen optimizer, and Node.js—everything you need.
Your First Compilation
Start with something trivial to confirm the toolchain works:
// math.c
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
Compile it:
emcc math.c -o math.js -s EXPORTED_FUNCTIONS='["_add","_multiply"]' -s MODULARIZE=1
This produces math.js (glue code) and math.wasm (the binary). The underscore prefix is C’s name mangling convention.
Calling from JavaScript
Load the module and call your functions:
import createModule from './math.js';
const Module = await createModule();
const result = Module._add(5, 3);
console.log(result); // 8
For anything beyond integers, you’ll need to manage memory explicitly.
Working with Strings and Memory
Strings require heap allocation. Emscripten provides helpers:
// greet.c
#include <string.h>
#include <stdlib.h>
char* greet(const char* name) {
char* result = malloc(256);
snprintf(result, 256, "Hello, %s!", name);
return result;
}
JavaScript side:
const Module = await createModule();
// Allocate string in WASM memory
const namePtr = Module.allocateUTF8("World");
const resultPtr = Module._greet(namePtr);
// Read result back
const greeting = Module.UTF8ToString(resultPtr);
console.log(greeting); // "Hello, World!"
// Free both allocations
Module._free(namePtr);
Module._free(resultPtr);
Memory leaks in WASM are real. Track your allocations.
Optimization Flags That Matter
Default builds are huge. Production needs optimization:
# Development: fast builds, debuggable
emcc math.c -o math.js -O0 -g
# Production: small and fast
emcc math.c -o math.js -O3 -s MINIMAL_RUNTIME=1
Key flags:
-O3: aggressive optimization-s MINIMAL_RUNTIME=1: strips unused glue code-s FILESYSTEM=0: drops filesystem emulation (saves ~50KB)--closure 1: minifies glue code with Closure Compiler
A simple function that starts at 100KB can shrink below 5KB.
Using WASM Without Glue Code
For minimal footprint, skip the JS glue entirely:
emcc math.c -o math.wasm -s STANDALONE_WASM=1 -s EXPORTED_FUNCTIONS='["_add"]' --no-entry
Load it directly:
const response = await fetch('math.wasm');
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes);
console.log(instance.exports.add(2, 3)); // 5
This gives you maximum control but means implementing memory management yourself.
Debugging Tips
When things break:
- Compile with
-gfor source maps - Use
-s ASSERTIONS=1to catch memory errors - Chrome DevTools can step through C source if DWARF info is included
The browser’s WebAss