Emscripten Wasm Guide: Run Legacy C++ in Browser [2026]
Bottom Line
If your old C++ code is mostly portable, you usually do not need a rewrite. A small export surface, a clean Emscripten build, and a real local server are enough to get a browser-ready Wasm module running fast.
Key Takeaways
- ›Use emsdk and the latest SDK tag to avoid stale local toolchains.
- ›Export only the C/C++ symbols you need with -sEXPORTED_FUNCTIONS.
- ›For browser modules, combine -sMODULARIZE with -sEXPORT_ES6.
- ›Serve
.wasmover HTTP, notfile://, or loading will fail.
Running legacy C++ in the browser sounds heavier than it usually is. If the code is already fairly portable and does not depend on OS-specific syscalls, Emscripten can compile it to WebAssembly and generate the JavaScript glue you need to ship it on the web. This guide walks through a minimal, production-minded path: install the SDK, expose a narrow API, compile a browser-friendly module, verify the output, and fix the three browser issues that block most first-time ports.
- Use emsdk and the latest SDK target to stay aligned with current Emscripten builds.
- Keep the exported API small with -sEXPORTED_FUNCTIONS so dead-code elimination can do its job.
- Generate modern browser output with -sMODULARIZE and -sEXPORT_ES6.
- Always test through HTTP because
.wasmloading commonly fails onfile://.
Prerequisites
Bottom Line
Treat your first browser port like a packaging exercise, not a rewrite. Start with one callable function, one clean module build, and one browser verification loop.
Before you touch the compiler, make sure your setup matches what the current Emscripten documentation expects.
- Git installed, because the recommended SDK flow starts by cloning
emsdk. - Python 3.8+ available on your machine. That is the documented baseline for Windows, and Emscripten uses Python tooling across platforms.
- A modern browser with WebAssembly support.
- A codebase that is mostly portable C or C++ and not tightly coupled to native windowing, threads, or filesystem assumptions.
- A local HTTP server for testing. Emscripten's own emrun is the easiest path for first verification.
Step 1: Install Emscripten
Emscripten recommends using emsdk, not a third-party package manager, because that is the officially supported and continuously tested path. The safest habit is to install the latest SDK target 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
Check that the toolchain is live:
emcc --version
em++ --version
Expected result:
- The commands return version information instead of
command not found. - Your shell session now has the Emscripten paths loaded.
Step 2: Prepare Legacy C++
Do not start by porting the entire application. Start by exposing one narrow function from the old codebase and prove the browser round trip. That reduces ambiguity around exports, name mangling, and runtime setup.
Minimal example
Create a file named legacy_math.cpp:
#include <emscripten/emscripten.h>
extern "C" {
EMSCRIPTEN_KEEPALIVE
int score_value(int base, int multiplier) {
return base * multiplier + 7;
}
}
This example does three useful things:
extern "C"avoids C++ name mangling for the exported symbol.- EMSCRIPTEN_KEEPALIVE makes the function a clear candidate for retention.
- The function signature uses plain integers, which makes the first JavaScript call path simple.
If your real code depends on file I/O, sockets, or platform APIs, isolate the algorithmic core first. Browser ports go faster when computation is separated from environment code.
Step 3: Compile to Wasm
For a browser-facing module, a modern default is an ES module wrapper plus explicit exports. Emscripten documents that EXPORTED_FUNCTIONS keeps native symbols alive and accessible, and that EXPORTEDRUNTIMEMETHODS is required when you want helpers like ccall.
mkdir -p web
em++ legacy_math.cpp -O2 --no-entry \
-o web/legacy_math.mjs \
-sMODULARIZE \
-sEXPORT_ES6 \
-sEXPORTED_FUNCTIONS=_score_value \
-sEXPORTED_RUNTIME_METHODS=ccall
Why these flags matter:
- em++ is the C++ frontend.
- -O2 gives you a realistic optimized build without jumping straight to the most aggressive settings.
- --no-entry fits a library-style module that exports functions instead of booting a native-style
main(). - -sMODULARIZE wraps the output as an async factory.
- -sEXPORT_ES6 emits ES module exports, which fit modern browser code cleanly.
- -sEXPORTEDFUNCTIONS=score_value preserves the one native function you intend to call.
- -sEXPORTEDRUNTIMEMETHODS=ccall makes
Module.ccall()available to your JavaScript loader.
After the build, you should see at least these outputs in web/:
legacy_math.mjslegacy_math.wasm
Step 4: Load and Verify in the Browser
Now create a tiny browser harness. Because Emscripten's modularized ES output initializes asynchronously, your page should await the module before making calls.
HTML file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Legacy C++ in Wasm</title>
</head>
<body>
<h1>Legacy C++ in the Browser</h1>
<p id="result">Loading...</p>
<script type="module" src="./main.js"></script>
</body>
</html>
JavaScript loader
import init from "./legacy_math.mjs";
const Module = await init();
const value = Module.ccall("score_value", "number", ["number", "number"], [9, 4]);
document.getElementById("result").textContent = `score_value(9, 4) = ${value}`;
console.log("Wasm result:", value);
Serve the directory over HTTP. Emscripten documents emrun specifically for running generated HTML through a local web server and avoiding browser restrictions on file://.
cd web
emrun --no_browser --port 8080 index.html
Open http://localhost:8080/index.html in your browser.
Verification and expected output
- The page renders
score_value(9, 4) = 43. - The browser console prints
Wasm result: 43. - The network panel shows the browser requesting both
legacy_math.mjsandlegacy_math.wasm.
If you see those three signals, you have a working end-to-end C++ to Wasm browser flow.
Troubleshooting Top 3
1. The browser fails to fetch or instantiate the Wasm file
This is usually a serving problem, not a compiler problem. MDN documents WebAssembly.instantiateStreaming() as the efficient loading path and notes that the server should return the .wasm file with the application/wasm MIME type.
- Do not double-click the HTML file and run from
file://. - Use emrun or another HTTP server.
- Check the network tab for a 404 or wrong content type.
2. JavaScript says the function is missing
If Module.ccall("score_value", ...) fails, the symbol was probably removed or never exported.
- Confirm the compile command includes -sEXPORTEDFUNCTIONS=score_value.
- Keep the leading underscore in the compiler flag. Emscripten documents that native symbols in EXPORTED_FUNCTIONS require it.
- Keep
extern "C"on simple C-style exports to avoid name-mangled symbols.
3. The code compiles, but your real app still does not port cleanly
This is where browser assumptions matter more than compiler syntax.
- Audit filesystem access, threads, blocking calls, and native windowing first.
- Split computation from platform integration so only the portable core goes to Wasm.
- Port incrementally: one function, then one subsystem, then the full module.
What's Next
Once the minimal module is working, move from proof of concept to production shape.
- Replace the demo function with a real computation-heavy routine from your codebase.
- Measure startup time, transfer size, and call overhead before broadening scope.
- Decide whether direct exports,
ccall, or higher-level bindings are the right interface for your app. - Add a build script so the Emscripten command becomes reproducible in CI.
- Test the module behind your actual frontend bundler and deploy path, not only in a standalone folder.
The practical lesson is simple: Emscripten works best when you port the stable, CPU-bound core first and treat browser integration as a separate concern. That approach keeps the migration measurable, keeps the exported surface small, and gives you a browser result early enough to justify deeper investment.
Frequently Asked Questions
How do I run a C++ file in the browser with Emscripten? +
emsdk, compile the source with em++, and generate a browser module plus a .wasm file. Then serve the files over HTTP, import the generated module in JavaScript, and call the exported function after initialization completes.Why does my Emscripten build work in Node but fail in the browser? +
.wasm files, wrong MIME types, or CSP restrictions are more common than compiler problems in first browser tests.What does EXPORTED_FUNCTIONS do in Emscripten? +
_score_value.Do I need ccall to use WebAssembly from JavaScript? +
ccall is a convenience layer, not a requirement. It is useful for first ports because it handles simple argument marshaling cleanly, but you can later move to direct exports or more specialized bindings if you need tighter control.Get Engineering Deep-Dives in Your Inbox
Weekly breakdowns of architecture, security, and developer tooling — no fluff.