How To Use Emscripten & WASM in Web Apps
A year ago, I published a post about how I built WASM SYNTH. As I learned a lot during the process, the post ended up becoming huge. I even ended up putting a whole section in the post's appendix.
Then yesterday, a friend of mine asked me about my opinion on emscripten, the tool I had used to build WASM SYNTH. Coincidentally, I ended up referring him to the WASM SYNTH post's appendix, where I had described my build pipeline in detail.
Today, it made me realize that I could publish that appendix as an individual post. And so that what I ended up doing. In the following sections, you'll learn about the build process that I'm using in the WASM SYNTH project.
"How do I integrate WebAssembly into my regular web app build without bloating it too much?" I wondered when I first started working with WebAssembly. To put things into perspective: I had just recently switched from Webpack to rollup.js as I was sick of spending hours setting up Webpack with its ten thousand magic configuration options.
To my surprise, however, neither framework has reasonable defaults for handling WebAssembly yet. Notably, when we generated the WebAssembly outputs using emscripten and they grew bigger than 4KB. But there were other problems too:
- rollup.js bundles all source file into a single JavaScript file.
- Booting the app, also means booting react.js, which itself starts a continuous process that we'll somehow have to take into account.
- emscripten generates a
.wasm
and a.js
file. The.wasm
file contains all C++ source code as well as WebAssembly instructions. The.js
file acts as the glue between WebAssembly and JavaScript.- In some cases, there's this 4KB size limit on loading
.wasm
files - The emscripten
.js
glue code is super useful. I can recommend including it into your code deliverable.
- In some cases, there's this 4KB size limit on loading
- The
AudioWorklet
is supposed to be included into the page by usingnew AudioContext().addModule(<path>)
- It has a separate global scope. But emscripten's glue code relies on the global scope too (
Module
object)
- It has a separate global scope. But emscripten's glue code relies on the global scope too (
- Rollup.js doesn't have good defaults or plugins for bundling emscripten-generated
.wasm
code.
In the end, this led me to build a custom pipeline to bundle emscripten builds every time my source changed. But before I explain how, let's take a step back to understand how emscripten's compilation works.
On installation, emscripten exports a command-line utility called emcc
into the shell. To demonstrate its capabilities, let's try compiling the Hello World
example from the beginning of the post:
1 |
|
To make it executable by a browser, we'll use emcc
to compile it to a .wasm
and a .js
file.
1 | $ emcc main.cc --bind -WASM=1 -o main.js |
emcc
here simply compiles all functions contained in main.cc
into a main.wasm
file. Besides, it also generates this glue code (main.js
) using Embind. It allows us to invoke functions, initially written in C++, directly from within JavaScript as if they were just regular classes. So to make emscripten's outputs work correctly, we'll have to include the .js
into the page. On the initialization of the page, emscripten's clue code is loading the .wasm
file asynchronously. Through some trick, the browser skips the 4KB file size limit, and all our C++ classes are now available as native JS classes:
1 | <html> |
So far, so good. But, as I've mentioned previously, one of the problems now is that the AudioWorklet
, being spawned in a separate thread with a separate global scope, won't be aware of emscripten's window.Module
. So how do we export code from within a Worklet?
After spending quite some time researching that question, I discovered that the Google Chrome Labs team recommends transpiling all JavaScript source into native modules and then simply use ES6's import from
syntax. Well, it turns out the JS modules syntax isn't widely supported yet, particularly not in Workers
, let alone Worklets
...
But luckily, after banging my head another few days against JS modules and other fancy new front end frameworks, I found a surprisingly simple trick.
I reduce emscripten's outputs to a single .js
file that contains the .wasm
as base64 string using it's -s SINGLE_FILE=1
option. As we need emscripten's .js
file to define window.Module
in the AudioWorklet
's global scope, I then simply append the emscripten JavaScript file with the AudioWorklet
' JavaScript file to make them configure the same global scope. That allows me to execute WebAssembly from within a Worklet without using the poorly supported import from
syntax. Ultimately, I ended up with the following bash script (or on GitHub):
1 |
|
To update the page every time the source changed, I additionally used npm-watch
to re-run the routine each time file changes occur.
To find the latest version of this build process, check github.com/TimDaub/wasm-synth. Stay up to date by following my newsletter.