Rust and WebAssembly /

Hello World with Rust and WebAssembly


This article is part of a serie:  Rust and WebAssembly

  1. Hello World with Rust and WebAssembly
  2. 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.

Throwing Ferris

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.

Ferris wearing a construction helmet with a Wasm logo on it.
No Wasm Ferris will be harmed during this project

This will demonstrate how to:

Frontend

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: cargo new

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: wasm-bindgen and web-sys. The first one does not have any particularities, but the second one, web-sys, does. It is used to access JavaScript APIs. 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:

1
2
3
4
5
6
7
8
9
[dependencies.web-sys]
version = "0.3.4"
features = [
  'Document',
  'Element',
  'HtmlElement',
  'Node',
  'Window',
]

The features to be enabled are mentioned in the crate documentation, in the description of each method.

The final Cargo.toml should look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
[package]
name = "wasm-app"
version = "0.1.0"
authors = ["Adrien Chardon <adrienchardon@mailoo.org>"]
edition = "2018"

[lib]
crate-type = ["cdylib"]
path = "src/main.rs"

[dependencies]
wasm-bindgen = "0.2"

[dependencies.web-sys]
version = "0.3.4"
features = [
  'Document',
  'Element',
  'HtmlElement',
  'Node',
  'Window',
]

Entry point: pub fn main()

For the first lines of code, let’s start with the entry point, before talking about DOM manipulation and Ferris implementation.

Entry Point

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. To access some JavaScript APIs, 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 pub attribute. 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 #[wasm_bindgen(start)].

Here is a minimal entry point, which, at the moment, does absolutely nothing. Very boring I must say.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use wasm_bindgen::JsCast;
use wasm_bindgen::prelude::*;  // JsValue, Closure, dyn_into
use web_sys;

#[wasm_bindgen]
pub fn main() -> Result<(), JsValue> {

    // ... code ...

    Ok(())
}

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. To see something, you would have to use JavaScript’s console.log() or alert(). Alternatively, you could also return a value, for instance a string, and print it from the JavaScript code executing the Wasm module.

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 document. You can get it from the window object, itself obtained through web_sys::window().

Since that’s Rust, every call could fail, so there are a lot of Options everywhere, unwrapped with unwrap(). 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 web-sys follows JavaScript’s API. 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 mousedown event. I could also pass a canvas reference to the closure, but Rust’s borrow checker would be verbose and complicated. Using an id is much easier.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#[wasm_bindgen]
pub fn main() -> Result<(), JsValue> {
    let document = web_sys::window().unwrap().document().unwrap();
    let body = document.body().unwrap();

    // Hello from Rust

    let p = document.create_element("p").unwrap();
    p.set_inner_html("Hello from Rust!");
    body.append_child(&p).unwrap();

    // create canvas

    let canvas = document.create_element("div").unwrap();
    canvas.set_id("canvas");
    body.append_child(&canvas).unwrap();

    // return

    Ok(())
}

Now this is a basic hello world. You could compile and run it, and see a new <p> appear. But again, let’s be a bit more ambitious and continue.

Main object: struct Ferris

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).

Structure declaration

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct Ferris {
    // physics
    pos_x: i32,
    pos_y: i32,
    vel_x: i32,
    vel_y: i32,

    // html
    html: Option<web_sys::HtmlElement>,
}

Ferris::new()

The Ferris::new() method has some interesting points worth mentioning.

When creating the HTML element, document.create_element() returns a generic web_sys::Element. But later it will be needed to update the CSS attributes to control the position, which is done by calling the style() method. This method can only be called on an web_sys::HtmlElement, which is why the web_sys::Element must be casted into an web_sys::HtmlElement with dyn_into().

The second point worth explaining is the closure, which has caused me a lot of frustration. Passing closures to JavaScript callbacks is a bit complicated, and I have not yet found a clean and easy way to do it. 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 set_interval() function. In our case, we use FnMut() instead of Fn(), because our Ferris object is mut. 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// sizes in px
const CANVAS_WIDTH: i32 = 800;
const CANVAS_HEIGHT: i32 = 600;
const FERRIS_WIDTH: i32 = 80;
const FERRIS_HEIGHT: i32 = 50;

const ANIM_DELAY: f64 = 0.025;  // 0.025 sec -> 40 FPS

// in pxl/frame and pxl/frame**2
const INITIAL_VELOCITY_X: i32 = 10;
const INITIAL_VELOCITY_Y: i32 = 15;
const ACCELERATION_Y: i32 = -1;


impl Ferris {
    pub fn new(px: i32, py: i32) {

        // if out of the canvas, let's not even bother instantiate a Ferris

        if ! Ferris::is_in_canvas(px, py) {
           return;
        }

        let window = web_sys::window().unwrap();
        let document = window.document().unwrap();

        // create ferris html element and rust struct

        let html = document
            .create_element("div").unwrap()
            .dyn_into::<web_sys::HtmlElement>().unwrap();
        html.set_attribute("class", "ferris").unwrap();

        document
            .get_element_by_id("canvas").unwrap()
            .append_child(&html).unwrap();

        let mut ferris = Ferris {
            pos_x: px,
            pos_y: py,
            vel_x: INITIAL_VELOCITY_X,
            vel_y: INITIAL_VELOCITY_Y,

            html: Some(html),
        };

        // initial update, dont wait the callback's first tick

        ferris.update();

        // register callback

        let callback = Closure::wrap(Box::new(move || {
            ferris.update();
        }) as Box<dyn FnMut()>);

        window.set_interval_with_callback_and_timeout_and_arguments_0(
            callback.as_ref().unchecked_ref(),
            (ANIM_DELAY*1000.0) as i32,
        ).unwrap();

        callback.forget();
    }
}

Ferris::update()

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 None. 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 html.style().set_property().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
impl Ferris {
    pub fn is_in_canvas(pos_x: i32, pos_y: i32) -> bool {
        (pos_x-FERRIS_WIDTH/2 > 0) && (pos_x+FERRIS_WIDTH/2 < CANVAS_WIDTH)
        && (pos_y-FERRIS_HEIGHT/2 > 0) && (pos_y+FERRIS_HEIGHT/2 < CANVAS_HEIGHT)
    }

    pub fn update(self: &mut Ferris) {

        // if html element does not exist, do nothing and return

        let html = match &self.html {
            None => return,
            Some(html) => html,
        };

        // update vel and pos

        self.vel_y += ACCELERATION_Y;

        self.pos_x += self.vel_x;
        self.pos_y += self.vel_y;

        if ! Ferris::is_in_canvas(self.pos_x, self.pos_y) {
            html.remove();
            self.html = None;
            return;
        }

        // display

        html.style().set_property(
            "top",
            &format!("{:}px", (CANVAS_HEIGHT-self.pos_y)-FERRIS_HEIGHT/2),
        ).unwrap();

        html.style().set_property(
            "left",
            &format!("{:}px", self.pos_x-FERRIS_WIDTH/2),
        ).unwrap();
    }
}

Integrating everything with the mousedown event

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 web_sys::MouseEvent.

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/// pub fn main() { ...

    // mousedown callback

    let callback = Closure::wrap(Box::new(|event: web_sys::MouseEvent| {
        Ferris::new(
            event.offset_x(),
            CANVAS_HEIGHT-event.offset_y(),
        );
    }) as Box<dyn Fn(_)>);

    canvas.add_event_listener_with_callback(
        "mousedown",
        callback.as_ref().unchecked_ref(),
    ).unwrap();

    callback.forget();

/// ... }

This concludes the frontend code, which was the most complicated. To finish this section about the frontend, there is one last step: compilation.

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. But it has some limitations, and more importantly, the JavaScript code to instantiate the Wasm module is different. If you have some weird errors, double check this. In our case, we stick with --target web.

The official documentation, with several examples, can be found here.

Backend

With the main app done, we can now switch to the less interesting, but nonetheless necessary, part: the backend. For my small example, I will write a minimal HTML file with a few lines of JavaScript, just enough to load and run our Wasm module.

Loading the Wasm module

I had a few issues to make this work. There was mainly two things to get right:

  1. The first one is obvious and common in web development: the path of the Wasm files was bad and lead to a 404.
  2. The second one is due to wasm-pack’s --target option: the JavaScript used to load the module is different when using web or no-module.

In our case, the correct version (for --target web) is as follows. I’m definitely not a JavaScript expert, so I won’t comment. In any case, apart from the custom CSS, the code is just a copy paste, and looks pretty simple.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<html>
    <head>
        <meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>

        <style>
            #canvas {
                width: 800px;
                height: 600px;
                margin: auto;
                position: relative;
                border: 1px solid black;
                background-color: #E0E0E0;
            }

            .ferris {
                width: 80px;
                height: 50px;
                position: absolute;
                background-image: url("https://blog.nodraak.fr/images/2020/06/wasm-ferris.png");
                background-size: cover;
            }
        </style>

        <script type="module">
            import init, { main } from '/static/wasm/throwing_ferris_frontend.js';
            async function run() {
                await init();
                main();
            }
            run();
        </script>
    </head>

    <body>
        <p>Hello from html.</p>
    </body>
</html>

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).

Demo

If you did everything right, you should see something similar to this:

Demo showing Ferris appearing on the screen with an initial velocity and falling down.
Rad, isn’t it?

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.

  1. The main HTML file index.html is loaded
  2. import loads the file throwing_ferris_frontend.js which exports a (default) init() and a main() functions
  3. When called, init() loads the Wasm module with WebAssembly.instantiate(). It returns an instance object, whose export will be binded to the wasm variable (this is important for the next step).
  4. When 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 #[wasm_bindgen] and pub.

Conclusion

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.

Next steps

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.

Next article

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.