Starting out with the Webassembly and Emscripten has been an interesting experience. There are some quirks to get around a somewhat fragmented support community that I have found so far.
But I am really excited by this technology. In many aspects, it's a return to where I started, assembly coding on 8-bit and 16-bit machines. And it's a shot in the arm for the browser, opening up many new possibilities for frontend web development.
I never got excited about the node/npm/babel/webpack world of library bloat that is the realm of today's frontend development. But it pays the bills well enough.
We're heading into exciting times again.
So I want to take you through my easy way to set up a small project using WebAssembly and Emscripten, but hand coding everything and making everything as simple and understandable as possible.
Before we kick off with this demo, I'll give a very simplified explanation of what this technology is all about.
It starts with a program from another language, in our case, C/C++. Imagine if you could execute that code from the browser?
You can take a C/C++ program, and compile the code using Emscripten into a WASM file.
Javascript can load the WASM file and take the functions that are in it, and assign them to Javascript functions. Therefore if you called the particular Javascript function, it would then call the function from your C/C++ program.
And your HTML file would simply load your Javascript file in as per normal to start the entire process.
If you can follow that OK, let's try it out.
Another wonderful thing about this new technology is that it ships with all the latest browsers, and you don't have to bother with bloated frontend libraries to start a project. All you need is:
That's it! No babel, no Webpack!
The easiest guide to setting up Emscripten is here.
Let's get started.
You don't even need a C/C++ compiler installed on your system. Emscripten will compile your code for you. Start with a really simple program that only contains a single function, in our demo, that gives the calculation for ohms law:
float resistance(float voltage, float current){ if (current > 0){ return voltage/current; } return 0; }
Save this as demo.c
.
That's our total program. Next is to compile it into a WASM file. This is the following line I use to compile the demo.c program:
emcc demo.c -O3 -s WASM=1 -s "EXPORTED_FUNCTIONS=['_resistance']" -o demo.wasm
This will output a compiled WASM called demo.wasm. I have specified the compiler flag of -O3
as the optimisation to use to make the file very small. But I didn't do this to save space, I did it because this is appears to be required by WebAssembly. Every other optimisation flag, or no optimisation appears to result in an unusable WASM file.
I am still trying to find out why.
“EXPORTED_FUNCTIONS=['_resistance']”
ensures that the resistance
function is compiled and not optimised away.
You'll need to browse this via an HTML file of course. This will load the Javascript file that, in turn, will load your WASM and execute your C/C++ function.
Nothing ground breaking, just a simple one:
<!doctype html> <html> <head> <title>WebAssembly Demo</title> <meta charset="utf-8"> </head> <body> <h1>WebAssembly Demo</h1> <script src="demo.js"></script> </body> </html>
I've specified a meta tag with a utf-8
charset only to silence any complaints from Firefox or any other browser. Nice to have a clean slate and no warnings.
The script tag contains a source link to demo.js
which doesn't exist yet. So we'll create that now.
let resistance; function loadWasm(filename){ return fetch(filename) .then(response => response.arrayBuffer()) .then(bits => WebAssembly.instantiate(bits)) .then( results => results.instance ); }; loadWasm('demo.wasm') .then(instance => { resistance = instance.exports._resistance; })
We'll work through this line by line.
let resistance;
This is a variable definition using es6 syntax. Modern browsers now all ship with es6 features like let
, Promises
, and fetch
. No Babel required, hoorah!
function loadWasm(filename){ return fetch(filename)
We define a function called loadWasm. You could call this anything you like of course. Next, we network fetch whatever filename is passed. Hint: (it will be demo.wasm later) :)
.then(response => response.arrayBuffer())
Fetch will return a promise. When resolving the promise with a then()
, you get the response from fetch and return the array buffer of bytes from the WASM.
.then(bits => WebAssembly.instantiate(bits))
The array buffer is contained inside a promise, and when resolved with then()
the actual data from the array buffer is available. This is passed to the WebAssembly.instantiate()
function to be turned into a module
and an instance
.
The instance
will contain our resistance function from the C/C++ program.
.then( results => results.instance );
The instance
is returned inside a promise and is resolved with then()
and the instance property is returned.
This is a lot of passing down and converting to get to the final instance. I don't understand the necessities and advantages of going through each of these stages as yet. Let's just be comforted with the fact that you arrive at a final instance containing your function, which is the objective of this demo. ;)
loadWasm('demo.wasm') .then(instance => { resistance = instance.exports._resistance; })
Next, we make use the loadWasm
function and pass in the demo.wasm
filename. Our instance is returned as a promise and once resolved with then()
we assign the _resistance
function in the WASM (inside the instance.exports
properties) to the Javascript function variable called resistance
. Remember let resistance;
?
Start a webserver using Emscripten with:
emrun --port 8080 demo.html
Your browser will start and will load the demo.html
page.
The title of the page will be loaded.
Open the developer tools. The response for the html, js and wasm files should be a 200.
If you go to the console, you can try out your new function:
resistance(9, 20) 0.44999998807907104
Success! You have successfully called a C/C++ function using WebAssembly in a browser.