Motivation
QR codes normally contain URLs which are opened in a web browser, but there’s nothing stopping you from putting something else in there. For reasons I do not fully understand, I’ve decided to embed WebAssembly in a QR code.
This QR code contains 283 bytes of WebAssembly, but more on that later. You can try scanning it with your phone, but it’s extremely unlikely anything will happen – it’s just a chunk of binary data and does not identify as WASM, so your phone has no idea what it is.
What is WebAssembly?
WebAssembly, abbreviated as WASM, is a compact binary executable format that can run at near-native speeds in web browsers and elsewhere. It’s often used to port legacy C/C++ (and Rust, which I still haven’t tried!) code and run it on the web. There are a lot of awesome WebAssembly applications out there, with Figma being perhaps the most well-known.
Apart from its compact size, another useful property of WASM is sandboxing: anything the code operates on, including memory, has to be explicitly provided from “outside”, e.g. the web page that loads the WASM binary, making it secure by design.
Let’s create something with WebAssembly!
Creating a WASM Module with Emscripten
While you can write WebAssembly from scratch, using the WebAssembly Text Format (WAT), but you’re much more likely to writing in C and compiling it to WASM.
Chances are you’ll stumble upon Emscripten, which is “a complete compiler toolchain to WebAssembly, using LLVM, with a special focus on speed, size, and the Web platform”. In fact, the tutorial on MDN assumes you use Emscripten, which makes a lot of sense.
Let’s start with the traditional “Hello, world” example:
#include <stdio.h>
int main() {
printf("Hello, world\n");
return 0;
}
Compile it to WASM using the emcc
compiler frontend:
$ emcc -o hello.html hello.c
Which results in the following output files being generated:
-rw-r--r--@ 1 ahs staff 65 Feb 5 13:20 hello.c
-rw-r--r--@ 1 ahs staff 22064 Feb 5 13:21 hello.html
-rw-r--r-- 1 ahs staff 57929 Feb 5 13:21 hello.js
-rwxr-xr-x 1 ahs staff 12066 Feb 5 13:21 hello.wasm
😳 That’s more than we expected! The WASM weighs in at a respectable 12 KB, accompanied by 58 KB of JS glue code and 22 KB of HTML to demo it (which we can safely ignore), screenshot below.
So what happened to compact? Part of the value proposition of Emscripten is that you can use it to compile your existing
C/C++ code and run it on the web. That code will probably include things like printf()
, malloc()
or strlen()
, commonly
known as the C standard library.
Your macOS or Windows install ships with (probably
several copies) of the standard library, but your WebAssembly runtime does not. When you compile your code to
WASM using Emscripten, a lot of behind-the-scenes magic happens to make things “just work”. For instance, it will map
that printf()
call in the example to a console.log()
invocation, something that exists in the JS runtime.
A QR Code can hold up to 3 KB of data, and it would be a huge code, making it impractical to scan. Clearly, if we want our WASM to fit in a QR Code, we must slim down.
Creating a Barebones WebAssembly Module
Computation-Only, No Standard Library
First of all, we will make our module computation-only and not attempt any I/O or other syscalls. We will not depend on the standard library.
At this point, most WebAssembly tutorials will use a factorial(n)
or sum(x, y)
computation as an example.
But that is rather dull, so instead, our module will draw something.
The module will receive Memory from JavaScript,
and provide a single function which will write RGBA pixel values to it. Those pixel values can
then be conveniently drawn to a Canvas via JavaScript.
To make it a bit more interesting, we will also use a tiny random number generator to create a different image every
time the function is invoked. Without further ado, here’s the complete code of demo.c
:
#include <stdint.h> // for uintX_t
// the address of this symbol denotes the start of the WebAssembly.Memory
extern uint8_t memory;
// do not strip functions (-fvisibility=hidden) with this attribute
#define WASM_EXPORT __attribute__((visibility("default")))
// a compact RNG, see:
// https://en.wikipedia.org/wiki/Lehmer_random_number_generator#Sample_C99_code
static uint32_t _rand() {
static uint32_t _rng_state = 42;
return _rng_state = (uint64_t)_rng_state * 48271 % 0x7fffffff;
}
// generate some random noise
WASM_EXPORT void go(int width, int height) {
uint8_t* im = (uint8_t *)&memory;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int offset = (y * width + x) * 4; // row-major order, 4 channels (RGBA)
im[offset] = _rand() % 255; // random R
im[offset + 1] = _rand() % 255; // random G
im[offset + 2] = _rand() % 255; // random B
im[offset + 3] = 255; // opaque alpha
}
}
}
And here is the output of calling it repeatedly, in all its glory. Yes, if you’re seeing a box of random noise below this text, then that’s the WASM running and its output drawn to a canvas. It’s not a GIF or a video.
The sky above the port was the color of television, tuned to a dead channel.
(William Gibson, Neuromancer)
Compiling the Module
Building the module without Emscripten is a bit more involved. Emscripten is an amazing project but for this experiment it’s like using a Sledgehammer to crack a nut.
Instead, we can directly invoke Clang/LLVM, which is used under the hood by Emscripten. If you’re reading this, chances
are that you already have clang
installed on your system. On my macOS/Homebrew install, I had to manually install
the WASM linker backend using brew install wasm-component-ld
to get it to work, YMMV.
Here’s how to compile the module:
clang \
--target=wasm32 \
-fvisibility=hidden \
-nostdlib \
-O3 \
-Wl,--no-entry \
-Wl,--export-dynamic \
-Wl,--strip-all \
-Wl,--import-memory \
-o demo.wasm \
demo.c
🤯 What do all these flags do? The interesting ones are:
-nostdlib
excludes the standard library during compilation and linking.-O3
aggressively optimize the generated code. There is also-Os
which should optimize for size, but it created a slightly larger binary.--strip-all
combined with-fvisibility=hidden
removes all symbols by default. This means anything the WASM module should export needs to declared explicitly. If you don’t provide this, the WASM will contain extra symbols, some of which I don’t fully understand.--import-memory
indicates that the WASM memory will be supplied from JavaScript.--no-entry
is a linker flag telling the linker that there is no entrypoint (we are building a library), i.e. nomain
.
So, where does that leave us?
$ ls -l *.wasm
-rwxr-xr-x@ 1 ahs staff 283 Feb 6 10:29 demo.wasm
🤩 Much better, only 283 bytes! Encoded in base 64, we can even conveniently show it here.
AGFzbQEAAAABBgFgAn9/AAIPAQNlbnYGbWVtb3J5AgACAwIBAAYIAX8BQZCIBAsHBgECZ28AAArUAQHRAQIGfwF+AkAgAUEBSA0AIABBAUgNACAAQQJ0IQJBACEDQQAoAoCIgIAAIQRBgICAgAAhBQNAIAUhBiAAIQcDQCAGQQNqQf8BOgAAIAYgBK1Cj/kCfkL/////B4IiCKdB/wFwOgAAIAZBAWogCEKP+QJ+Qv////8HgiIIp0H/AXA6AAAgBkECaiAIQo/5An5C/////weCpyIEQf8BcDoAACAGQQRqIQYgB0F/aiIHDQALIAUgAmohBSADQQFqIgMgAUcNAAtBACAENgKAiICAAAsLCwsBAEGACAsEKgAAAA==
Inspecting the WASM Output
Using the wasm2wat tool from the WebAssembly Binary Toolkit (WABT), we can do a quick visual inspection of the contents of the WASM file and see if there’s anything that looks redundant.
$ wasm2wat demo.wasm
(module
(type (;0;) (func (param i32 i32)))
(import "env" "memory" (memory (;0;) 2))
(func (;0;) (type 0) (param i32 i32)
(local i32 i32 i32 i32 i32 i32 i64)
block ;; label = @1
local.get 1
i32.const 1
[...]
local.get 4
i32.store offset=1024
end)
(global (;0;) (mut i32) (i32.const 66576))
(export "go" (func 0))
(data (;0;) (i32.const 1024) "*\00\00\00"))
🫡 LGTM. There’s a single export (our function), an import for the memory, a data variable (the RNG state) and a global, which is probably the symbol holding the memory offset (?).
Embedding the WASM in a QR Code
To generate the QR Code containing our newly minted WASM, I’ve chosen to use the popular Zint
barcode generator (brew install zint
). Unfortunately the online HTML manual is
an unreadable ad-ridden mess (why oh why…), but luckily there’s also a text version on Github.
Let’s generate the QR Code from our .wasm file:
$ zint \
--barcode=58 \
--secure=1 \
--binary \
--quietzones \
--scale 2 \
--input=demo.wasm \
--output=qrcode.png
The relevant options are:
--barcode=58
sets QR Code as output symbology.--secure=1
sets the lowest level of error correction (ECC level L), keeping the QR Code as small as possible.--binary
indicates to the encoder that the data is non-textual and may contain nasty things like null bytes or non-printable characters.--quietzones
and--scale 2
control the visual appearance, adding a bit of border area (the quiet zone, mandated by the ISO specs) and slightly increasing the output resolution.
This results in a nice 61x61 QR Code, meaning the square grid has 61 elements in horizontal and vertical direction, making it a version 11 code.
QR Codes have a version, which specifies their size. They also have a model, which is actually much closer to what 99% of people would think of when they hear “version”. There is only one model (Model 2, Model 1 was deprecated years ago) in use today. It’s all very confusing.
Putting it All Together
Since you can scan QR Codes in the browser, we can even test it here. Hint: it works best if you open this article on a smartphone and scan the QR Code from a screen or printed out on paper.
If you’re interested in the plumbing behind this, check out the code on GitHub.
Wrapping Up
What’s the purpose of this little experiment, couldn’t we just have put the URL of the WASM in the QR Code? Then we could have shortened the URL and made the WASM updatable? Sure, those are valid points, but personally I find it fascinating that we can use the QR Code itself to transmit a (tiny) executable program.
I’ve tinkered with these kinds of “side-channel use cases” in the past when I tunneled an internet connection through NFC, and there might be legitimate use cases for something like this when you’re unable to use an internet connection but have some other means to communicate.
And above all, sometimes we should just do things for fun. 😀