A beginner friendly and hands on introduction to WebAssembly


What is WebAssembly?


WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications.

Source



This means that you now can write code in a different language than JavaScript, most notably C, C++ and Rust to get a faster web-app. And thus blurring the line between web-development and desktop-applications.


Why do we want to use Wasm?


If we have parts of our code that is running slow (since JavaScript is not the most efficient language), we can now write those parts in a faster language. Games, Video editing and Machine learning are all examples that require heavy computing. Or maybe we just want to hide some Bitcoin/Dogecoin/Whetevercoin mining in our code and become rich while the users' computers do all the work.


How do we use Wasm?


Since we are not writing Wasm by hand, but rather compiling code into it, we first need a piece of code written in some other language.


In this article we will use C++ and compile it to Wasm using Emscripten. We will go into some of the more technical bits of using Emscripten in the examples below. But we will keep a hands-on approach, so we won't get into the nitty gritty details of Wasm and Emscripten. And when we have our Wasm, we will also need a webpage for our users to interact with. In Example 1 we have a basic React app and in Example 2 we have plain Html + CSS + JavaScript combo.


Example 1: Benchmark

Repository: https://github.com/eriktoger/react_wasm

Demo: https://cpp-benchmark.netlify.app/


The page lets us compare the speed of JavaScript and C++, by the press of a button (well technically we have to push two buttons):





The C++ code in this project is quite straightforward. One function (nrOfPrimes) that does some computation. But the more interesting part is the EMSCRIPTEN_BINDINGS.


int nrOfPrimes(int limit) {

  vector<int> primeNumbers(limit);
  iota(primeNumbers.begin(), primeNumbers.end(), 0);
  primeNumbers[0] = -1;
  primeNumbers[1] = -1;
  for (auto prime : primeNumbers) {
	if (prime == -1) {
  	  continue;
	}
	for (long int i = prime * prime; i < limit; i += prime) {
  	  if (i % prime == 0) {
    	primeNumbers[i] = -1;
  	  }
	}
  }

  return count_if(primeNumbers.begin(), primeNumbers.end(),
              	[](int i) { return i != -1; });
}

EMSCRIPTEN_BINDINGS(my_module) {
   emscripten::function("nrOfPrimes", &nrOfPrimes);
}


This is the magic glue that lets our JavaScript code be aware of nrOfPrimes. And you only need to bind the functions that you want to expose to the outside world. In Example 2 (which contains multiple .cpp and .h files) we will only bind a fraction of our functions.


The next step is to compile our C++ code, which is done with the command:


em++ --bind -O3 -s ENVIRONMENT='web' -s MODULARIZE=1 -s SINGLE_FILE=1 -s ALLOW_MEMORY_GROWTH=1 -o src/wasm/wasm.js CPP/wasm.cpp

Let's break it down bit by bit.

  • em++: Our C++ tool. There is also a C tool named emcc.

  • --bind: Binds the functions/objects that we declared in EMSCRIPTEN_BINDINGS so they can be called inside of our JavaScript code.

  • -O3: The level of optimization. Longer compile time but faster execution.

  • -s: Indicates that there is an option following.

  • ENVIRONMENT='web': Targets the code for the web, without it the code can also be run with Node.

  • MODULARIZE=1: Transforms our code into a JavaScript module so we have something to import.

  • SINGLE_FILE=1. Without it we will get one .js file and one .wasm file. But since we are bundling everything with webpack later on, we bundled the .js and .wasm right away.

  • ALLOW_MEMORY_GROWTH=1. Giving the C++ code access to more memory.

  • -o src/wasm/wasm.js. Where to output the result.

  • CPP/wasm.cpp. What C++ file to compile to Wasm.

So now that we have our wasm.js file we need to use it in our React application. And for that we use a small React hook:


import { useEffect, useState } from "react";
import WASM_MODULE from "./wasm/wasm.js";

const useWASM = () => {
  const [wasm, setWasm] = useState({});
  useEffect(() => {
	try {
  	  WASM_MODULE().then((module) => setWasm(module));
  	  console.log("WASM loaded!");
	} catch (e) {
  	  console.log(e);
  	  return {};
	}
  }, []);
  return wasm;
};

export default useWASM;

And then we use that hook to retrieve the C++/Wasm-function.

const { nrOfPrimes } = useWASM(); 

And call it as a regular JavaScript function.


const answer = nrOfPrimes(limit);

Which is a very elegant way to introduce a whole different language to your JavaScript. Just as easy as using any other library.



Example 2: Bouncing Circles

Repository: https://github.com/eriktoger/circles_wasm

Demo: https://circles-cpp.netlify.app/ A page where the user can add and remove bouncing circles.



Since it takes some more logic to get the circles to bounce than calculating primes, this time around the code is split into several .cpp and header files. But we are just going to inspect one of them, the one with EMPSCRIPTEN_BINDINGS:

EMSCRIPTEN_BINDINGS(my_module) {
  emscripten::class_<Circle>("Circle")
  	  .property("x", &Circle::getX)
  	  .property("y", &Circle::getY)
  	  .property("r", &Circle::getR)
  	  .property("cr", &Circle::getCR)
  	  .property("cg", &Circle::getCG)
  	  .property("cb", &Circle::getCB);
  emscripten::register_vector<Circle>("vector<Circle>");

  emscripten::function("increaseCircles", &increaseCircles);
  emscripten::function("decreaseCircles", &decreaseCircles);
  emscripten::function("changeColor", &changeColor);

  emscripten::function("getCircles", &getCircles);
  emscripten::function("init", &init);
}

In the above code we are exposing a couple of functions, but also a class and a vector. We need to expose Circle and vector<Circle> since getCircle is returning vector<Circle>. This way we will get an array in our JavaScript code which we can destruct just as a JavaScript object!

  const circles = Module.getCircles(canvas.width, canvas.height);

  context.clearRect(0, 0, canvas.width, canvas.height);
  for (let i = 0; i < circles.size(); i++) {
	const { x, y, r, cr, cg, cb } = circles.get(i);
	context.beginPath();
	context.arc(x, y, r, 0, 2 * Math.PI, false);
	context.fillStyle = `rgba(${cr},${cg},${cb},0.75`;
	context.fill();
  }

The next step is to compile our code into Wasm with:


em++ --bind lib/CPP/*.cpp -o public/CPP/canvas.js -s ALLOW_MEMORY_GROWTH=1

  • em++, --bind and ALLOW_MEMORY_GROWTH=1 have already been discussed in Example 1.

  • lib/CPP/*.cpp: We want to use all the cpp files in that folder.

  • -o public/CPP/canvas.js: Since we are not using SINGLE_FILE=1 this time, we will get canvas.js and canvas.wasm.


And then we need to apply our canvas.js to our html-page:



<!DOCTYPE html>
<html>
  <head>
	<meta charset="utf-8" />
	<title>Webassembly Demo</title>
	<link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div id="main">
  	  <div id="header">
   	    <div class="info">
      	  <h1>Webassembly Demo with C++</h1>
    	</div>
    	<div class="buttons">
      	  <button onClick="increaseCircles()" class="button">+</button>
      	  <button onClick="decreaesCircles()" class="button">-</button>
    	</div>
  	  </div>

  	  <canvas id="canvas"></canvas>

  	  <div id="footer">
        <div class="info">
          <h2>Footer</h2>
        </div>
  	  </div>
	</div>
	<script src="canvas.js"></script>
	<script src="circles.js"></script>
	<script>
  	  init();
	</script>
  </body>
</html>

This way we can access Module in circles.js (We saw Module.getCircles in the code above).


Because it is through Module that we can access the functions declared in EMSCRIPTEN_BINDING:


For example:


const increaseCircles = () => Module.increaseCircles();

const decreaesCircles = () => Module.decreaseCircles();


Conclusion

In both examples:

  • We first wrote some C++ and exposed our functions with EMSCRIPTEN_BINDINGS.

  • Compiled the C++ code with Emscripten.

  • Built a web app.

  • Accessed the Wasm in our JavaScript.

  • And finally executed our functions for a faster web app.


There is of course much more to learn about Wasm and Emscripten. But now we know enough to start coding! And that is my preferred way of learning, get a prototype up and running to tinker with while slowly digesting all the technical stuff.



Bonus

I'm currently working on a chess computer with a Svelte interface and C++ as the chess engine. Check it out if you want to take a look at a non-trivial Wasm/Emscripten project.

Repository: https://github.com/eriktoger/chess

Demo: https://chess-wasm.netlify.app/



By Erik Töger



0 kommentarer

Senaste inlägg

Visa alla