Skip to content

C and Emscripten

Emscripten is the classic toolchain for bringing C and C++ code to the web. It compiles to WebAssembly (or the older asm.js fallback), emulates POSIX APIs, and can generate a JavaScript loader that handles instantiation for you.

Terminal window
# Clone the Emscripten SDK
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
# Install and activate the latest release
./emsdk install latest
./emsdk activate latest
# Add emcc to PATH (run this in every new shell, or add to your profile)
source ./emsdk_env.sh
# Verify
emcc --version

Create math.c:

#include <math.h>
// Compute the hypotenuse: sqrt(a*a + b*b)
double hypotenuse(double a, double b) {
return sqrt(a * a + b * b);
}
// Fibonacci (iterative)
int fibonacci(int n) {
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; i++) {
int tmp = a + b;
a = b;
b = tmp;
}
return b;
}
Terminal window
emcc math.c \
-o math.js \
-sEXPORTED_FUNCTIONS='["_hypotenuse","_fibonacci"]' \
-sEXPORTED_RUNTIME_METHODS='["ccall","cwrap"]' \
-sASSERTIONS=1 \
-O2

This produces two files:

  • math.wasm — the compiled binary
  • math.js — the JS loader and runtime glue

The EXPORTED_FUNCTIONS list uses a leading underscore because C symbol names are prefixed with _ by convention in Emscripten.

// In a browser: load the Emscripten-generated JS loader first
// <script src="math.js"></script>
Module.onRuntimeInitialized = function () {
// ccall — one-shot call: (name, returnType, argTypes, args)
const h = Module.ccall(
'hypotenuse', // C function name (without leading _)
'number', // return type
['number', 'number'], // argument types
[3.0, 4.0] // argument values
);
console.log('hypotenuse(3, 4) =', h); // 5
// cwrap — create a reusable JS function
const fib = Module.cwrap('fibonacci', 'number', ['number']);
console.log('fibonacci(10) =', fib(10)); // 55
console.log('fibonacci(20) =', fib(20)); // 6765
};

Emscripten can generate a self-contained HTML shell that embeds the loader and a terminal-style output canvas:

Terminal window
emcc math.c \
-o math.html \
-sEXPORTED_FUNCTIONS='["_hypotenuse","_fibonacci"]' \
-sEXPORTED_RUNTIME_METHODS='["ccall","cwrap"]' \
-O2

Open math.html in a browser and the Emscripten terminal canvas appears. Any C printf calls output there automatically.

When passing strings across the Wasm boundary you must manually allocate and free memory:

#include <string.h>
#include <stdlib.h>
// Returns a heap-allocated copy of the input in upper-case
char* to_upper(const char* s) {
int len = strlen(s);
char* out = malloc(len + 1);
for (int i = 0; i <= len; i++)
out[i] = (s[i] >= 'a' && s[i] <= 'z') ? s[i] - 32 : s[i];
return out;
}
Terminal window
emcc strings.c \
-o strings.js \
-sEXPORTED_FUNCTIONS='["_to_upper","_malloc","_free"]' \
-sEXPORTED_RUNTIME_METHODS='["ccall","cwrap","allocateUTF8","UTF8ToString"]' \
-O2
Module.onRuntimeInitialized = function () {
const ptr = Module.allocateUTF8('hello wasm');
const resultPtr = Module.ccall('to_upper', 'number', ['number'], [ptr]);
console.log(Module.UTF8ToString(resultPtr)); // HELLO WASM
Module._free(ptr);
Module._free(resultPtr);
};
OptionBenefitCost
C/C++ + EmscriptenPort an existing C/C++ codebase almost immediately, reaching decades of accumulated C/C++ ecosystemGlue code and the POSIX emulation layer add runtime overhead, producing a noticeably larger binary than other toolchains
Rust + wasm-bindgenMuch smaller binary, no POSIX emulation to carryRequires rewriting the existing code in Rust — there’s no direct port path
  • Shipping an unoptimized build — not stripping the POSIX emulation layer and not using flags like -O3 or -sMINIMAL_RUNTIME — so the bundle is needlessly bloated
  • Choosing Emscripten just because the team knows C, even for a small module with no existing codebase to port, shipping a runtime far bigger than necessary when Rust or AssemblyScript would fit better
  • Forgetting to Module._free() a pointer allocated with malloc or allocateUTF8, causing a memory leak that accumulates on a long-running page

💡 Real-world example

Figma compiles its own C++ rendering engine with Emscripten to run it in the web app, getting desktop-class performance without rewriting the engine in another language — the company measured more than a 3x improvement in load time after the switch (Figma engineering blog).

Why do EXPORTED_FUNCTIONS entries start with an underscore (e.g. '_hypotenuse')?
What does Module.cwrap return?
Which emcc flag lists functions that should be accessible from JavaScript?
What must you do after allocating a string pointer with allocateUTF8?