Hit the Ground Running with WebAssembly đ
A tutorial on using WebAssembly with Emscripten and C/C++ (even if you donât know any C/C++)
JavaScript â which was famously designed in 10 days way back in 1995 â has until very recently remained the only language you could use natively on the web. It wasnât until 2017 that WebAssembly 1.0 was released, making it the second language supported by all major web browsers.
What is WebAssembly?
As the name implies, you can think of WebAssembly as an Assembly language for the Web (though weâll see later why itâs also useful outside the browser). As with other Assembly languages youâve heard about in CS101, no one actually writes code directly in WebAssembly â except Ben Smith đ. Instead, us mere mortals generally use it as a âcompilation targetâ, i.e. you take code written in other languages (like C, C++ and Rust), compile that code to WebAssembly, and run it in the browser.
Whatâs all the fuss about?
WebAssembly is a Big Deal⢠because, unlike other technologies (like the JVM), itâs a standard born out of a collaboration between all major browser vendors, and it was designed with the sandbox model of the web in mind. That is what makes WebAssembly so compelling.
Incidentally, this is also why WebAssembly can be very useful outside the browser, as weâll discuss later on.
Letâs write some WebAssembly!
As our âHello Worldâ program, weâll find some off-the-shelf C code for calculating the MD5 hash of a string, compile it to WebAssembly, and run it in the browser!
Step 1: Set up your environment
A popular tool used to compile code from C/C++ to WebAssembly is Emscripten. Itâs powerful because it provides a lot of very useful features to make our lives easier when porting code to WebAssembly. This includes wrappers for C/C++ compilers, auto-generated JavaScript boilerplate code for running our WebAssembly code, a virtual file-system (for code that needs to read/write files), and access to a host of pre-ported libraries (such as zlib or SDL).
Although installing Emscripten from scratch can take some time, I made a Docker image you can use to simplify this process:
# Fetch image containing Emscripten
$ docker pull robertaboukhalil/emsdk:1.38.30# Create a local folder for our WebAssembly adventures
$ mkdir ~/wasm
$ cd ~/wasm# Launch a container called "wasm" (in the background)
$ docker run -dt --name wasm \
--volume ~/wasm:/src \
-p 12345:80 \
robertaboukhalil/emsdk:1.38.30
The --volume
option allows us to run Emscripten inside a container and retrieve the generated .wasm
files directly from ~/wasm
(i.e. no need to transfer files from the container to the host!). Note that we also expose the containerâs port 80
as port 12345
, and Iâve set up the container image such that it will automagically launch a web server so you can directly browse the .html
files output by Emscripten, as weâll see below.
Phew, weâre now ready to use WebAssembly! đ
Step 2: Compile code to WebAssembly
For our MD5 hash example, weâll use this repo: https://github.com/pod32g/MD5.
Letâs go inside the container and fetch the code:
# Go inside the container we created above
$ docker exec -it wasm bash# Now we're inside the container!
$ git clone https://github.com/pod32g/MD5.git
Inside the MD5
folder, youâll see an md5.c
file:
$ ls
README.md md5.c
You can see the full code here, but hereâs pseudocode to illustrate how the code is structured:
# md5.cfunction md5(myString):
return some_complicated_math(myString)function main(argv):
# Expect user to provide a string as input:
# argv[0] = name of executable
# argv[1] = string to hash
if length(argv) < 2:
print("usage: %s 'string'\n", argv[0])
return 1 # Calculate and output the hash
hash = md5(argv[1])
print(hash)
Essentially, main()
fetches the input string provided by the user and calls the md5()
function on that string.
Setting WebAssembly aside for a second, if we wanted to compile md5.c
to good-old binary, we would do:
$ gcc -o md5 md5.c
Since Emscripten provides wrappers around tools like gcc
, compiling to WebAssembly is simply:
$ emcc -o md5.html md5.c
All we did was to use Emscriptenâs gcc
wrapper called emcc
(Emscripten also provides wrappers for g++
, make
, cmake
and configure
called em++
, emmake
, emcmake
and emconfigure
, respectively). We also ask Emscripten for a .html
output file instead of a binary.
Behind the scenes, Emscripten creates the compiled WebAssembly file md5.wasm
, but it will also generate md5.js
and md5.html
, which are the boilerplate code we need to automatically initialize our WebAssembly module.
Step 3: Running our code in the browser
If you followed the instructions above for creating the wasm
container, you should be able to launch http://localhost:12345/MD5/md5.html in your browser and see something like this:
To test our new tool, open the developerâs console and type the following to compute the md5 hash of the string test
:
> Module.callMain([ "test" ])
098f6bcd4621d373cade4e832627b4f6
Module.callMain()
is a utility function provided by Emscriptenâs glue code (md5.js
), which calls our C programâs main()
function with a list of arguments (in our case, just one). In the process, callMain()
also converts the string test
into an array of integers (since Wasm only understands ints and floats!).
Step 4: A simple application
Of course in real life, we donât ask the user to open the dev console, so how do we use WebAssembly in our apps?
Hereâs a simple app.html
file to illustrate the idea. When the page is loaded, it asks the user for a string, runs our WebAssembly code on it, and outputs the MD5 hash to the user. To do so, weâll extend the Module
object I mentioned above â the details are beyond the scope of this article, but the comments should give you a sense for how this works:
<!-- app.html --><script type="text/javascript">
var Module = {
// Don't run main() on page load
noInitialRun: true, // When Wasm module is ready, ask user for a string
onRuntimeInitialized: () => {
let myString = prompt("Enter a string:");
Module.callMain([myString]);
}, // Redirect stdout to alert()
print: txt => alert(`The MD5 hash is: ${txt}`)
};
</script>
<script src="md5.js"></script>
You can now launch http://localhost:12345/MD5/app.html and hash a string of interest:
And voilĂ ! In just a few steps, we ported an off-the-shelf utility from C to WebAssembly.
Granted, this was a simple example: we have a single C file with simple inputs, where we only expose the main()
function, require no file system support, no graphics of any kind, and no heavy computations. That said, hopefully this example gives you a glimpse of how code is compiled to WebAssembly.
â ď¸ Caution: Shameless plug aheadď¸ â ď¸
If youâre ready to dive into WebAssembly in a lot more depth (and how to port much more complex codebases like awk or Pacman), check out my book Level up with WebAssembly, a practical and approachable guide to using WebAssembly in your own web applications đ
Real-world use cases
Now that weâve seen our âHello MD5 Worldâ example, letâs explore real-world use cases for WebAssembly.
1) Use WebAssembly to surgically speed up your web applications
You can sometimes use WebAssembly to replace slow JavaScript computations and speed up your web applications. This is possible because WebAssembly is a typed language, features a linear memory structure, and is stored in a compact binary format which is faster to download and interpret than JavaScript.
If a library that performs the data analysis you need already exists, and is written in C/C++/Rust, WebAssembly is the clear choice. In such cases, itâs not worth spending the effort to port the code to JavaScript in the first place (and then proceed to validate and optimize it!).
Resources:
- Case Study: The app 1Password achieved order-of-magnitude speedups by using WebAssembly
- Case Study: How we sped up a web app by 20X by replacing slow JavaScript calculations with WebAssembly
- Blog Post: Discussion of the overhead of running WebAssembly code, and why we should be careful with âmicro-benchmarksâ
2) Port desktop apps and games to WebAssembly
Although you can sometimes get away with not having to make changes to a codebase to port it to WebAssembly (as was the case for our MD5 example), youâll almost always have to when it comes to games and other graphical applications. This is because, although most games are written as infinite loops that wait for user input (move the mouse, press a key, etc), infinite loops donât play nicely in the browser, where they block the main thread and can crash your browser tab. Luckily, Emscripten provides a slew of useful functions to get around this limitation, which you can use to essentially simulate infinite loops by calling the same function at a regular interval.
Resources:
- Case study: How AutoCAD used WebAssembly to port their 30-year-old codebase to the web without having to rewrite everything from scratch đ
- Demo: Play a clone of Doom3 in the browser, built with WebAssembly
- Tutorial: How to port a simple Asteroids game (written in C) to the web
3) Port command-line tools to the web
Beyond porting graphical applications, we can also use WebAssembly to port command-line tools to the web! Why would you ever do that? One reason would be to reuse an existing feature that is already efficiently implemented (e.g. if you wanted to show differences between two files, you could reuse the diffing algorithm included in the diff command line tool). Or you could build an interactive playground that allows users to test out (and learn about) the tool without first having to install it.
Resources:
- Code: SQLite running in the browser!
- Tutorial: How to build a playground for the jq command line tool
4) WebAssembly outside the browser
Despite the âWebâ in its name, WebAssemblyâs usefulness reaches well beyond the browser. In fact, you can use it as a JVM-like way to run a .wasm
binary on any platform, provided that platform supports a WebAssembly runtime, such as Wasmtime or Wasmer.
But if we want to productively run WebAssembly code outside the browser, how do we support features like files, threads, and sockets? The answer is that we need some sort of standard interface that WebAssembly can use to communicate to the outside world to request resources. To address that, there is an ongoing standardization effort around the WebAssembly System Interface (or WASI) that aims to make this possible.
Weâre still in the early days of running WebAssembly outside the browser (WASI was announced just a few months ago in March 2019), but as youâll see in the resources below, we live in exciting times!
Resources:
- Blog Post: Detailed introduction to WASI, how it works, and why itâs useful
- Demo: Run WebAssembly binaries on the command line using Wasmer
- Tutorial: Using Wasmerâs Go library to execute WebAssembly programs
5) Serverless WebAssembly
Another use case for using WebAssembly outside the browser is to use it as part of a serverless/function-as-a-service architecture. One benefit is that by supporting WebAssembly, cloud providers would be indirectly supporting a lot more languages, including C, C++, and Rust, which are not usually directly supported by serverless cloud providers. Moreover, the sandboxed nature of WebAssembly means it is more amenable to running multiple independent modules from different clients in parallel within the same process/container, which results in much faster function initialization times.
Resources:
Closing remarks
Although weâre still in the early stages, WebAssembly is already being used in real-world applications, whether to port entire CLIs/games/desktop apps to the web, applying it surgically to speed up front-end computations, or even using it outside the browser. Looking to the near future, WebAssembly will gain support for threads, SIMD instructions, garbage collection, and the ability to directly manipulate the DOM, just to name a few.
Of course, it wonât always make sense to use WebAssembly â shocking, I know â so you should evaluate whether the benefits it brings are worth the added complexity.
I hope this article has made WebAssembly feel a little less like magic đ§, and a little more like a powerful tool in your toolbox.
If you enjoyed reading this and want a practical guide to get started with WebAssembly, check out my book Level up with WebAssembly!