Emscripten SDK Setup
Emscripten is a complete compiler toolchain that compiles C/C++ to WebAssembly. It provides its own CMake toolchain file, standard library implementation, and JavaScript glue code generation. The SDK includes clang, the LLVM WebAssembly backend, and system libraries.
# Install 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
# Set up environment variables (add to shell profile)
source ./emsdk_env.sh
# Verify installation
emcc --version
# emcc (Emscripten gcc/clang-like replacement) 3.1.x
# Check which CMake toolchain file is provided
echo $EMSDK
# Shows path to toolchain file at:
# $EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake
./emsdk install 3.1.51. Different versions can produce different WASM output sizes and have varying browser compatibility.
emcmake/emconfigure Usage
Emscripten provides wrapper commands that automatically inject the correct toolchain file and environment. emcmake wraps CMake, while emconfigure wraps autoconf-based configure scripts.
# Method 1: Using emcmake wrapper (recommended)
emcmake cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -S . -B build-wasm
cmake --build build-wasm
# Method 2: Explicit toolchain file
cmake -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake \
-DCMAKE_BUILD_TYPE=Release \
-S . -B build-wasm
# Method 3: Using CMake presets (recommended for projects)
# CMakePresets.json with emscripten toolchain configured
cmake --preset wasm-release
cmake --build --preset wasm-release
cmake_minimum_required(VERSION 3.21)
project(WasmApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(app src/main.cpp src/engine.cpp)
# Emscripten-specific link flags
if(EMSCRIPTEN)
set_target_properties(app PROPERTIES
SUFFIX ".html" # Generate HTML harness alongside .js and .wasm
)
target_link_options(app PRIVATE
-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap']
-sEXPORTED_FUNCTIONS=['_main','_process_data']
-sALLOW_MEMORY_GROWTH=1
-sINITIAL_MEMORY=33554432
--preload-file ${CMAKE_SOURCE_DIR}/assets@/assets
)
endif()
Memory Configuration
WebAssembly linear memory is a contiguous byte array. Emscripten manages this memory with configurable initial size and optional growth. Incorrect memory settings cause out-of-memory crashes or excessive page allocation overhead.
cmake_minimum_required(VERSION 3.21)
project(WasmMemory LANGUAGES CXX)
add_executable(app src/main.cpp)
if(EMSCRIPTEN)
target_link_options(app PRIVATE
# Initial memory: 64MB (must be multiple of 64KB = WASM page size)
-sINITIAL_MEMORY=67108864
# Allow memory to grow beyond initial allocation
-sALLOW_MEMORY_GROWTH=1
# Maximum memory cap (prevents runaway allocation)
-sMAXIMUM_MEMORY=536870912 # 512MB max
# Stack size (default 64KB may be too small for recursive code)
-sSTACK_SIZE=1048576 # 1MB stack
# Abort on allocation failure (clearer than silent corruption)
-sABORTING_MALLOC=1
)
endif()
ALLOW_MEMORY_GROWTH=1 is set, every typed array view (e.g., Module.HEAPU8) can be invalidated after any memory-growing call. JavaScript code holding references to the heap buffer must re-acquire them after calling into WASM. This is the most common source of mysterious crashes in WASM apps.
JavaScript API Bindings
Emscripten's embind system provides type-safe bindings between C++ and JavaScript — exposing classes, functions, and enums as JavaScript objects without manual glue code.
// src/bindings.cpp — Expose C++ API to JavaScript via embind
#include <emscripten/bind.h>
#include <string>
#include <vector>
class ImageProcessor {
public:
ImageProcessor(int width, int height)
: width_(width), height_(height),
pixels_(width * height * 4, 0) {}
void setPixel(int x, int y, int r, int g, int b, int a) {
int idx = (y * width_ + x) * 4;
pixels_[idx] = r;
pixels_[idx + 1] = g;
pixels_[idx + 2] = b;
pixels_[idx + 3] = a;
}
void applyGrayscale() {
for (int i = 0; i < width_ * height_; ++i) {
int idx = i * 4;
int gray = (pixels_[idx] * 299 +
pixels_[idx+1] * 587 +
pixels_[idx+2] * 114) / 1000;
pixels_[idx] = pixels_[idx+1] = pixels_[idx+2] = gray;
}
}
emscripten::val getPixelData() const {
return emscripten::val(
emscripten::typed_memory_view(pixels_.size(), pixels_.data())
);
}
private:
int width_, height_;
std::vector<uint8_t> pixels_;
};
EMSCRIPTEN_BINDINGS(image_module) {
emscripten::class_<ImageProcessor>("ImageProcessor")
.constructor<int, int>()
.function("setPixel", &ImageProcessor::setPixel)
.function("applyGrayscale", &ImageProcessor::applyGrayscale)
.function("getPixelData", &ImageProcessor::getPixelData);
}
cmake_minimum_required(VERSION 3.21)
project(EmbindExample LANGUAGES CXX)
add_executable(imageapp src/bindings.cpp)
if(EMSCRIPTEN)
target_link_options(imageapp PRIVATE
--bind # Enable embind
-sALLOW_MEMORY_GROWTH=1
-sMODULARIZE=1 # Wrap in factory function
-sEXPORT_NAME='createModule' # Module factory name
-sENVIRONMENT='web' # Web-only (smaller output)
)
set_target_properties(imageapp PROPERTIES
SUFFIX ".js"
)
endif()
File System Access
Emscripten provides virtual file systems for WASM modules that need file I/O. MEMFS is in-memory (fast, volatile), while IDBFS persists to IndexedDB in browsers.
cmake_minimum_required(VERSION 3.21)
project(FileSystemApp LANGUAGES CXX)
add_executable(fsapp src/main.cpp)
if(EMSCRIPTEN)
target_link_options(fsapp PRIVATE
# Embed files at build time (packaged into .data file)
--preload-file ${CMAKE_SOURCE_DIR}/data@/data
# Or embed small files directly in JS (no .data download)
--embed-file ${CMAKE_SOURCE_DIR}/config.json@/config.json
# Enable IDBFS for persistent storage
-lworkerfs.js
-sFORCE_FILESYSTEM=1
-sALLOW_MEMORY_GROWTH=1
)
endif()
// src/main.cpp — File system usage with IDBFS persistence
#include <emscripten.h>
#include <stdio.h>
#include <string.h>
int main() {
// Mount IDBFS for persistent storage
EM_ASM(
FS.mkdir('/save');
FS.mount(IDBFS, {}, '/save');
// Sync from IndexedDB to memory
FS.syncfs(true, function(err) {
if (err) console.error('IDBFS load error:', err);
else console.log('Persistent storage loaded');
});
);
// Read preloaded file
FILE* f = fopen("/data/levels.json", "r");
if (f) {
char buf[1024];
size_t n = fread(buf, 1, sizeof(buf) - 1, f);
buf[n] = '\0';
printf("Loaded: %s\n", buf);
fclose(f);
}
return 0;
}
Worker Threads
Emscripten supports pthreads via Web Workers, enabling true multithreading in WASM. This requires SharedArrayBuffer and COOP/COEP headers on the hosting server.
cmake_minimum_required(VERSION 3.21)
project(ThreadedWasm LANGUAGES CXX)
add_executable(threaded src/main.cpp src/worker.cpp)
if(EMSCRIPTEN)
# Enable pthread support
target_compile_options(threaded PRIVATE -pthread)
target_link_options(threaded PRIVATE
-pthread
-sPTHREAD_POOL_SIZE=4 # Pre-spawn 4 workers
-sPTHREAD_POOL_SIZE_STRICT=0 # Allow dynamic growth
-sALLOW_MEMORY_GROWTH=1
-sINITIAL_MEMORY=67108864
)
endif()
Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp headers. Without these, SharedArrayBuffer is unavailable and your threaded WASM will fail to initialize.
Asyncify
Asyncify transforms synchronous C/C++ code to support yielding — enabling blocking operations (sleep, network calls, file dialogs) without freezing the browser's main thread. It works by instrumenting the call stack to save and restore state.
cmake_minimum_required(VERSION 3.21)
project(AsyncApp LANGUAGES CXX)
add_executable(asyncapp src/main.cpp)
if(EMSCRIPTEN)
target_link_options(asyncapp PRIVATE
-sASYNCIFY=1
# Whitelist functions that may sleep (reduces overhead)
-sASYNCIFY_ONLY=['main','gameLoop','loadAsset']
-sALLOW_MEMORY_GROWTH=1
)
endif()
// src/main.cpp — Using asyncify for browser-friendly sleep
#include <emscripten.h>
#include <stdio.h>
void gameLoop() {
for (int frame = 0; frame < 1000; ++frame) {
printf("Frame %d\n", frame);
// This would block the browser WITHOUT asyncify
// With asyncify, it yields to the event loop
emscripten_sleep(16); // ~60 FPS
}
}
int main() {
printf("Starting game loop with asyncify...\n");
gameLoop();
printf("Game loop complete.\n");
return 0;
}
ASYNCIFY_ONLY to limit instrumentation to functions that actually need to yield — this can reduce the overhead to under 5%.
Side Modules
Side modules enable dynamic loading of WASM libraries at runtime — useful for plugin architectures or loading large modules on demand.
cmake_minimum_required(VERSION 3.21)
project(PluginSystem LANGUAGES CXX)
# Main module (has the runtime)
add_executable(main_app src/main.cpp)
# Side module (loaded dynamically)
add_library(plugin SHARED src/plugin.cpp)
if(EMSCRIPTEN)
# Main module links against dlopen support
target_link_options(main_app PRIVATE
-sMAIN_MODULE=2 # Dynamic linking support (optimized)
-sALLOW_MEMORY_GROWTH=1
)
# Side module — no runtime, linked at load time
target_link_options(plugin PRIVATE
-sSIDE_MODULE=2 # Minimal side module
)
set_target_properties(plugin PROPERTIES
SUFFIX ".wasm"
PREFIX ""
)
endif()
Browser Deployment
Optimized deployment requires compression, proper MIME types, and minimal output configuration. Emscripten can target web-only environments, reducing generated code size.
cmake_minimum_required(VERSION 3.21)
project(ProductionWasm LANGUAGES CXX)
add_executable(app src/main.cpp src/engine.cpp)
if(EMSCRIPTEN)
# Production optimization flags
target_compile_options(app PRIVATE
-O3 # Maximum optimization
-flto # Link-time optimization
-fno-exceptions # Saves ~50KB
-fno-rtti # Saves ~10KB
)
target_link_options(app PRIVATE
-O3
-flto
-sENVIRONMENT='web' # Web only (no Node.js support code)
-sMODULARIZE=1 # ES module compatible
-sEXPORT_NAME='initApp'
-sALLOW_MEMORY_GROWTH=1
-sFILESYSTEM=0 # Disable FS if not needed (saves ~70KB)
-sASSERTIONS=0 # Disable runtime assertions
-sMINIFY_HTML=0 # We provide our own HTML
--closure 1 # Run Closure Compiler on JS glue
)
set_target_properties(app PROPERTIES SUFFIX ".js")
endif()
# Build and check output sizes
emcmake cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -S . -B build-prod
cmake --build build-prod
# Check sizes
ls -la build-prod/app.wasm # Typically 100KB–2MB depending on code
ls -la build-prod/app.js # JS glue: 10–50KB
# Compress for serving (Brotli is best for WASM)
brotli -9 build-prod/app.wasm -o build-prod/app.wasm.br
brotli -9 build-prod/app.js -o build-prod/app.js.br
A typical C++ game engine compiled with Emscripten produces a 4MB .wasm file unoptimized. With -O3 -flto --closure 1 -sFILESYSTEM=0 -sASSERTIONS=0, this drops to ~800KB. After Brotli compression, the download is ~250KB — competitive with equivalent JavaScript bundles. Always serve .wasm with Content-Type: application/wasm for streaming compilation.
WASI Target
WASI (WebAssembly System Interface) provides a standardized system call interface for running WASM outside the browser — in runtimes like Wasmtime, Wasmer, and WasmEdge. CMake can target WASI using the wasi-sdk toolchain.
# Install wasi-sdk (provides clang targeting wasm32-wasi)
# Download from https://github.com/WebAssembly/wasi-sdk/releases
export WASI_SDK_PATH=/opt/wasi-sdk
# Configure with wasi-sdk toolchain
cmake -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=${WASI_SDK_PATH}/share/cmake/wasi-sdk.cmake \
-DCMAKE_BUILD_TYPE=Release \
-S . -B build-wasi
cmake --build build-wasi
# Run with Wasmtime
wasmtime build-wasi/app.wasm
# Run with Wasmer
wasmer build-wasi/app.wasm -- --arg1 --arg2
cmake_minimum_required(VERSION 3.21)
project(WasiApp LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
add_executable(app src/main.cpp)
# WASI doesn't support exceptions or RTTI
target_compile_options(app PRIVATE
-fno-exceptions
-fno-rtti
)
# Grant file system access (WASI capability model)
# Runtime must provide --dir=. or --mapdir for file access