In a previous post, I said I’d like to follow up and attempt building clientside web apps by compiling rust to WebAssembly, or wasm for short. I’ve been wanting to compile to wasm since… probably first watching The Birth & Death of JavaScript.

I had no idea if this would work. It seems pretty experimental / work-in-progress technology. But it worked! And it wasn’t as hard as I expected!

tl;dr; here is the source and the demo.

Note: For brevity, I’ll paraphrase most of the code segments. I’ll use ... to omit code, so please check the source for a complete version.

Step One: Split Game/View

My plan was to split the game into the game-logic and the text rendering. main.rs looked like this:

mod game;
mod text;

fn main() {
    text::run_loop();
}

with game.rs exposing 3 public functions:

pub fn setup() -> Board {
    ...
}
pub fn board_slice(board: &Board, x: isize, y: isize) -> bool {
    ...
}
pub fn next_generation(board: &Board) -> Board {
    ...
}

Note that board_slice is exposing the appearance of integer cartesian coordinate, but actually translating them into unsigned array offsets behind the scenes.

Now text::run_loop can look like this:

pub fn run_loop() {
    let mut board = setup();
    for i in 0.. {
        print!("{}[2J", 27 as char); // clear screen
        let num_cells = board.len();
        print_board(&board);
        sleep(Duration::from_millis(DURATION));
        board = next_generation(&board);
    }
}

And that worked! Same functionality as before.

Step Two: from txt to web

I opted to test out the yew framework, which itself build atop the great work of cargo-web and stdweb.

Using version 0.2.0 of yew, we can replace txt::run_loop with yew::run_loop in our fn main:

mod game;
mod model;
mod view;
mod actions;

#[macro_use]
extern crate yew;

use yew::{initialize, run_loop, html::App, services::interval::IntervalService};

fn main() {
    initialize();
    let mut app = App::new();
    let context = model::Context {
        interval: IntervalService::new(app.sender()),
    };
    let model = model::Model {
        board: game::setup(),
        speed: 5,
        clock: 0,
        job: None,
        running: false,
    };
    app.mount(context, model, actions::update, view::render);
    run_loop();
}

Whoa! What is all this stuff? It basically all comes down to app.mount, which takes:

  • a context, in this case allowing us to interface with setInterval
  • a Model, which is a struct defining our app state
  • an update function, with signature
    pub fn update(context: &mut Context, model: &mut Model, msg: Msg) {...}
    

    that mutates the model and context in response to a message

  • a render function, with signature
    pub fn render(model: &Model) -> Html<Msg> {...}
    

    that renders a virtual DOM via yew’s html! macro.

Step Three: filling in model, update, and render

The model definition is fairly straightforward:

pub struct Model {
    pub board: game::Board,
    pub clock: u64,
    pub speed: u8,
    pub job: Option<Box<yew::services::Task>>,
    pub running: bool,
}
pub enum Msg {
    Step,
    Incr,
    Decr,
    Start,
    Stop,
    Reset,
}
pub struct Context {
    pub interval: yew::services::interval::IntervalService<Msg>,
}

The update function processes a message, and can fully mutate context and model:

pub fn update(context: &mut Context, model: &mut Model, msg: Msg) {
    match msg {
        Msg::Step => {
            model.clock += 1;
            model.board = game::next_generation(&model.board);
        }
        Msg::Start => {
            do_start(context, model);
        }
        Msg::Stop => {
            do_stop(model);
        }
        ...
    }
}
fn do_start(context: &mut Context, model: &mut Model) {
    let timeout = from_speed(model.speed);
    let handle = context.interval.spawn(timeout, || Msg::Step);
    model.job = Some(Box::new(handle));
    model.running = true;
}
fn do_stop(model: &mut Model) {
    if let Some(mut task) = model.job.take() {
        task.cancel();
    }
    model.job = None;
    model.running = false;
}

Pretty nifty! Now all that’s left is a render function:

pub fn render(model: &Model) -> Html<Msg> {
    let rows = board2grid(&model.board);
    html! {
        <div>
            <section class="section",>
                ...
                <div class="container",>
                    <div class="level",>
                        <div class="level-item",>
                            <table class="grid",>
                                { for rows.iter().map(view_row)  }
                            </table>
                        </div>
                    </div>
                </div>
                ...
            </section>
        </div>
    }
}

Actions get dispatched by attaching a closure to an event listener, in my case “onclick”. Eg

fn show_start() -> Html<Msg> {
    html! {
        <div class="level-item",>
            <button class="button", onclick=move|_| Msg::Step,>{ "Step" }</button>
            <button class="button", onclick=move|_| Msg::Start,>{ "Start" }</button>
            <button class="button", onclick=move|_| Msg::Reset,>{ "Reset" }</button>
        </div>
    }
}

So clicking those buttons will trigger the appropriate state changes we I defined up above.

And that’s largely it! Plus filling in the boiler place, implementing the rest of the functions, adding some styles with bulma.

Conclusions

rust+wasm seems pretty exciting and I’m overall pretty happy with my experience, given how new the ecosystem is. It still has alot of room for improvement, but I’m looking forward to some stable of these libraries.

What worked well:

  • I didn’t write any JS! yay!!
  • when I get the code to compile, it just works
  • page reloading with cargo web start --release --auto-reload

What I struggled with:

  • it’s not always easy to get the code to compile :)

What could be improved:

  • wasm runtime seems bloated, could be faster
  • wasm target currently doesn’t support debug builds :(

    warning: debug builds on the wasm32-unknown-unknown are currently totally broken

  • incremental compilation is enabled on rust stable as of 1.24, but compiling stilll feels pretty slow
  • yew is still alpha level. I had to cherrypick some patches from the master branch back to 0.2.0 (the version I’m using)
  • yew’s html! doesn’t statically check for well formed html