I’ve been exploring Rust+WebAssembly recently. Here is an experiment made with those - my friend said it looked like thousands of fruit loops dancing to “music”. It runs in your browser (and maybe even your phone). One file, just 4092 bytes.
WARNING - This experiment makes some sound
Click the image to try it out in your browser. When it opens, click the musical notes to start.
On phones you can watch in landscape or portrait, but at least on my android, landscape gives the best performance.
What is going on here? Why are fruit loops dancing?
This was an experiment to see if I could make anything interesting when combining a few different technologies while trying to keep the result under the challenging file size budget of just 4KB (4096 bytes).
I used:
- Rust to implement and compile the main part of the file
- WebAssembly, a new executable file format supported by modern web browsers as an alternative to Javascript
- WebGL to efficiently render graphics and audio with GPUs
- WebAudio to play sound
- Javascript to connect the Rust/WebAssembly part to the normal browser/web APIs
- HTML to get everything running
- Python to generate data files
I also set myself a few extra requirements:
- I wanted it to be all in one file
- I didn’t want to use any external compression system (such as coding the data as a PNG image the way JsExe does) - rather I wanted to find other ways to make the code density high
- I wanted the result to be viewable on both desktops and phones (as many kinds as possible)
- Landscape or portrait aspect ratios, scaling to different screen sizes
- I wanted there to be something resembling music
And some non-requirements:
- There was no need to use any of these tools “properly” - this was an experiment to see what is possible, and to learn more about the systems in the process
The fruit loops weren’t planned - they just kind of emerged as part of this process.
Seriously, what is going on here?
Let’s take a look at the file your browser gets when opening the page:
$ hd ws4k.html
00000000 3c 21 2d 2d 0a 00 61 73 6d 01 00 00 00 01 0c 02 |<!--..asm.......|
...
00000d50 cc 62 d0 1c 76 6d 20 2d 2d 3e 3c 73 63 72 69 70 |.b..vm --><scrip|
00000d60 74 3e 73 3d 5b 4f 2c 55 2c 50 5d 3d 5b 5f 3d 3e |t>s=[O,U,P]=[_=>|
...
00000ff0 29 29 29 3c 2f 73 63 72 69 70 74 3e |)))</script>|
The first few bytes are an HTML comment opening tag. That means the browser ignores everything it sees until it finds a corresponding comment close tag - which is lucky, because straight after that is the header of a WASM binary file, which would otherwise be a bit of a surprise for the browser when it was expecting to see an HTML file.
Further along in the file, we see the comment closing tag. After that the browser’s HTML parser is expecting to see things it recognises as HTML, and the next thing is an opening script tag followed by a block of 678 bytes of javascript code, which the browser will run immediately after it has been parsed. A closing script tag ends the file.
Now I’ll show you how to produce a similar minimal page like this using Rust.
Making a minimal self-contained runnable WASM page with Rust
Using Rust to generate WebAssembly programs is well documented, and if you want to make a serious program then those tools and techniques are the way to go. In particular, the wasm-bindgen and web-sys crates give you robust, type-checked access to the entire range of Web APIs. I’ve used all of those before, and I recommend them.
(Also, here is a really nice blog post diving into some aspects of how Rust and WebAssembly work together).
However, to keep things really small and learn more, I decided to use the more basic support for compiling WebAssembly programs from Rust.
With Rust and cargo (the Rust build management system) installed, start with this kind of “Cargo.toml” file:
[package]
name = "ws4k"
version = "0.1.0"
edition = "2018"
[dependencies]
[lib]
crate-type = ["cdylib"]
[profile.release]
lto = true
opt-level = "z"
and this in “src/lib.rs”:
#![no_std]
#[link(wasm_import_module = "i")]
extern {
fn r(which:u32);
}
const JS : &str = "-->\
<script>\
fetch('').then(\
r=>r.arrayBuffer().then(\
b=>WebAssembly.instantiate(\
b.slice(4),{\
i:{r:a=>console.log('Rust say',a)}\
}).then(\
o=>{o.instance.exports.d();}\
)\
)\
)\
</script>";
#[no_mangle]
pub extern fn d() -> u32 { // entry point
unsafe {
r(42); // Call to JS
}
JS.as_ptr() as u32 // Return this to prevent str js being removed from binary
}
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
This starts with a no_std directive, which prevents using any of the rust standard library (which could increase the file size).
Then we declare an external function “r” which can be called (unsafely) to interact with the host javascript environment,
Next, we define a const string containing the host javascript code. When the program is compiled, this will be placed at the end of resulting wasm file. This code wil run first when the file loads, and will:
- First reload the entire page as a binary array
- Strip off the 4 byte header
- Pass that to the WebAssembly system for parsing
- Define the callback function from Rust (“r”) which prints a value to the console
- Finally, call the entry function “d” to start running the compiled Rust (WebAssembly) code
Next we define the entry function “d”. It just calls back to the host with the value 42, and returns the address of the JS string (to prevent it being optimised out of the binary).
Finally, a dummy panic handler is needed to compile, but it will not be used or produce any code.
To compile this:
$ cargo build --target=wasm32-unknown-unknown --release
The .wasm file is at target/wasm32-unknown-unknown/release/ws4k.wasm.
Unfortunately, it is 26KB! There is clearly more work to do before this will be small enough to use.
To get the .wasm file size down, we need the “wasm-opt” tool from the WebAssembly binaryen package.
I use it like this:
wasm-opt -O4 --strip-debug --strip-producers --minify-imports-and-exports target/wasm32-unknown-unknown/release/rustwasmmin.wasm -o ws4k_opt.wasm
The resulting file ws4k_opt.wasm is now only 266 bytes, quite a reduction.
To make a runnable file we simply need the 4 byte header “<!–":
echo -n '<!--' > header.js
cat header.js ws4k_opt.js > ws4k_final.html
Now if you start up a local http server (I usually use “python3 -m http.server”), open “ws4k_final.html” in your browser and check the debug console, you should see:
If so, that means;
- The browser ran the wasm loading code and called Rust function “d”
- The Rust code for function “d” ran, and called back to the javascript function “r” with argument “42”
- The javascript function “r” ran and printed the argument from Rust to the console
So everything is up and running on both sides, and we only needed one small (270 byte) file.
Now draw the rest of the owl
There is quite a lot more plumbing work to do before we can see and hear the dancing fruit loops.
There needs to be a way to call a wide range of javascript functions from Rust. Then we will need to use the WebGL and WebAudio APIs to generate some output.
I will go into more of those details in the next post (see Part 2), .