Learn WebAssembly while building a Wasm-based QR decoder for the browser

Jacky Efendi
Jul 16 · 10 min read

WebAssembly (Wasm) has been around for about two years. It is still relatively new technology.

For some reason, something about it always feels scary to me. Maybe because it has Assembly in its name? Or maybe it’s having to code in a language that is very different to JavaScript?

Whatever it was, I am a curious person and after watching a talk by Surma, I convinced myself to try and learn WebAssembly.


What is WebAssembly

“[WebAssembly] provides a way to run code written in multiple languages on the web at near-native speed, with client apps running on the web that previously couldn’t have done so.” — MDN

Basically, WebAssembly allows us to compile code, written in languages such as C, Rust, etc. (A complete list of languages can be found here), in a .wasm file and then run it in the browser.

It is incredible in terms of opening up all kinds of possibilities for the web platform. If you have tried Unity to build games before, you might be familiar with this picture.

The Unity game: Tanks!

It is a sample 3D game, created with Unity, ported to the browser with the help of WebAssembly. The crazy thing is, it’s not sluggish to play. You can try it out yourself here.

Another is AutoCAD. AutoCAD was ported to the web also. You can watch this video to learn more about it.

WebAssembly isn’t magic. It won’t automatically take your existing program written in C++, compile it, and run it in the browser. What WebAssembly allows us to do is reuse code from other languages and run it on the browser at near-native speed!


Should I Learn WebAssembly

If your web app doesn’t include heavy computations and only does light tasks such as rendering the UI, making requests to APIs, etc., then you probably don’t need WebAssembly.

WebAssembly allows us, as web developers, to achieve things we previously couldn’t (or at least not feasibly) do with only JavaScript. It acts as a complement to JavaScript, instead of as a replacement for it.

That being said, it is always beneficial to learn it and try it out.


Wasm to Decode QR Code

In this article, we will use Wasm to decode QR code from an image input.

We will be using Rust and wasm-bindgen.

The final result can be seen in this repo:

Note, however, that I am by no means an expert in WebAssembly or Rust.

My experience in software engineering mostly consists of web development and writing JavaScript. This basically means that what I am doing in this article might not be good practice and some of it may not be technically accurate.

It is, however, a genuine learning journey of someone new to this environment. Hopefully, you will find this useful in some way.


Setting Up

First, we will need to install Rust. Follow this guide to do it.

Once installed, we will add wasm as a compilation target to the Rust compiler. We do this by running the following command:

rustup target add wasm32-unknown-unknown

Now, we should install the wasm-bindgen-cli because we will be using it later. We will install it using cargo. It should already be installed along with Rust.

Think of it as npm, but for Rust.

Run the following command to install wasm-bindgen-cli:

cargo install wasm-bindgen-cli

That’s all we need for now.

Let me give you an overview of what we are going to do.

  1. Write a function in Rust which can decode QR code.
  2. Compile the Rust code to Wasm.
  3. Try using the Wasm file in a simple HTML + JS webpage.

Writing Code in Rust

If you are familiar with Node, you’d usually start a project by running npm init.

In Rust, we use cargo new hello_world instead.

This will create a directory named hello_world with some files pre-made for us. In Rust, this project is called a package. We can import third-party crates as well, just as, in Node, we can import third-party modules.

Now, let’s take a look at the Cargo.toml file. Yours probably looks a bit empty right now, but that’s okay. Just modify it to follow the following snippet:

Filled Cargo.toml file

You might see that this file contains the information of this package and its dependencies. This file is much like a package.json file, it is the manifest of the package.

The important thing here is that in the [lib] section, we are defining the crate-type as ["cdylib"]. This is required when we are targeting Wasm.

Also, we are not going to write a QR decoder ourselves, so we will be using a third-party crate. We will use rqrr as the QR decoder.

To create an image that will be fed to the decoder, we will use the image crate, and we will also use wasm-bindgen to help provide Rust-to-JavaScript bindings.

Now, rename src/main.rs to src/lib.rs and write the following code in it. lib.rs is the entry point of our package when we compile it to Wasm later.

I am not very familiar with Rust, but I will try my best to explain.

The extern crate and use statements are used to import the crates we will use, in this case, they are wasm_bindgen, rqrr and image.

Then, we create a public function using the pub fn keyword, named decode_qr. This function accepts an array of unsigned 8-bit integer named bytes (bytes:&[u8]), representing the image data.

It will decode the image and return a String. The #[wasm_bindgen] attribute tells wasm_bindgen that we want this function to be exposed to our JavaScript when we use it. This information is used by wasm-bindgen to create appropriate bindings for us.

#[wasm_bindgen]
pub fn decode_qr(bytes: &[u8]) -> String {

We then create an image from this array, using the load_from_memory method provided by image crate.

As this operation can fail, we use the match keyword and handle cases when the method returns Ok and Err results.

On error, we will just return the string “[Error] Failed when trying to load image” to the JavaScript side.

let img = match image::load_from_memory(&bytes) {
Ok(v) => v,
Err(_e) => return format!("{}", "[Error] Failed when trying to load image"),
};

Then, we convert this image to a grayscale image before feeding it to rqrr.

let img = img.to_luma();

You might be thinking: “Where do all these method names come from?”

These methods, along with other information, can be found in the crate documentation pages.

I have linked to the documentation pages above, but I will provide it again in case you missed it. rqrr docs, image docs, and wasm_bindgen docs.

Now, we prepare the image and then feed it to rqrr, along with the case handling.

Finally, we return the String to the JavaScript side.


Compiling to Wasm

Now that we have our Rust code, we can easily compile it to Wasm using the following command:

cargo build --target wasm32-unknown-unknown --release

This will generate a Wasm file in target/wasm32-unknown-unknown/release.

We generated our first .wasm file! 🎉

Note that the name of the file is qr_rust.wasm. This is because the name of my package is qr-rust, so the output file is named accordingly.

We are not done, however. As we are using wasm-bindgen, we need to run wasm-bindgen against this Wasm file to generate another Wasm file. This will contain a JavaScript file with the bindings needed to help us use the Wasm file easily.

Run the following command to do this:

wasm-bindgen target/wasm32-unknown-unknown/release/qr_rust.wasm --out-dir ./dist --no-modules --no-typescript

You should now see two new files in the dist directory.

The output files after using wasm-bindgen cli to create bindings

If you try to look inside the .js file, you will see a bunch of code generated by wasm-bindgen so we can easily use the Wasm module.

Two notable things it does for us:

  1. It helps us instantiate the Wasm module in the init() function:
Initializing the wasm module

2. It provides the necessary things needed to pass data between JavaScript and Wasm:

If we are not using wasm-bindgen, we would have to write these things ourselves.


Trying It Out

First, let’s create a simple HTML file that includes the JavaScript bindings generated by wasm-bindgen.

The bindings have to be executed before we can do anything with the Wasm module.

Let’s create this index.html file in the ./dist directory.

<html>
<!-- the javascript bindings -->
<script src="qr_rust.js"></script>
</html>

The bindings create a wasm_bindgen variable in the global scope, which we can use to load our Wasm module.

Let’s try serving this HTML file locally and see what happens.

The easiest way to do this would be to use http-server npm module and serve our ./dist directory.

npm install http-server -g
http-server ./dist -g

Open the URL. If everything is correct, you should see It is loaded! in the browser console.

We loaded our Wasm module on the browser!

The function that we wrote in Rust can be accessed from the global wasm_bindgen variable.

const { decode_qr } = wasm_bindgen;

At this point, we can just pass an array of unsigned 8-bit integers to the function and log the output.

Here, I am passing new Uint8Array([1,2,3,4,5]); to the function.

It failed!

Apparently, it fails trying to load the image. This is to be expected as we are just passing a random array which doesn’t represent image data.

Let’s create a <video /> element to get the image data from.

If you refresh the browser, your browser should be asking permission to use the camera now. Allow it, and you should see your camera feed in the browser.

Me, holding a QR code image in front of the webcam

Next, we want to be able to get the current frame of the video, get the image data as an array of unsigned 8-bit integers, and send it to the decode_qr() function.

We will do this with the help of canvas and FileReader. Here is a captureImage function that will do just that.

Now, we just need to periodically call this function. We can simply do this with setInterval. We will start the interval after the video stream is created.

navigator.mediaDevices.getUserMedia({ video: true })
.then(stream => {
video.srcObject = stream;
setInterval(captureImage, 300);
});

So, our final HTML file should look like this:

Refresh your browser, time to try it out!

It works! The QR code says “http://en.m.wikipedia.org”

Sometimes, you might see an error saying: “unreachable code”.

I haven’t figured out why that happens yet. If you do know the cause, please let me know!


What You Can Try Next

We have taken a functioning QR decoder, written in Rust, compiled it to WebAssembly, and used it in the browser with the help of wasm-bindgen.

However, if you look at the network requests in DevTools, you’ll see that the size of this Wasm file is huge!

736kb for a QR decoder!?

Note that we haven’t done any compression or optimization.

You can try looking up wasm-opt to optimize the size. This guide will help you with that.

Then, you can compress it down with any compression algorithm you want; a common one is gzip. See how small you can get the Wasm file size. I am currently at 264 KB gzipped.

Next, you can try publishing your creation as an npm module so other people can easily use it. I have done this myself.

If you want to take a look at my implementation, you can check out my repository below. It contains mostly the same code included in this piece.


Conclusion

So, is WebAssembly scary? For me, the answer is no.

The reason why it seemed so scary at first was that it was a mysterious piece of technology I was unfamiliar with. As someone who mostly codes in JavaScript, it was odd to code in Rust.

After reading and watching information about WebAssembly, I could no longer ignore it and decided I had to get over my fear of WebAssembly. A good way to get over the fear is to actually dive into it.

Note that you don’t even have to create your own Wasm! There is an increasing number of Wasm modules, created by other people, published to npm. We can just consume these modules in our projects.

The purpose of the exercise is that we got to know WebAssembly better. In practice, we probably wouldn’t have to do that, but if you needed to use it one day, at least you’ll have an overview of how it works already.

WebAssembly opens up a lot of possibilities for the web platform, especially for tasks that were too heavy to do with just JavaScript.

Your use cases might not need WebAssembly. For example, our QR decoder (after some optimizations) is 264 KB. There’s this QR scanner, written in JavaScript, which is only ~12.4 KB gzipped.

Depending on your use case, you could argue that, even with the performance advantage, the WebAssembly solution is overkill for this purpose and you could be right.

The point is that tools are just tools and WebAssembly is a great addition to our toolbox.


Resources

Better Programming

Advice for programmers.

Jacky Efendi

Written by

Software Engineer @ Tokopedia - opinions I write are my own and not necessarily the views of my employer

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade