Making really tiny WebAssembly graphics demos

I've been studying WebAssembly recently, which has included porting some of my m4vga graphics demos. I started with the Rust and WebAssembly Tutorial, which has you use fancy tools like wasm-pack, wasm-bindgen, webpack, and npm to produce a Rust-powered webpage.

And that's great! But I want to know how things actually work, and those tools put a lot of code between me and the machine.

In this post, I'll show how to create a simple web graphics demo using none of those tools — just hand-written Rust, JavaScript, and HTML. There will be no libraries between our code and the platform. It's the web equivalent of bare metal programming!

The resulting WebAssembly module will be less than 300 bytes. That's about the same size as the previous paragraph.

Prereqs

I'll assume some familiarity with both Rust and JavaScript. (It's not a bad idea to go through the tutorial I mentioned above, at least the setup section, to make sure you have the tools installed.) You will not need to know very much about WebAssembly.

Concretely, you will need these tools from the tutorial:

Because we're going to be manipulating the compiler output, you'll also need two sets of tools that weren't mentioned in the tutorial:

Finally, all the examples below will assume you're on some flavor of Unix.

This post will cover the process of creating tiny graphics demos without doing any binary hacking, hex editing, or writing WASM by hand. Those are all fun techniques, and I'll probably write a separate post on them later. But for this article, you don't need to understand any of that stuff.

I've posted the complete code in a GitHub repository, with one commit per tutorial step. I'll link to the commit from each step below if you want to follow along.

Scope and goals

Our goal is to write a tiny program in Rust, and display the output in a browser. While we could use text for the output, my preferred way of showing something works is by drawing pictures. So let's do that.

To get the program loaded into the browser, we'll compile it as a WebAssembly module. A WebAssembly module is essentially a dynamic (shared) library. That is, it is not a traditional executable with a main function that gets run — it is a collection of exported things, which can be functions or variables. JavaScript loads and instantiates the module, and then has access to whatever it exports. You can have a main function, if you want, but it isn't special — you will need to call it yourself, from JavaScript, if you want it to execute.

Out of the box, the WebAssembly environment doesn't have a concept of "graphics." The only things a module can do to interact with the outside world (including the browser) are

  1. Compute things in functions that are exported to JavaScript.

  2. Call functions that are imported from JavaScript (or other WebAssembly modules).

We could write a bunch of JavaScript functions wrapping canvas or something, and provide them to the WebAssembly module as imports. But since we aren't using tools like wasm-bindgen, that would be complicated and tedious.

Instead, we are going to produce an image in the simplest way possible: we're going to deposit pixels into a region of memory from the WebAssembly program, and then tell JavaScript where to find it. (JavaScript will then be responsible for pasting those pixels into a canvas.)

Let's get started!

Creating the simplest possible WebAssembly module

(Code for this step on GitHub)

As a first step, let's make a tiny WebAssembly module that does nothing useful. This will serve as a template for our real code.

Create a new project using Cargo:

$ cargo new --lib bare-metal-wasm

(We requested a lib style project because, as I noted above, a WebAssembly module is basically a shared library.)

Alter the crate type of the new project to be cdylib by adding these lines to Cargo.toml:

[lib]
crate-type = ["cdylib"]

And now, build it:

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

(We're building with --release because we want small binaries.)

Great! We now have a WebAssembly module that contains no functions or variables of any kind. It should be tiny, right? Let's look!

$ ls -lh target/wasm32-unknown-unknown/release/bare_metal_wasm.wasm
-rwxr-xr-x 2 cbiffle cbiffle 812K Jun  7 19:36 target/wasm32-unknown-unknown/release/bare_metal_wasm.wasm

...yes, that says 812 kiB, which is, um, bigger than we were expecting. If you've tried to create small executables before, you can probably guess why: the binary still contains debug symbols. Let's strip it.

$ wasm-strip target/wasm32-unknown-unknown/release/bare_metal_wasm.wasm
$ ls -lh target/wasm32-unknown-unknown/release/bare_metal_wasm.wasm
-rwxr-xr-x 2 cbiffle cbiffle 102 Jun  7 20:32 target/wasm32-unknown-unknown/release/bare_metal_wasm.wasm

Now it's 102 bytes. It would fit in a tweet! That's a better place to start. We can do better, though, by applying wasm-opt:

$ wasm-opt -o opt.wasm -Oz target/wasm32-unknown-unknown/release/bare_metal_wasm.wasm
$ ls -lh target/wasm32-unknown-unknown/release/bare_metal_wasm.wasm
-rw-r--r-- 1 cbiffle cbiffle 71 Jun  7 20:35 opt.wasm

Now we're down to 71 bytes. We could make that smaller1, but we'll stop there.

Dumping the code of our 71-byte binary shows that it does absolutely nothing.

$ wasm-objdump -d opt.wasm

opt.wasm:	file format wasm 0x1

Code Disassembly:

Just like we wanted!

1

There are some default exports related to memory management that we could remove, and we could shorten some internal names if we really wanted. This could save a couple dozen bytes, but it would take more work than just running wasm-strip and wasm-opt. I may cover this in a future post.

Creating the smallest useful WebAssembly module

(Code for this step on GitHub)

Let's add a Rust function that produces a number, and print that number in JavaScript.

First, we need some prerequisites. We aren't going to use Rust's std library — it's awesome, but we can do without. Instead, we'll rely on core, which underpins std. We will alter src/lib.rs to opt-out of std2, so that we'll get an error if we try to use it. We then we need to provide a function to handle panics — something std normally does for us.

2

Opting out of std doesn't change the size of your program. Rust programs only include the code from std that they use. So if you don't use std, like the programs on this page, you don't strictly speaking need to opt out of std to make small programs. I choose to opt out because it means fewer surprises of the form "hey, my program suddenly grew by 12kiB, what gives?"

Remove the generated code from src/lib.rs and start with:

// src/lib.rs

#![no_std]

#[panic_handler]
fn handle_panic(_: &core::panic::PanicInfo) -> ! {
    loop {}
}

Any panic will hang the page, so try not to panic.

Now, the function we'll call from JavaScript.

#[no_mangle]
pub extern fn the_answer() -> u32 {
    42
}

(#[no_mangle] ensures that the function is exported as, verbatim, the_answer in JavaScript. In general, you will want this attribute on anything that you export from Rust.)

And we're done! We need to build and minimize the binary again. That's going to get repetitive, so let's put it in a shell script called build.sh:

#!/bin/bash

set -euo pipefail

TARGET=wasm32-unknown-unknown
BINARY=target/$TARGET/release/bare_metal_wasm.wasm

cargo build --target $TARGET --release
wasm-strip $BINARY
mkdir -p www
wasm-opt -o www/bare_metal_wasm.wasm -Oz $BINARY
ls -lh www/bare_metal_wasm.wasm

Run it:

$ chmod +x build.sh
$ ./build.sh
    Finished release [optimized] target(s) in 0.01s
-rw-r--r-- 1 cbiffle cbiffle 103 Jun  7 21:29 www/bare_metal_wasm.wasm

We're up to 103 bytes, but we now have code to actually do something, which we can see if we objdump the binary:

$ wasm-objdump -d www/bare_metal_wasm.wasm

www/bare_metal_wasm.wasm:	file format wasm 0x1

Code Disassembly:

000063 <the_answer>:
 000064: 41 2a                      | i32.const 42
 000066: 0b                         | end

Let's embed it in a webpage and see if it works. Here's www/index.html:

<!DOCTYPE html>
<script type="module">
  async function init() {
    const { instance } = await WebAssembly.instantiateStreaming(
      fetch("./bare_metal_wasm.wasm")
    );

    const answer = instance.exports.the_answer();
    console.log(answer);
  }

  init();
</script>

Now, if we load that page in a browser, we would hope to see 42 printed in the developer console. But there's a catch: browsers won't load WebAssembly modules from local files. You need to serve the page and module through a web server. If you already have a web server somewhere, toss the page and module on it and test them out. Otherwise, here's a simple webserver:

A simple webserver for serving this app

Python includes a webserver that will do the job, but it doesn't serve the correct MIME type for WebAssembly modules. So we have to configure it.

Paste this into a file called serve.py:

#!/usr/bin/env python3

import http.server
import socketserver

PORT = 8080

Handler = http.server.SimpleHTTPRequestHandler

Handler.extensions_map[".wasm"] = "application/wasm"

with socketserver.TCPServer(("", PORT), Handler) as httpd:
    print("serving at port", PORT)
    httpd.serve_forever()

Now, if you cd www and run serve.py, the contents of the www directory will be served up at http://localhost:8080.

Browse to our index.html file on your webserver, and open your browser's developer tools console (typically F12). You should see 42 printed.

We now have a trivial — but working! — web application using Rust. For those keeping score, our file sizes are currently:

$ ls -l www
-rw-r--r-- 1 cbiffle cbiffle 103 Jun  7 21:29 bare_metal_wasm.wasm
-rw-r--r-- 1 cbiffle cbiffle 276 Jun  7 21:32 index.html

Less than 400 bytes for the entire app, and we haven't even minified the JavaScript yet.

Now let's do some graphics.

Making some pixels

(Code for this step on GitHub)

As I mentioned above, we're going to generate an image in the memory of the WebAssembly module, and then transfer it onto a canvas using JavaScript. This takes a surprisingly small amount of code, but there is a subtle part: how we lay out the image in memory.

We are going to use JavaScript's ImageData class to hold the image. That class has opinions about how image data should be formatted: each pixel consists of exactly four bytes, in the order R, G, B, A. ("A" is alpha, or opacity. We'll always set A to 0xFF, or "fully opaque.") These four-byte pixels are organized in the common raster order: left to right, top to bottom, like English text.

We'll represent the four-byte pixels using u32 in Rust. Because ImageData thinks of pixels as an array of four bytes, and we're treating it as a u32, we have to consider endianness. WebAssembly is little-endian, so the u32 contains the pixel components in the reversed order 0xAA_BB_GG_RR.

Let's declare a decent-sized image buffer in Rust. For simplicity, we'll put it at a fixed location in memory using static. This isn't idiomatic Rust, but by opting out of std we have given up our ability to allocate memory — so we do the smaller, simpler, and slightly less safe thing instead.

// in src/lib.rs

const WIDTH: usize = 600;
const HEIGHT: usize = 600;

#[no_mangle]
static mut BUFFER: [u32; WIDTH * HEIGHT] = [0; WIDTH * HEIGHT];

BUFFER is #[no_mangle] again, because we're going to be reaching into the module from JavaScript to find it, and we want its name to be predictable. (#[no_mangle] on a static also has the side effect of exporting it from the module. I still find this counter-intuitive, but that's how it works.)

We declare the BUFFER to be initially filled with zeros, because doing so is cheap. But we don't want it to be zeros forever. In particular, if we were to draw the buffer full of zeros, nothing would happen — because all the alpha bytes are zero, the image is entirely transparent.

So let's write a routine to fill it with something. Go ahead and delete the_answer and replace it with:

// back in src/lib.rs

#[no_mangle]
pub unsafe extern fn go() {
    // This is called from JavaScript, and should *only* be
    // called from JavaScript. If you maintain that condition,
    // then we know that the &mut we're about to produce is
    // unique, and therefore safe.
    render_frame_safe(&mut BUFFER)
}

// We split this out so that we can escape 'unsafe' as quickly
// as possible.
fn render_frame_safe(buffer: &mut [u32; WIDTH * HEIGHT]) {
    for pixel in buffer.iter_mut() {
        *pixel = 0xFF_FF_00_FF;
    }
}

The split between these two functions may be surprising. Remember that Rust doesn't allow accesses to mutable static variables in safe code3. Since WebAssembly is single-threaded, when JavaScript enters our module by calling go, we know that no other Rust code has a reference to BUFFER. So we can safely run the unsafe code &mut BUFFER.

3

You might be wondering why accesses to static mut variables are unsafe. It's because &mut references are by definition unique, but since any code in a module can see a static, any code in the module can just say &mut BUFFER at any time — making an unlimited number of supposedly unique references! Use of static mut is incredibly rare in Rust code; I do it in my embedded work to avoid needing a memory allocator, which is the same reason we do it here.

But at that point, we want to get out of unsafe and back to the guarantees we love so much. So we pass the &mut into render_frame_safe. The reference we pass is a unique reference to BUFFER, so manipulating the buffer through it is safe.

Now we need to alter the JavaScript to actually do something with the buffer. Here's the updated www/index.html:

<!DOCTYPE html>
<html>
  <head>
    <script type="module">
      async function init() {
        const { instance } = await WebAssembly.instantiateStreaming(
          fetch("./bare_metal_wasm.wasm")
        );

        const width = 600;
        const height = 600;

        const canvas = document.getElementById("demo-canvas");
        canvas.width = width;
        canvas.height = height;

        const buffer_address = instance.exports.BUFFER.value;
        const image = new ImageData(
            new Uint8ClampedArray(
                instance.exports.memory.buffer,
                buffer_address,
                4 * width * height,
            ),
            width,
        );

        const ctx = canvas.getContext("2d");

        instance.exports.go();

        ctx.putImageData(image, 0, 0);
      }

      init();
    </script>
  </head>
  <body>
    <canvas id="demo-canvas"></canvas>
  </body>
</html>

Run build.sh and load the files through your webserver. If everything is working, your page should now contain a bright magenta square!

Let's replace it with a simple procedural texture.

(Code for this step on GitHub)

// Replace the old render_frame_safe with this:
fn render_frame_safe(buffer: &mut [u32; WIDTH * HEIGHT]) {
    for y in 0..HEIGHT {
        for x in 0..WIDTH {
            buffer[y * WIDTH + x] = (x ^ y) as u32 | 0xFF_00_00_00;
        }
    }
}

Build and reload, and the magenta square should be replaced by a red, tartan-like pattern. It should look like this:

That's not a screenshot, incidentally, that's the actual program running. A screenshot PNG would have taken 14kiB; this took 189 bytes.

(If you can't see the texture above, you won't be able to see the ones you write, either. It's time for a browser upgrade.)

You now have a procedural texture, written in WebAssembly, and displaying in a browser! Try messing around with the generation routine to produce other patterns. The framework we've built here is enough to have quite a bit of geeky art fun, but let's keep going.

Adding animation

(Code for this step on GitHub)

Static images are fine, but moving images — that's a whole different medium. Our strategy to animate the program will rely on JavaScript and the browser's event loop.

  1. The Rust program will keep track of its state frame-to-frame. Initially, this will mean keeping track of a frame number, but it might also derive the next frame from the previous contents of BUFFER — up to you.

  2. The JavaScript wrapper will invoke the Rust go function once per frame to update the contents of BUFFER, and then display those contents.

  3. We'll use the requestAnimationFrame JavaScript API to schedule updates at each frame.

To introduce simple animation to our existing texture, we'll incorporate the frame number into our pixel formula in addition to x and y.

First: let's add some global state to the WebAssembly module to keep track of the frame number. The last global state we added was BUFFER, which required unsafe code to access (because it's a static mut and Rust is suspicious of our ability to write thread-safe code). If we just want to store a single number, we can use a much easier tool: atomics.

// in src/lib.rs

use core::sync::atomic::{AtomicU32, Ordering};

static FRAME: AtomicU32 = AtomicU32::new(0);

Now, each time JavaScript calls go, we want to update the BUFFER and then advance the FRAME. AtomicU32 provides a handy fetch_add operation that can retrieve the current frame number, and advance the global counter, in one easy step. Our render_frame_safe is now:

fn render_frame_safe(buffer: &mut [u32; WIDTH * HEIGHT]) {
    // This line is new:
    let f = FRAME.fetch_add(1, Ordering::Relaxed);

    for y in 0..HEIGHT {
        for x in 0..WIDTH {
            // This line has changed:
            buffer[y * WIDTH + x] = 
                f.wrapping_add((x ^ y) as u32) | 0xFF_00_00_00;
        }
    }
}

If you build and reload, you should see the same static tartan. We haven't changed the interface between JavaScript and Rust at all, so the program works without updating the JavaScript — it just doesn't animate. Let's fix that. We only need to change about four lines at the end of init:

    <!-- the only changes to this script are at the end... -->
    <script type="module">
      async function init() {
        const { instance } = await WebAssembly.instantiateStreaming(
          fetch("./bare_metal_wasm.wasm")
        );

        const width = 600;
        const height = 600;

        const canvas = document.getElementById("demo-canvas");
        canvas.width = width;
        canvas.height = height;

        const buffer_address = instance.exports.BUFFER.value;
        const image = new ImageData(
            new Uint8ClampedArray(
                instance.exports.memory.buffer,
                buffer_address,
                4 * width * height,
            ),
            width,
        );

        const ctx = canvas.getContext("2d");

        // CHANGES BEGIN HERE
        const render = () => {
          instance.exports.go();
          ctx.putImageData(image, 0, 0);
          requestAnimationFrame(render);
        };

        render();
      }

      init();
    </script>
    <!-- rest of page omitted in example -->

We've created a closure called render that will...

  1. Call the Rust go function.
  2. Splat its buffer into the canvas.
  3. Schedule itself to be called at the next frame.

We then have to call it once to prime the pump, as it were, and the process will run forever.

You should see this:

It's a self-mutating tartan! (Mutartan?)

And how much have we paid to introduce animation to our program? Let's check sizes:

$ ls -l www/*.wasm
-rw-r--r-- 1 cbiffle cbiffle  213 Jun  8 09:29 bare_metal_wasm.wasm

We're up to 213 bytes.

This is cool, because displaying even a few seconds of the animation as a GIF would have taken tens or hundreds of kilobytes. Every frame of our 213-byte animation is unique (though very subtly so) and there are 232 of them. Over the course of two years, it will gradually become green, and eventually, blue. That'd be a big GIF, but it's a tiny program.

At this point, I encourage you to play around with the pixel generation function and create some of your own patterns! The rest of this article is devoted to troubleshooting issues you may encounter.

Help! My binary just got much bigger! (Diagnosing and fixing sudden bloat.)

The tl;dr here is: you probably introduced panicking code, probably a bounds check. This adds anywhere from 300 bytes to 2kiB in my experience, depending on how hard the code works to produce a nice error message.

You can find out what code you just introduced by inspecting your binary. You need to look at the binary before we strip it, so you can't use build.sh. I strongly suggest you install rustfilt if you haven't already:

$ cargo install rustfilt

Now, generate an unstripped binary and dump it:

$ cargo clean
$ cargo build --target wasm32-unknown-unknown --release
$ wasm-objdump -d \
    target/wasm32-unknown-unknown/release/bare_metal_wasm.wasm \
    | rustfilt | less

You don't need to read actual WebAssembly instructions for this to be useful — just look at the function names. If you see a line like the following, you have introduced a runtime bounds check that may panic:

0001eb <core::panicking::panic_bounds_check>:

More generally, you have introduced a potential panic if you see a line reading (the number at the beginning of the line may be different):

00026d <core::panicking::panic_fmt>:

Avoiding the problem

Try to avoid introducing panicking code. Here are some tips:

Prefer references to explicitly sized arrays, rather than slices.

    &mut [u32; WIDTH * HEIGHT]   // can avoid bounds checks
    &mut [u32]                   // often can't

Use iterators rather than indexing.

fn probably_has_bounds_checks(a_slice: &mut [u32]) {
    for i in 0..WIDTH {
        a_slice[i] = 0;
    }
}

fn definitely_no_bounds_checks(a_slice: &mut [u32]) {
    // Exploit &mut[] being an iterator
    for i in a_slice {
        a_slice[i] = 0;
    }
}

Handle corner cases yourself instead of relying on panic. For every operation that might panic in core, there's an alternative that can't panic. For example, if you really need to use slices, you can access then with get:

fn might_panic(a_slice: &[u32]) -> u32 {
    // This will probably produce bounds-checking code,
    // because the compiler may not be able to convince
    // itself that a_slice points to something with at
    // least 13 elements.
    a_slice[12]
}

fn no_panic(a_slice: &[u32]) -> u32 {
    // get is still checked, but lets you decide how
    // to handle a failure.
    match a_slice.get(12) {
        Some(x) => x,
        None => {
            // If you're very confident this will
            // never happen, you can do *anything*
            // here -- return a placeholder value,
            // enter an infinite loop, draw a big
            // sad-face into the framebuffer. Here
            // is a very compact option:
            loop {}
        },
    }
}

Don't try to replace that match with unwrap() — that just replaces one panic with another.

Hacking around it with wasm-snip

In the last function above, we "handled" a panic case ourselves by entering an infinite loop. Wouldn't it be great if we could change some setting and do this to every panic — current and future?

Rust kind of provides this with the abort on panic setting, but it still brings in a bunch of panic-related code (about 2kiB of it).

There's another solution: wasm-snip. It is an imperfect solution, but it can be useful.

$ cargo install wasm-snip

Here is a binary that I produced with panicking code included:

$ ls -lh panics.wasm
-rw-r--r-- 1 cbiffle cbiffle 2.0K Jun  8 11:22 panics.wasm

And here it is after snipping:

$ ls -lh snipped.wasm
-rw-r--r-- 1 cbiffle cbiffle     663 Jun  8 11:28 snipped.wasm

wasm-snip analyzes a WebAssembly binary and replaces calls to certain functions (which you choose) with an unreachable instruction. This means the program will halt if you try to use a snipped function, reporting an exception to JavaScript. That's basically what we want in the event of a panic.

You need to snip a binary before you strip it, because wasm-snip makes use of the debug symbols to decide what to snip. To incorporate snipping into your build process, alter build.sh to read as follows:

#!/bin/bash

set -euo pipefail

TARGET=wasm32-unknown-unknown
BINARY=target/$TARGET/release/bare_metal_wasm.wasm

cargo build --target $TARGET --release

# NEW PART:
wasm-snip --snip-rust-fmt-code \
          --snip-rust-panicking-code \
          -o $BINARY \
          $BINARY

wasm-strip $BINARY
mkdir -p www/
wasm-opt -o www/bare_metal_wasm.wasm -Oz $BINARY
ls -lh www/bare_metal_wasm.wasm

This is an imperfect solution, because (as I showed above) the binary has increased from 213 to 663 bytes. If we've removed the panic code, what's responsible for the additional 450 bytes?

The answer is frustrating: string literals.

Panics take a message, and the built-in panics for things like "array index out of bounds" include messages explaining the condition. wasm-snip is currently not able to detect that those messages are unused once the panic code is removed, and so they get included in our binary. You can see them by using wasm-objdump -x:

$ wasm-objdump -x www/bare_metal_wasm.wasm

bare_metal_wasm.wasm:	file format wasm 0x1

Section Details:

# Bunch of stuff omitted for the purposes of this post...

Data[1]:
 - segment[0] size=312 - init i32=2488580
  - 025f904: 7372 632f 6c69 622e 7273 0000 04f9 2500  src/lib.rs....%.
  - 025f914: 0a00 0000 2a00 0000 0d00 0000 0200 0000  ....*...........
  - 025f924: 0000 0000 0100 0000 0300 0000 696e 6465  ............inde
  - 025f934: 7820 6f75 7420 6f66 2062 6f75 6e64 733a  x out of bounds:
  - 025f944: 2074 6865 206c 656e 2069 7320 2062 7574   the len is  but
  - 025f954: 2074 6865 2069 6e64 6578 2069 7320 0000   the index is ..
  - 025f964: 30f9 2500 2000 0000 50f9 2500 1200 0000  0.%. ...P.%.....
  - 025f974: 3030 3031 3032 3033 3034 3035 3036 3037  0001020304050607
  - 025f984: 3038 3039 3130 3131 3132 3133 3134 3135  0809101112131415
  - 025f994: 3136 3137 3138 3139 3230 3231 3232 3233  1617181920212223
  - 025f9a4: 3234 3235 3236 3237 3238 3239 3330 3331  2425262728293031
  - 025f9b4: 3332 3333 3334 3335 3336 3337 3338 3339  3233343536373839
  - 025f9c4: 3430 3431 3432 3433 3434 3435 3436 3437  4041424344454647
  - 025f9d4: 3438 3439 3530 3531 3532 3533 3534 3535  4849505152535455
  - 025f9e4: 3536 3537 3538 3539 3630 3631 3632 3633  5657585960616263
  - 025f9f4: 3634 3635 3636 3637 3638 3639 3730 3731  6465666768697071
  - 025fa04: 3732 3733 3734 3735 3736 3737 3738 3739  7273747576777879
  - 025fa14: 3830 3831 3832 3833 3834 3835 3836 3837  8081828384858687
  - 025fa24: 3838 3839 3930 3931 3932 3933 3934 3935  8889909192939495
  - 025fa34: 3936 3937 3938 3939                      96979899

We've got the source file name, a message explaining a bounds check failure, and a curious table of numbers that probably has something to do with formatting the index as decimal. We don't need any of these, but our tools don't know that.

You can fix this by hacking the binary, but that's out of scope for this post.

Help! I need trig! (Importing functions from JavaScript.)

A lot of graphics demos wind up needing trigonometry — sin and cos, mostly. If you're playing with procedural image generation, you're probably going to hit this when you try to implement Perlin noise or plasma.

Here's a replacement render_frame_safe that generates a tiny moiré pattern using sin:

fn render_frame_safe(buffer: &mut [u32; WIDTH * HEIGHT]) {
    let f = FRAME.fetch_add(1, Ordering::Relaxed);

    for y in 0..HEIGHT {
        for x in 0..WIDTH {
            // NOTE: you don't actually want to write the
            // function this way. See the note at the end
            // of this section.
            let v = (x as f32).sin() * 255.
                  + (y as f32).sin() * 255.;
            buffer[y * WIDTH + x] =
                f.wrapping_add(v as u32) | 0xFF_00_00_00;
        }
    }
}

Unfortunately, this code fails to compile:

error[E0599]: no function or associated item named `sin` found
              for type `f32` in the current scope

What? Of course f32 has a sin function. It's right there in the docs!

Unfortunately, trig routines are part of std, not core. I personally think this decision is silly4, but in our case, it turns out to be valuable.

4

I think it's silly because, on the embedded processors where I do most of my hacking, the trig routines from std often reduce to single instructions, because they use compiler features that aren't exposed to mere users like me on stable Rust. Anything I write will be less efficient, and I like efficiency.

The easiest fix is to just use std:

  1. Remove the #![no_std] attribute.
  2. Remove our custom #[panic_handler].

However, if you try this, you'll notice something alarming: calling sin alone adds 5kiB to your binary!

Why? Well, WebAssembly currently doesn't have a sin operation (though one has been proposed, and may be added in the future). This means the Rust std library has to include its own implementation of sin, and a high-quality implementation of sin takes a fair amount of code.

So, by excluding sin, core has actually just saved us from a surprise binary inflation. But now what do we do?

Sure, you could write your own low-quality version of sin, which is a time-honored tradition among demo programmers. But there's a much easier option.

I said that WebAssembly doesn't provide a sin operation, but you know who does? JavaScript.

Add this to your Rust code to request an import called js_sin. (The name is not special, but it needs to match the name we use in the JavaScript below.)

extern {
    fn js_sin(x: f32) -> f32;
}

Rust considers all imported functions to be unsafe by default. This is correct: from JavaScript we can scribble all over WebAssembly's memory! But we know that sin doesn't do anything like that. So, we provide a safe wrapper.

fn sin(x: f32) -> f32 {
    unsafe { js_sin(x) }
}

We can now call this freely in our Rust code — but for the calls to work, we have to alter the JavaScript too. At the very top of the JavaScript where we instantiate the module, we need to provide an imports object:

const { instance } = await WebAssembly.instantiateStreaming(
  fetch("./bare_metal_wasm.wasm"),
  // New imports object:
  {
    "env": {
      "js_sin": Math.sin,
    },
  }
);

Now replace all your calls to x.sin() with sin(x), and you can use trigonometry, for a cost of about a dozen bytes!

Importing other math functions from JavaScript works the same way, as long as the functions only deal in numbers. Functions that manipulate JavaScript strings, objects, etc. are better left to wasm-bindgen.

A word of caution: calling sin for every pixel like I showed above takes a lot of processing power. This is why I haven't embedded the WebAssembly program here as an example: it would drain your battery while you're reading. Just like on computers of yore, call sin ahead of time and generate a lookup table!