cpp-on-the-web
Compiling C and C++ for the modern web using WebAssembly. Use this skill when you need to port C++ code, build C++ libraries with Emscripten, or set up high-performance C++ components in the browser.
System prompt
Compiling C++ to the Web using WebAssembly
This skill provides guidance for using Emscripten to target the modern web with C and C++. It focuses on ES6 module output, clean JS/C++ interop, and common pitfalls.
Quick Start
- Installation: Use the Emscripten SDK (emsdk) as the quickest way to install.
./emsdk install latest ./emsdk activate latest source ./emsdk_env.sh - Environment: Ensure
emccis in your PATH. - Boilerplate: Use the
hello-worldtemplate inassets/hello-world.main.cpp: Basic Embind example.index.html: Modern ES6 module loading.Makefile: Recommended flags for modern web.
Recommended Compilation Flags
-sSTRICT: Opt into modern Emscripten behavior.-sEXPORT_ES6: Output a modern ES6 module (this implies-sMODULARIZE).-sENVIRONMENT=web: For optimal codesize limit the output only run on the web.-Werror -Wall: Treat warnings as errors for safer C++ code.-Oz/-Os: To minimal the payload size use-Oz/-Osfor release builds rather then-O2/O3.-flto: Use this flag when both compiling and linking in release mode to enable LTO for optimal performance.-sALLOW_MEMORY_GROWTH: Allow the WASM heap to grow (required for many real-world apps).--bind: Enable Embind for clean, class-based interop.
Best Practices
- Prefer standard flags: Use standard compiler flags (e.g.,
-pthread,-g,-O3) over Emscripten-specific-sflags where possible. - Boolean flags: For boolean Emscripten flags (like
-sSTRICT), omit the=1suffix. Emscripten treats the presence of the flag as enabled. - List-based flags: For flags that take lists (e.g.,
-sEXPORTED_FUNCTIONS), use the simple comma-separated form (e.g.,main,malloc) rather than the more verbose JSON form.
Separate Compilation
For larger projects, always use separate compilation (compiling .cpp to .o files before linking). This allows for incremental builds and is the standard practice for C/C++ development.
Compilation step:
em++ -c main.cpp -o main.o $(CXXFLAGS)Linking step:
em++ main.o -o module.mjs $(LDFLAGS)Optimized Builds (Release)
emcc -O3 -flto -c main.cpp -o main.o
emcc -O3 -flto -sSTRICT -sEXPORT_ES6 --bind main.o -o module.mjsDebug Builds
emcc -g -c main.cpp -o main.o
emcc -g -sSTRICT -sEXPORT_ES6 --bind main.o -o module.mjsModern Web Workflows
- Interop with JS: Prefer Embind over
extern "C". It handles complex types (strings, vectors, objects) and classes more safely. - Async Execution: Use Asyncify (
-sASYNCIFY) or JSPI (-sJSPI) for C++ code that needs to call async JavaScript functions. - Porting Libraries: See
references/library-porting.mdfor CMake, autoconf, and Docker workflows.
Common Pitfalls to Avoid
- Blocking the Main Thread: C++ code that runs for long periods without returning control to the browser will freeze the UI. Use
emscripten_set_main_loop()or offload to a Web Worker. - Direct File Access: Standard I/O (e.g.,
fopen) is virtualized. Use MEMFS for small files or specialized Emscripten APIs for persistent storage (IDBFS). - Manual Memory Management: While C++ uses pointers, Emscripten's heap is separate. Be careful with large allocations and always allow memory growth.
- Standard Sockets: Standard BSD sockets won't work in a browser. Use WebSockets or the Emscripten Fetch API.
Reference Materials
- Library Porting & Docker: library-porting.md
- Asset Template:
assets/hello-world/
Attachments
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>C++ to Web Hello World</title>
</head>
<body>
<h1>C++ to Web Hello World</h1>
<p id="output">Loading module...</p>
<script type="module">
import Module from './hello.mjs';
async function init() {
try {
const instance = await Module();
const greeting = instance.sayHello("Modern Web");
document.getElementById('output').textContent = greeting;
console.log('Successfully called C++ function:', greeting);
} catch (err) {
console.error('Error loading module:', err);
document.getElementById('output').textContent = 'Error loading module. Check console.';
}
}
init();
</script>
</body>
</html>
#include <emscripten/bind.h>
#include <iostream>
std::string sayHello(std::string name) {
return "Hello, " + name + " from C++!";
}
EMSCRIPTEN_BINDINGS(hello_module) {
emscripten::function("sayHello", &sayHello);
}
int main() {
std::cout << "C++ Module Loaded!" << std::endl;
return 0;
}
CC = emcc
CXX = em++
CFLAGS = -Wall -Werror -sSTRICT
CXXFLAGS = $(CFLAGS) --bind
# Output flags for modern web: ES6 module, modularized, minimal glue
LDFLAGS = -sEXPORT_ES6 \
-sMODULARIZE \
-sEXPORT_NAME=Module \
-sALLOW_MEMORY_GROWTH \
-sENVIRONMENT=web \
--bind
TARGET = hello.mjs
SRCS = main.cpp
OBJS = $(SRCS:.cpp=.o)
all: $(TARGET)
# Link step
$(TARGET): $(OBJS)
$(CXX) $(OBJS) $(LDFLAGS) -o $(TARGET)
# Compilation step (separate compilation)
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -f $(TARGET) hello.wasm hello.mjs.map $(OBJS)
.PHONY: all clean
# Porting C/C++ Libraries to the Web
This guide covers porting libraries with standard build systems (CMake, autoconf) and using Docker for cross-platform builds.
## General Best Practices
1. **Static Libraries Only:** Emscripten primarily works with static libraries (`.a`). Shared libraries (`.so`, `.dll`) are not supported in the same way.
2. **Handle I/O:** Libraries that rely on direct file system access (e.g., `fopen`, `fread`) will use Emscripten's virtual file system. Ensure you configure it properly (MEMFS by default).
3. **Threading:** Standard pthreads (`-pthread`) require `SharedArrayBuffer`, which needs specific HTTP headers (`Cross-Origin-Embedder-Policy: require-corp` and `Cross-Origin-Opener-Policy: same-origin`).
4. **Network:** Standard BSD sockets are not available. Use `emscripten/fetch.h` or WebSockets.
---
## CMake Porting
The most reliable way is using `emcmake`.
```bash
# In your library directory
mkdir build && cd build
emcmake cmake .. -DCMAKE_BUILD_TYPE=Release
emmake make
```
`emcmake` acts as a wrapper that sets the necessary toolchain variables.
---
## Autoconf Porting (configure scripts)
Use `emconfigure`.
```bash
emconfigure ./configure --host=wasm32-unknown-emscripten --disable-shared
emmake make
```
### Building on Windows (using Docker)
Windows users often lack the environment for `autoconf` (configure scripts). Use the official Emscripten Docker image.
```bash
# Run this from the root of your project
docker run --rm -v $(pwd):/src emscripten/emsdk \
sh -c "emconfigure ./configure --host=wasm32-unknown-emscripten --disable-shared && emmake make"
```
- `-v $(pwd):/src`: Mounts your current directory to `/src` in the container.
- `emscripten/emsdk`: The official SDK image.
- `--host=wasm32-unknown-emscripten`: Critical for configure to recognize the target.
---
## Common Pitfalls
### 1. Blocking the Main Thread
**The Problem:** C++ code often uses synchronous loops or waits. In the browser, this freezes the UI.
**The Fix:**
- Use `emscripten_set_main_loop()` to return control to the browser.
- Use **Asyncify** (`-sASYNCIFY`) to suspend/resume C++ execution during async JS calls.
- Run heavy computation in a **Web Worker**.
### 2. Main Memory Growth
By default, WASM heap is fixed.
**The Fix:** Always compile with `-sALLOW_MEMORY_GROWTH` for modern apps.
### 3. Stack Overflow
The WASM stack is separate from the heap and much smaller by default.
**The Fix:** Increase stack size with `-sSTACK_SIZE=X` (e.g., 5MB) if you have deep recursion or large stack-allocated arrays.