Hello World with Rust and WebAssembly
This article is part of a serie: Rust and WebAssembly
- Hello World with Rust and WebAssembly
- Introduction to Rust and WebAssembly
For the second part of my Rust & WebAssembly journey, I will write a basic hello world project.
Note: you can jump to the demo by clicking here.
This will give me the opportunity to demonstrate how to write a simple Wasm module in Rust. I will focus on a simple frontend and ignore the backend: no complicated GET or POST requests, no websockets, etc. This article will present how to build a simple game, such as Matt’s Pont. For more ambitious games, for instance with multiplayers, I’ll probably cover websockets in a following article.
I am going to build a very simple game, with just enough features to demonstrate how to manipulate the DOM and handle events:
the Wasm module will listen for
mousedown events, and spawn a Ferris when the user clicks.
The Ferris will have an initial velocity and fall down due to gravity.
This will demonstrate how to:
- Add elements to the DOM
- Handle events with callbacks (
- Update the DOM with callbacks (
In this section, I will focus on the actual Wasm module. In particular, there are a few caveats that one needs to pay attention to. I will explain them to avoid you wasting too much time.
I will talk about the backend in a next section. It does not matter much here, and will be rather straightforward.
Creating the project:
Creating a Wasm module starts like any Rust project:
cargo new throwing-ferris.
But before one can start writing code,
Cargo.toml needs a few adjustments.
First, the module will not be compiled with cargo directly, we will later see how to use wasm-pack.
Wasm-pack expects the project to be compiled as a library, not an executable.
To do this, you need to specify a
lib section with
crate-type = ["cdylib"].
Since cargo will now look for
src/lib.rs, I also like to add
path = "src/main.rs", I think it makes more sense.
Second, we need (at least) two dependencies:
The first one does not have any particularities, but the second one,
It is very modular, and before compiling, the features used in the project need to be specified.
Here are the features needed by our hello world project:
The features to be enabled are mentioned in the crate documentation, in the description of each method.
Cargo.toml should look like this:
pub fn main()
For the first lines of code, let’s start with the entry point, before talking about DOM manipulation and Ferris implementation.
Naturally, a few imports are needed:
wasm_bindgen::prelude::* is the absolute minimum.
Since I am here, I will also import what will be needed later, in particular
web-sys for DOM accesses.
js-sys could be needed as well.
Exported functions can have any name: a Wasm module does not expects any special name, only the
#[wasm_bindgen] attribute macro and the
In my example,
main is just a convention.
Note that you can specify a function to be executed when the Wasm module is loaded.
To achieve that, use
Here is a minimal entry point, which, at the moment, does absolutely nothing. Very boring I must say.
At this point, you should be able to compile, which I explain a bit later in this article. Nothing very interesting will happen, so you’re better off not skipping anything and just keep reading!
Note that there is no point in adding some
println(), since there is no stdout.
Minimal Hello World
Let’s try to add something in the DOM.
To manipulate the DOM and to create elements, you will first need a handle on
You can get it from the
window object, itself obtained through
Since that’s Rust, every call could fail, so there are a lot of
Options everywhere, unwrapped with
If the call to get
window fails, or if
create_element() fails, everything is probably on fire, so most probably crashing is not a big deal.
Of course, it depends on your requirements, but for the moment I’ll just sprinkle some
unwrap() and continue.
Creating an element is quite straightforward, since
Here, I first create a
<p>, and then a
<div> which will contain our Ferris.
To set the HTML content of an Element,
set_inner_html() can be used.
Note that I also set an HTML
id to my canvas.
The goal is to make it easier to get a handle on that later with
get_element_by_id(): adding Ferris will use a closure binded to the
I could also pass a canvas reference to the closure, but Rust’s borrow checker would be verbose and complicated.
id is much easier.
Now this is a basic hello world.
You could compile and run it, and see a new
But again, let’s be a bit more ambitious and continue.
The idea is to have a Ferris with a position and a velocity, moving through the canvas.
We will first declare the structure, then implement the
Ferris::new() method (called by the
mousedown event handler), and finally implement a
Ferris::update() method (called every given time interval).
The structure is quite straightforward.
I include a
web_sys::HtmlElement which will hold the HTML representation of our Ferris, so we can update its properties (position, but we could change any other CSS or HTML properties).
It is wrapped in an
Option, because it will be deleted when it reaches the edge of the canvas.
If it was needed to keep it around (in case it could reenter the canvas at some point), we could also use a boolean and just temporarily hide the HTML element.
Ferris::new() method has some interesting points worth mentioning.
When creating the HTML element,
document.create_element() returns a generic
But later it will be needed to update the CSS attributes to control the position, which is done by calling the
This method can only be called on an
web_sys::HtmlElement, which is why the
web_sys::Element must be casted into an
The second point worth explaining is the closure, which has caused me a lot of frustration.
A lot of magic is needed to avoid cryptic errors and make the borrow checker happy.
The main idea is to declare a
callback: Closure<Box<dyn Fn()>> variable, and pass
callback.as_ref().unchecked_ref() to the
In our case, we use
FnMut() instead of
Fn(), because our Ferris object is
An important thing not to forget, is to call
callback.forget(): it is needed to tell Rust to not destroy the callback when exiting from the function.
This is the quick and dirty way, in real production code, one would at least want to save the handler returned by
set_interval() to be able to stop the calls and free the callback.
The documentation has some examples.
After the complex
Ferris::new() method, the
Ferris::update() method is very simple.
First, the HTML element is checked to make sure it is not
If it is
None, then the object has been deleted (but the callback is still running, because we don’t cancel it, for ease of implementation).
Then, the physics is updated.
Until here, nothing fancy.
Finally, the interesting part: the CSS property is set via
Integrating everything with the
Lastly, to connect everything, a new Ferris will be instantiated at each mouse click.
For this, I will bind a simple closure to the
mousedown event, which will just create a new Ferris object.
The code for the closure is similar to the one in
Ferris::new(), which I already explained, so I won’t get into the details here.
The only difference is the closure parameter
Note that the
mousedown event will pass a
web_sys::MouseEvent to the closure.
If the closure declares another type, for instance a
web_sys::KeyboardEvent, it will crash.
I’m not sure how this could be prevented at compiled time, but in any case the crash is pretty obvious if you execute the closure at least once.
This concludes the frontend code, which was the most complicated. To finish this section about the frontend, there is one last step: compilation.
Compilation, although simple in theory, can be difficult, especially for new and unstable technologies such as Rust-Wasm.
There are several ways of compiling Rust into a Wasm module. For personal reasons, I don’t want to use NPM nor Webpack, unless absolutely required (I’m a C-Python-Rust dev, sorry not sorry). In the Wasm world, this means that we will compile without a bundler.
The magic command is:
wasm-pack build --target web.
In addition to the usual (in Rust world)
target/ folder (which we don’t care here),
wasm-pack will create a
pkg/ folder with several files.
Amongst them, one will of course find the famous
.wasm module, but also a
.js which contains some glue code to bootstrap the Wasm module.
There are a few other files, but for our small toy example, they are not important (again, I’m not a frontend dev, I don’t use Typescript & cie. files!).
Note that cargo is not called manually,
wasm-pack takes care of it with its single-command compilation.
One important remark, which made me lost a lot of time scratching my head:
--target no-module is the older way of compiling without a bundler, so it might still be mentioned in some places.
If you have some weird errors, double check this.
In our case, we stick with
The official documentation, with several examples, can be found here.
Loading the Wasm module
I had a few issues to make this work. There was mainly two things to get right:
- The first one is obvious and common in web development: the path of the Wasm files was bad and lead to a 404.
- The second one is due to
In our case, the correct version (for
--target web) is as follows.
In any case, apart from the custom CSS, the code is just a copy paste, and looks pretty simple.
We can run this awesome and feature-full website (haha) with Python’s builtin web server:
python3 -m http.server
For a deployment to a production environment, the Wasm module would be saved alongside the static files (CSS, images, etc).
If you did everything right, you should see something similar to this:
You can play yourself with an online demo here.
Internals - how does it work
I’m always interested in looking under the hood of things to see how they work. WebAssembly is a real world piece of tech and not a toy project taking shortcuts and using simplified hypothesis. Nonetheless, it probably is quite clean and easy to play with, because it comes from a clean sheet design, with a instruction set designed for an ideal/virtual processor. The end result is that there probably are very few workarounds such as one could find for the x86 (booting in 16 bits Real Mode, then switching to 32 or 64 bits Protected Mode… It’s a mess of hacks to ensure backward compatibilities and avoid edge cases).
So, let’s try to trace the execution path, and see how we go from the HTML to Wasm.
- The main HTML file
importloads the file
throwing_ferris_frontend.jswhich exports a (
- When called,
init()loads the Wasm module with
WebAssembly.instantiate(). It returns an
exportwill be binded to the
wasmvariable (this is important for the next step).
main()is called, it calls in reality the
wasm.main()function, which is of course the
main()function of our Wasm module which was exported with
What we have learned
I have shown how to create a small hello world project with Rust and Wasm. It demonstrates how to write a simple game which manipulates the DOM and plays with callbacks. It can be used as a stepping stone for more ambitious projects, for instance by adding websockets and a backend.
If you want to continue in Rust-Wasm game development, you can have a look at the following projects. The code is surprisingly short and simple, for the result it produces.
- RustWasm’s Paint example. There are also a few other examples being written (parallel raytracing and a TODO MVC app).
- Matt’s Pont game, which I already mentioned in my previous article. This is a great example of a basic game with a very simple code.
- Olivia’s Sokoban game: a more ambitious game using an existing 2D game engine.
For my next article, I might implement Chrome’s Dino game. It’s a good step to a simple but real game. My in progress Tarot project is a bit big and I think it is better to avoid taking a bite to big to chew.
Alternatively, I might do a small example with a Rust backend to show how to use websockets, Serde, and the Rocket web framework.