Running C++ in a Web Browser with WASM
I recently started looking into compiling and running C++ for the web as a way to give some old projects a much-needed makeover. In this post we’ll look at some basic examples of compiling C++ and running it in a web browser.
The Emscripten Build Environment
We’ll need the Emscripten SDK and accompanying tools to compile our code. The docs recommend installing them directly on our machine, but I’ve never been much of a fan of that approach. Fortunately, there’s an officially supported Docker image we can use instead, so we’ll prefix any commands with the following snippet:
docker run -v $(pwd):/src emscripten/emsdk:2.0.18 ...
At the time of writing, the latest version of Emscripten is 2.0.18
, so we’ll target that specifically.
Hello, WebAssembly
Let’s start with the most basic example possible:
#include <iostream>
int main() {
std::cout << "Hello, WebAssembly" << std::endl;
}
Invoke the compiler as follows:
docker run -v $(pwd):/src emscripten/emsdk:2.0.18 \
emcc hello.cpp -o hello.js
We expect this to generate two files:
hello.wasm
– a binary file to be executed in the browserhello.js
– a script to load and execute the.wasm
These are the kind of files you add to .gitignore
and build as part of CI/CD.
A simple <script>
tag will be enough to run this example in a browser:
<script src="hello.js"></script>
Emscripten’s default behaviour is to call main()
and relay any output on stdout
to the developer console, so if it
worked, you should see Hello, WebAssembly when the page loads.
The generated JavaScript loads the compiled binary with an XHR request, which means the example won't work by simply loading the HTML file in a browser – we need a local web server instead.
An easy way to run the example on your localhost is with the following command in
the directory containing index.html
:
docker run -v $(pwd):/usr/share/nginx/html -p 80:80 nginx
Beyond the Basics
By this point, we have all we need to run basic C++ in the browser 🎉, but in order to actually do anything useful we need to be able to interact with the compiled binary beyond the initial page load. Typically, we want to define a public interface of functions and/or classes for JavaScript to make use of.
Let’s take the following C++ and see how to call square()
in the browser:
extern "C" int square(int n) {
return n * n;
}
We’ll talk about extern "C"
later. This time we want to call a function other than main()
, so we need to be more
specific with the compiler:
docker run -v $(pwd):/src emscripten/emsdk:2.0.18 \
emcc square.cpp -o square.js \
-s "EXPORTED_FUNCTIONS=['_square']" \
-s "EXPORTED_RUNTIME_METHODS=['cwrap']"
We specify the name of our function (with a preceding underscore) to prevent the compiler from removing it as part of
its “dead code elimination” process. We also told Emscripten to export cwrap
, which is the JavaScript function we’ll
use to actually call square()
.
A typical setup in HTML could look like this:
<script>
var Module = {
onRuntimeInitialized: () => {
const square = Module.cwrap('square', 'number', ['number']);
console.log(`5 squared is ${square(5)}`); // 5 squared is 25
}
};
</script>
<script src="square.js"></script>
Here we set Module.onRuntimeInitialized
to a callback function that Emscripten will execute when the binary file is
ready. We then wrap our C++ function and store it in square
so we can call it later. The second and third arguments to
cwrap()
describe the function signature: the return and parameter types, respectively. Finally, we call the wrapped
function just like any other JavaScript function call.
Both the examples in this post are running on this page, too, so if you open the developer console you should see 5 squared is 25 and Hello, WebAssembly. If you don’t, then either I screwed up (entirely possible), or your browser doesn’t support WebAssembly yet (see caniuse.com/wasm). In any case, here’s an interactive example:
A note about “name mangling”
If you’re familiar with C/C++ then you might be wondering why we used extern "C"
above. This tells the compiler to use
C naming conventions for our function and avoids the name mangling
process C++ uses to support function overloading. Without this, we’d need to know ahead of time how the compiler will
translate the name of our function so we can specify it during compilation – kind of a Catch-22!
Conclusion
This post hardly scratches the surface of what’s possible with WebAssembly, but hopefully these examples are enough to get someone off the ground. In the future I’ll write about how I’m using WebAssembly to bring some old projects to the web.