article

Compiling C to WebAssembly with Emscripten

3 min read

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:

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:

The browser’s WebAss