Compiling C to WASM Without Emscripten: A Step-by-Step Guide
Table of contents

When tasked with running C/C++ code in WebAssembly, the first tool you’ll hear about is Emscripten. It’s not just a compiler but a comprehensive toolkit that bundles C/C++ code, adds a JavaScript runtime, and even implements parts of the standard library. This makes it incredibly convenient, especially since earlier LLVM versions couldn’t produce wasm binaries. However, what if your code is designed specifically for the browser? Or if you want to deeply understand wasm+js interactions? Let’s strip away the extras and start with the basics.
Installing LLVM
We’ll use LLVM (which powers Emscripten under the hood) for compilation. Follow the official guide to install pre-built LLVM binaries for your system.
You can find pre-built releases here. Download and extract the files, ensuring the bin
folder contains the clang
compiler. For macOS, install LLVM 16.0.2 via Homebrew:
brew install llvm
Add the LLVM /path/to/LLVM/
bin directory to your PATH for easy access. Verify the installation:
$ clang --version
Homebrew clang version 16.0.2
Target: x86_64-apple-darwin22.4.0
Thread model: posix
InstalledDir: /usr/local/Cellar/llvm/16.0.2/bin
Writing main.c
Let’s create a simple add(x, y)
function to return the sum of two numbers:
double add(double x, double y) {
return x + y;
}
Compiling main.c
Compile the code with:
clang --target=wasm32 -nostdlib -Wl,--no-entry -Wl,--export-all -o main.wasm main.c
Flags explained:
--target=wasm32
: Compile for WebAssembly.-nostdlib
: Skip standard libraries (nolibc
,malloc
, etc.).-Wl,
: Flags passed to the linker.--no-entry
: Nomain
function required.--export-all
: Export all symbols (we’ll optimize this later).
Running main.wasm in the Browser
Create a basic index.html
to load the module:
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
</head>
<body>
<script type="module">
WebAssembly.instantiateStreaming(fetch("main.wasm")).then((source) => {
const add = source.instance.exports.add;
console.info(`add(1.1, 1.9) = ${add(1.1, 1.9)}`);
});
</script>
</body>
</html>
Browsers block loading local WASM files, so use a local server:
npx serve
Open http://localhost:3000
and check the console:
add(1.1, 1.9) = 3
Basic Size Optimization
The initial main.wasm
is 484 bytes. Adding -Oz
reduces it to 367 bytes. Next, strip unused symbols with -Wl,--strip-all
to shrink it to 272 bytes.
Using --export-all
exposes unnecessary symbols. Replace it with --export-dynamic
and explicitly export the add
function:
__attribute__((visibility("default")))
double add(double x, double y) {
return x + y;
}
Recompile with:
clang -Oz --target=wasm32 -nostdlib -Wl,--strip-all -Wl,--no-entry -Wl,--export-dynamic -o main.wasm main.c
The final main.wasm
is 55 bytes! The exports now include only add
:
Success! Only our C function remains.
This 55-byte module is smaller than an equivalent AssemblyScript example (which includes a sourceMap, resulting in 96 bytes).
Conclusion
We’ve set up LLVM, compiled C code into a minimal, compact WebAssembly module, and called it from JavaScript. We explored compilation flags and linker options for size optimization.
In the next tutorial, we’ll write tests, call JavaScript functions from C, and use WebAssembly built-in instructions without <math.h>
.
Hope this guide worked for you!