I have this perverse but pronounced desire to write Rust code that
runs in my browser. It's not too hard to get going with wasm-bindgen, but I found this
experience a little too magical. In pursuit of better understanding of the
underlying technologies, I put together a minimal wasm generation example that
depends only on the rustc
compiler and the
WebAssembly
browser API.
Although it's hard to imagine that the shittiest Hello World demo written the
most impossibly obtuse way (literally using three and a half different
programming languages ((including html!) kindof!!) will be useful to anyone else,
I am nonetheless leaving
this little breadcrumb for others who may be following this trail.
If you haven’t already looked at the amazing tutorial for
wasm-bindgen, definitely start there. It’s very full-featured and it
introduces the long toolchain that is oriented towards shipping
production-grade Rust to the web browser.
Ok enough, with the reminiscing of when my grandma taught me this recipe for delicious wasm. Start with a little Rust function that I want to make available to JavaScript
I'm a little scared of cargo so I want to use rustc directly.
If this works without error, it'll produce a bones.wasm
file in the directory
where you run it. The
last thing I'll need is an html page which runs some JavaScript that loads the
WebAssembly module. Something like this:
You might think that's enough to get going, but it's not. Because of the rules around Cross-Origin Resource Sharing (CORS), the only way to get wasm into your browser is to run a server. There are endless ways to do that, and if you're good with Rust, you can use the clever warp API to quickly put together a file server. However, as a Rust newcomer, I found warp a little hard to understand when I wandered off the paved path. As little as I like Python, I think it offers the easiest way to get a server going:
python3 -m http.server
Now, watch out!! ☠️ This command serves files with cache headers, and there is
nothing more confusing than unexpected caching. You have to use hard
refresh (Shift+Reload) to get your latest version each time you run. If that
bothers you as much as it bothers me, this
little Python script will add no-cache
headers to your simple
server.
This is enough to get started, but I wanted to share a couple more things I figured out the hard way.
Passing dynamic-length data
You're going to want to be able to log from Rust, since there's no other way to
debug wasm code. This means creating a string in Rust that JavaScript can read.
The magic of wasm-bindgen
can do this for you, but if you want to roll your own, may I suggest
the humble, null-terminated, UTF-8
encoded CString
? It has the virtues of representing
variable-length data with a single value and of being well supported in Rust's
standard library.
In order to give Rust the ability to log to the console, we need to provide it
with an extern
function to call. We pass the
consoleLog
function in the importObject
argument so it can be dynamically linked to the new WebAssembly module.
The "env
" name is arbitrary, but it is defined as the
default.
Since we are planning to receive in JavaScript a string that was allocated in Rust, we need some way
to tell Rust when we are done with it, otherwise this interface will leak memory.
We'll plan to expose a dealloc_cstring
function from Rust to do
just that.
In Rust, in addition to declaring the new consoleLog
extern
binding, we implement a littler helper,
along with the dealloc_cstring
function.
Passing a String from JavaScript to Rust works in reverse; we write a function in Rust that allocates a Vec of bytes and then tells the allocator to forget about them. Later, when we receive the string in Rust, we take ownership with CString::from_raw
.
Using Rust data structures
It took me forever to figure out how to use a Rust data structure in the wasm
context that I could hold onto between invocations. It didn't even occur to me
to try and use the static
keyword until I had banged my head
against all kinds of crazy byte encoding schemes and rebuilding the world from
a byte array on every invocation. The only really bad thing about using a
mutable static for this purpose is the ugly unsafe
blocks that
surround access to it. But I forgive Rust for not knowing that this code can
never run multi-threaded, and I appreciate that Rust is generally telling me
not to do this. I generally wouldn't. There are plenty of industrial-strength
solutions to this problem, including the lazy-static
crate,
but ultimately I convinced myself that it was ok to just use a static
singleton.