The TL;DR: I’ve been interested in Web Assembly for a while and finally sat down to do a little proof of concept to see how far I could get. I’m happy and encouraged with the result.
You might find this useful for your own research.
If you just want to get to the technical bits, skip this section.
Seriously. Just go read it.
I’m a self-taught C programmer who learned from the first edition of Kernighan and Ritchie’s seminal book (first edition, no less). I bought and used Borland’s C++ compiler waaay back, like 20 years ago. I’m very far away from those roots these days :(. Don’t get me wrong — I love what I do, but there’s something really fun and thrilling about working so directly with memory and being close to the hardware like that.
So, that funny article got me thinking about those old fun days and I decided to see how viable it would be to write C/C++ code and deploy it as part of a web application.
I got started by looking up “web assembly tutorial” and came across this article: https://tutorialzine.com/2017/06/getting-started-with-web-assembly
I did all of this work on a windows 10 laptop.
This is what happens at a high level:
- You generate some Wasm code. Wasm is a binary format that some browsers understand. As I write this, Chrome can load and work with WASM. Others can too, but I only worked with Chrome.
- You can generate Wasm binaries various ways. I chose to use Emscripten. I think there are other options.
- Emscripten will take your C or C++ code, compile it and generate the Wasm binary for you. I’m sure it won’t take just any C/C++ code. I have no idea what those boundaries might be.
Step 1: Installing Emscripten
I found this to be quite easy and not very long. Go here: https://kripken.github.io/emscripten-site/docs/getting_started/downloads.html.
Use the web installer.
Wait a while and you’re done.
Step 2: Write some C code
I used the TutorialZine code as a basis for my testing and added a bit:
As you can see, there’s a regular C main() function on line 8.
There are three additional functions:
- roll_dice: This is from the TutorialZine article. It generates a random number from 1 to 6. The JS UI calls into this when the user clicks on a CSS-rendered die and re-draws itself based on the result.
- I have a function, sayHi(). This one does a couple of things:
- At line 20, it invokes the function, _sayXyzzy(). _sayXyzzy is actually another function in the C program that just logs out to the console, “xyzzy” (again, via emscripten_run_script — see line 24 in the code).
It’s not doing much, but it’s good enough for this baby-steps POC.
Compile the C code
I started off by doing this via the Emscripten command line (part of the web installer) and it’s pretty simple:
The first time I ran the command, it did a bunch of initialization and there were some scary warnings. However, it all worked out. I think it’s actually compiling a bunch of standard C libraries, possibly downloading them first. Subsequent runs look like the screenshot above.
You should read the full Emscripten docs for a detailed run-down on the options. That command is compiling a single C file, dice-roll.c. It generates a WASM file (WASM=1). It does very little optimization (-O1 — note, it’s O for Optimize, no 0 for Zero). It also generates that glue JS file and names it “index.js”.
I switched over to using VS Code soon after that. VS Code helpfully told me there was a plugin for viewing C/C++ files and I installed that. I then created a task for the above. I ended up creating a .bat file with the actual command. My tasks.json looks like this:
Making It Work in the Browser
It’s pretty simple to get this to work in Chrome. Just create a barebones .html file (like index.html) and include that index.js file the emcc command generated in the prior step.
That index.js file is … something :). In my env, it’s 6,111 lines long :). It’s not *super* well documented, but was enough to help me figure out. This is a good place to start: https://kripken.github.io/emscripten-site/docs/api_reference/module.html#module.
Among 1,000 or so other things, it loads the Wasm file and creates a globally available object, Module. In fact, it supplements Module if you already created it. I think you’d always want to do that so that you can influence the initialization life cycle.
This index.js plus Module object allowed me to do several important things:
- Load the Wasm in the first place
- Add a function to the Module’s “postRun” array. The index.js initialization code runs these postRun functions once the Wasm is loaded and initialized.
- Provides a useful wrapper that lets me invoke the C code functions.
Here’s my HTML:
A lot of this is straight from that TutorialZine article. I’ve modified by adding a reference to “prerun.js”.
Emscripten created that index.js file (line 18) and that’s the 6100 line glue monster.
The rest of this is from TutorialZine. It references some CSS that does a good job showing a die. It adds an event listener on click, dispatching an anonymous function that call out to the C code at line 31:
var result = Module._roll_dice();
Line 21 actually throws a runtime error. That _sayHi() JS function, which is a proxy to the sayHi() function in C doesn’t exist yet. The index.js file does its work asynchronously. It is eventually available and by the time I get around to clicking on the die, it does. Line 33 never fails but line 21 always fails.
I obviously want to know when the C code is ready to use. This is where the “prerun.js” comes into play.
Here’s the prerun.js that gets loaded from line 17:
A line 1, I define an empty Module object.
I then define a couple of functions (sayAbc(), sayXyz(), sayInit()).
When the index.js glue code executes, it will supplement the Module object if it already exists or create a new one. In this case, I’ve already created it, so it supplements it.
In fact, it does more than supplement it. I can influence the Module life cycle by providing some functions for it to execute at certain times, like preRun, onInit, postRun and maybe more. The online docs led the way here and postRun was where I was able to reliable start running my C code.
You can see that I have a function, notifyReady() at line 18. It just creates an Event and publishes it onto the window object.
At line 25, I have an event listener. When it picks up the event, it logs out a happy message and then invokes Module._sayHi().
I added two functions to the postRun array on line 23.
When you spell it all out in detail like this, it might feel a bit overwhelming, but it’s actually not that complicated.
- Install Emscripten.
- Write some C code.
- Have some kind of build process to emcc your C code. There are a million ways to do this, of course.
- Use the generated JS “glue” code to load and process your Wasm.
- Take advantage of the lifecycle array hooks to safely execute your C functions.
Once I got the basic tooling done, it was a piece of cake. Edit my C file, press control-shift-B. Wait for that and then press F5 on the browser.
(Quick note on the browser — you need to use a real web server since the Wasm files are fetch()’d / XHR’d. Chrome won’t let you can’t fetch a file from the file system via the file:// protocol, so you need a real web server to do this. I use Fenix for these kinds of things — it’s ridiculously easy to work with.).
And that’s it! It feels like a pretty viable thing to try at this point. I am not entirely sure where to go next. I think I might try implementing a fancy sorting algorithm and do some compare/contrast with that and plain JS.
I recently published a book on TypeScript! It’s free and you can access it here: https://www.gitbook.com/book/pagalvin/yet-another-typescript-book/details