100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
WebAssembly

Compiling C/C++ to Wasm with Emscripten

Learn how the Emscripten toolchain turns C and C++ source code into WebAssembly modules that run in browsers and Node.js.

ToolchainsIntermediate9 min readJul 10, 2026
Analogies

What Is Emscripten?

Emscripten is a complete LLVM-based compiler toolchain that takes C and C++ source code and emits a WebAssembly binary plus a JavaScript glue file that loads, instantiates, and drives that binary. Under the hood it uses Clang to parse and lower your source to LLVM IR, then its own backend (built on wasm-ld) links that IR into a .wasm module. Because browsers have no POSIX filesystem, threads, or libc, Emscripten also ships reimplementations of malloc, a virtual filesystem, and large chunks of the C/C++ standard library so existing codebases compile with minimal changes.

🏏

Cricket analogy: It is like converting a Test-match innings recorded on paper scoresheets into a digital scorecard app: Virat Kohli's runs are the same underlying data, but the format and delivery mechanism change completely so a phone can display it, just as Emscripten repackages the same C logic for a browser runtime.

The emcc Compiler Driver

Flags That Shape the Output

emcc is the drop-in replacement for gcc or clang that you invoke to build Wasm; it accepts familiar flags like -O2/-O3 for optimization level and -g for debug info, plus Emscripten-specific settings passed via -s KEY=VALUE. Common settings include -s WASM=1 (the default since Emscripten 1.37, forcing Wasm rather than asm.js output), -s EXPORTED_FUNCTIONS=['_main','_add'] to control which C functions are callable from JavaScript, -s MODULARIZE=1 to wrap the glue code in a factory function instead of polluting the global scope, and -s ALLOW_MEMORY_GROWTH=1 to let the linear memory resize beyond its initial allocation. Optimization at -O3 also runs Binaryen's wasm-opt pass, which performs Wasm-specific transformations like dead code elimination and instruction combining that a generic LLVM backend wouldn't know to apply.

🏏

Cricket analogy: Choosing emcc flags is like a captain setting the field before a Rohit Sharma over: EXPORTED_FUNCTIONS is choosing which fielders (functions) are even allowed on the ground, while ALLOW_MEMORY_GROWTH is like keeping a runner on standby in case the innings runs long.

The Virtual Filesystem and JS Interop

Because Wasm has no native concept of files, Emscripten emulates one: MEMFS mounts an in-memory filesystem by default, so calls to fopen() or fread() in your C code operate on data preloaded into the module via --preload-file or written at runtime, while NODEFS and IDBFS let Node.js and browser environments back that filesystem with real disk or IndexedDB storage respectively. For calling into C from JavaScript, Emscripten exposes ccall() and cwrap() for simple functions, and embind (via #include <emscripten/bind.h>) for wrapping C++ classes, STL containers, and overloaded methods with automatic type marshaling. The glue JS file also manages the Module object, which holds callbacks like onRuntimeInitialized and the exported memory and function table.

🏏

Cricket analogy: MEMFS is like a scoring app that keeps the entire match data in the stadium's local scoreboard system during play, only syncing to the cloud (IDBFS-style persistence) once the umpires confirm the result, mirroring how in-memory Wasm filesystem data can be flushed to real storage.

bash
# hello.c
#include <stdio.h>
#include <emscripten/emscripten.h>

EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
    return a + b;
}

int main() {
    printf("Emscripten runtime ready\n");
    return 0;
}

# Build to Wasm + JS glue
emcc hello.c -O3 \
  -s WASM=1 \
  -s MODULARIZE=1 \
  -s EXPORTED_FUNCTIONS="['_add','_main']" \
  -s EXPORTED_RUNTIME_METHODS="['ccall','cwrap']" \
  -s ALLOW_MEMORY_GROWTH=1 \
  -o hello.js

At -O3, Emscripten pipes the LLVM-generated Wasm through Binaryen's wasm-opt, which applies Wasm-native optimizations (like coalescing locals and removing unreachable code after inlining) beyond what LLVM's generic backend produces. This is why -O3 Emscripten builds are often meaningfully smaller and faster than -O2 equivalents, not just marginally so.

Forgetting -s ALLOW_MEMORY_GROWTH=1 means your module's linear memory is fixed at its initial size (often 16MB); any malloc that exceeds it will abort the program with 'memory access out of bounds' instead of growing gracefully, a common source of confusing crashes in production.

  • Emscripten compiles C/C++ via Clang and LLVM IR, then links to .wasm using wasm-ld, plus generates a JS glue file to load it.
  • emcc mirrors gcc/clang syntax; Emscripten-specific behavior is controlled via -s KEY=VALUE settings.
  • EXPORTED_FUNCTIONS controls which C functions JavaScript can call; names must be prefixed with an underscore.
  • MEMFS, NODEFS, and IDBFS provide different backing stores for the emulated filesystem C code expects.
  • ccall/cwrap handle simple function calls from JS; embind handles C++ classes, overloads, and STL types.
  • -O3 triggers Binaryen's wasm-opt for Wasm-specific size and speed optimizations beyond generic LLVM output.
  • ALLOW_MEMORY_GROWTH=1 is required if your program's memory usage may exceed the initial linear memory allocation.

Practice what you learned

Was this page helpful?

Topics covered

#WebAssembly#WebAssemblyStudyNotes#WebDevelopment#CompilingCCToWasmWithEmscripten#Compiling#Wasm#Emscripten#Emcc#StudyNotes#SkillVeris