Compiling C and C++ Games with Emscripten

Updated June 2026
Emscripten is the toolchain that compiles C and C++ to WebAssembly and provides the runtime glue that connects your code to the browser. It maps OpenGL onto WebGL, implements the standard C library, simulates a filesystem, and generates the JavaScript loader, so an existing SDL or OpenGL game can often reach the web with surprisingly little change. This guide walks through the full process, from installing the SDK to shipping a playable build.

Emscripten has been the backbone of native-to-web ports for over a decade, and it is built on the same LLVM compiler infrastructure as Clang. That heritage is why it handles real C and C++ codebases rather than toy examples. The work is less about rewriting your game and more about adapting a handful of platform assumptions, chiefly the main loop and the way assets are loaded, to fit the browser. The steps below take you through that adaptation in order.

Step 1: Install the Emscripten SDK

Emscripten is distributed as the emsdk, a small manager that downloads and activates the compiler. Clone its repository, then install and activate the latest version. On a typical system that is three commands: cloning emsdk, running its install for the latest toolchain, and running its activate step. The final piece is sourcing the environment script it provides, which puts the emcc compiler and its companions on your path for the current shell. After this, typing emcc --version should report the installed version, confirming the toolchain is ready.

The one thing to remember is that the environment is per-shell unless you make it permanent. Each new terminal needs the environment sourced again, or you add the source line to your shell profile so it loads automatically. This trips up newcomers who install successfully, open a fresh terminal the next day, and find emcc missing. It is not gone, the shell simply has not loaded the Emscripten environment yet.

Step 2: Compile a Test Program

Before touching your game, prove the toolchain works on something trivial. Write a one-line C program that prints a message, and compile it with emcc hello.c -o hello.html. Emscripten produces three files: an HTML page, a JavaScript loader, and the wasm binary. You cannot open the HTML directly from the filesystem because browsers block module loading over the file:// protocol, so serve the folder with any local web server and open the page. Seeing your message appear in the page's console area confirms the whole pipeline, compiler, loader, and wasm execution, is working end to end.

This small step matters because it isolates toolchain problems from game problems. If hello world compiles and runs, every later issue is about your code or your flags, not about whether Emscripten is installed correctly. Skipping this and going straight to a large codebase makes every error ambiguous.

Step 3: Convert the Main Loop

The single most important change to a native game is the main loop. A native game typically runs an infinite while loop that updates and renders forever, blocking until the program exits. That model is incompatible with the browser, which is single-threaded and must stay in control to handle events, paint the page, and avoid freezing the tab. An infinite loop would hang the browser completely.

Emscripten's answer is emscripten_set_main_loop. You refactor your loop body, the part that updates one frame and renders it, into a function, and hand that function to Emscripten instead of looping yourself. The browser then calls your frame function once per animation frame through requestAnimationFrame under the hood, giving control back to the browser between frames. This is the same cooperative model every web game uses, and adapting to it is usually the bulk of the porting effort. Once your update-and-render logic lives in a per-frame function rather than an endless loop, the rest of the port tends to fall into place.

Key Takeaway

The infinite native game loop must become a per-frame function passed to emscripten_set_main_loop, so the browser stays in control and the tab never freezes. This is the heart of an Emscripten port.

Step 4: Map Graphics to WebGL

Emscripten cannot run desktop OpenGL in a browser, but it provides an OpenGL ES and WebGL compatible layer, and it bundles a port of SDL, the cross-platform library many C and C++ games already use for windowing, input, and rendering. If your game uses SDL or OpenGL ES style calls, you compile against Emscripten's versions and your rendering maps onto WebGL with little or no change to your drawing code. You enable these by passing the right flags, such as the SDL2 port flag, so the compiler links its browser-backed implementations instead of the native ones.

Games that use modern OpenGL ES 3.0 features map onto WebGL 2, which is widely supported. The friction appears when a game relies on desktop-only OpenGL features that have no WebGL equivalent, in which case those specific calls need adapting. For most 2D games and many straightforward 3D games built on SDL or OpenGL ES, though, this step is more about setting flags than rewriting renderers. The WebGL background is useful here for understanding what the layer can and cannot express.

Step 5: Bundle Assets and Set Flags

Native games load assets from disk, but a wasm build has no real filesystem. Emscripten simulates one, and you populate it by preloading files at build time. Passing the preload flag with your asset folder packages those files so your existing file-reading code finds them at the expected paths inside the virtual filesystem. For larger games you can load assets asynchronously over the network instead, but preloading is the simplest way to get an existing game running, since it keeps your file paths unchanged.

This is also where you set the flags that shape the production build. An optimization flag such as -O2 or -O3 turns on the compiler optimizations that make the wasm fast. Memory flags control how the module's linear memory grows. Export flags expose specific functions to JavaScript if you need to call into the module directly. Getting these right is the difference between a sluggish debug build and a tight release build, so a production compile looks quite different from the quick test compile in step two.

Step 6: Test and Ship

Finally, serve the build over HTTP and test it in more than one browser, since differences in WebGL support and performance show up across Chrome, Firefox, and Safari, and especially on mobile. Check load time honestly, because the wasm binary plus your preloaded assets can be large, and a first-time visitor waits for all of it. Streaming compilation and compression help, and trimming unused code and assets helps more. When it runs cleanly, you ship the generated HTML, JavaScript, and wasm files, plus any separate asset packages, to your web host exactly as static files.

From there the game is a URL like any other web page, which is the entire payoff of the port: a native C or C++ game, recompiled rather than rewritten, running in the browser with no install. If you are starting fresh rather than porting, the Rust toolchain is worth comparing, and the broader C++ in the browser article covers the same ground from the angle of a complete small game rather than the toolchain mechanics.