About two weeks ago, we kicked off our effort to collectively build Gloo, a modular toolkit for building fast and reliable Web apps and libraries with Rust and Wasm. We knew we wanted to explicitly cultivate the Rust and Wasm library ecosystem by spinning out reusable, standalone libraries: things that would help you out whether you were writing a green-field Web app in pure-Rust, building your own framework, or surgically inserting some Rust-generated Wasm into an existing JavaScript project. What was still fuzzy, and which we didn’t know yet, was how we were going design and expose these reusable bits.

Onion-Layered APIs

I’m pleased to tell you that that after some collaborative discussion in issue threads, we’ve come up with a promising approach to designing Gloo APIs, and we’ve since formalized it a bit in CONTRIBUTING.md. I’ve nicknamed this approach “onion-layered” API design.

Briefly, we want to build mid-level abstraction libraries on top of raw -sys bindings, build futures and streams integration on top of the mid-level APIs, and build high-level APIs on top of all that. But — crucially — every layer should be publicly exposed and reusable.

While this approach to API design is certainly not novel, we want to very deliberately follow it so that we

  • maximize reusability for the larger ecosystem, and
  • exercise our mid-level APIs when building higher-level APIs, to ensure their generality and suitability for acting as a solid foundation.

As we go through and examine each layer, I’ll use the setTimeout and setInterval Web APIs as a running example.

The Core: wasm-bindgen, js-sys, and web-sys

The innermost layer are raw bindings built on top of wasm-bindgen, js-sys and web-sys. These bindings are fast, have a light code size foot print, and are future-compatible with the host bindings proposal.

What they are not is super ergonomic to use all of the time. Using raw web-sys bindings directly can sometimes feel like making raw libc calls instead of leveraging Rust’s nice std abstractions.

Here is doing some operation after a 500 millisecond timeout using raw web-sys bindings:

use wasm_bindgen::{closure::Closure, JsCast};

// Create a Rust `FnOnce` closure that is exposed to JavaScript.
let closure = Closure::once(move || {
    do_some_operation();
});

// Get the JavaScript function that reflects our Rust closure.
let js_val = closure.as_ref();
let js_func = js_val.unchecked_ref::<js_sys::Function>();

// Finally, call the `window.setTimeout` API.
let timeout_id = web_sys::window()
    .expect("should have a `window`")
    .set_timeout_with_callback_and_timeout_and_arguments_0(js_func, 500)
    .expect("should set a timeout OK");

// Then, if we ever decide we want to cancel the timeout, we do this:
web_sys::window()
    .expect("should have a `window`")
    .clear_timeout_with_handle(timeout_id);

The callbacks Layer

When we look at the raw web-sys usage, there is a bit of type conversion noise, some unfortunate method names, and a handful of unwraps for ignoring edge-case scenarios where we prefer to fail loudly rather than limp along. We can clean all these things up with the first of our “mid-level” API layers, which in the case of timers is the callbacks module in the gloo_timers crate (which is also re-exported from the gloo umbrella crate as gloo::timers).

The first “mid-level” API built on top of the -sys bindings exposes all the same functionality and the same design that the Web does, but uses proper Rust types. For example, at this layer, instead of taking untyped JavaScript functions with js_sys::Function, we take any F: FnOnce(). This layer is essentially the least opinionated direct API translation to Rust.

use gloo::timers::callbacks::Timeout;
// Alternatively, we could use the `gloo_timers` crate without the rest of Gloo:
// use gloo_timers::callbacks::Timeout;

// Already, much nicer!
let timeout = Timeout::new(500, move || {
    do_some_operation();
});

// If we ever decide we want to cancel our delayed operation, all we do is drop
// the `timeout` now:
drop(timeout);

// Or if we never want to cancel, we can use `forget`:
timeout.forget();

Layering on Futures and Streams

The next layer to add is integrating with popular traits and libraries in the Rust ecosystem, like Futures or serde. For our running gloo::timers example, this means we implement a Future backed by setTimeout, and a Stream implementation backed by setInterval.

use futures::prelude::*;
use gloo::timers::futures::TimeoutFuture;

// By using futures, we can use all the future combinator methods to build up a
// description of some asynchronous task.
let my_future = TimeoutFuture::new(500)
    .and_then(|_| {
        // Do some operation after 500 milliseconds...
        do_some_operation();

        // and then wait another 500 milliseconds...
        TimeoutFuture::new(500)
    })
    .map(|_| {
        // after which we do another operation!
        do_another_operation();
    })
    .map_err(|err| {
        handle_error(err);
    });

// Spawn our future to run it!
wasm_bindgen_futures::spawn_local(my_future);

Note that we use futures 0.1 for now, because we’ve fought tooth and nail to get the Wasm ecosystem on stable Rust, but as soon as the new std::future::Future design is stable, we plan to switch over. We are very excited for async/await as well!

More Layers?

That’s all the layers we have for the setTimeout and setInterval APIs. Different Web APIs will have different sets of layers, and this is fine. Not every Web API uses callbacks, so it doesn’t make sense to always have a callbacks module in every Gloo crate. The important part is that we are actively identifying layers, making them public and reusable, and building higher-level layers on top of lower-level layers.

We will likely add even higher-level layers to other Web APIs where it makes sense. For example, the File API’s FileReader interface exposes methods that you shouldn’t call until after certain events have fired, and any attempt to call them earlier will throw. We can codify this as a state machine-based Future, that doesn’t even give you the ability to call those methods until after the relevant events have fired and the state machine reaches a certain state. Leveraging types at compile time for ergonomics and correctness!

Another future direction is adding more integration layers with more parts of the larger Rust crates ecosystem. For example, adding functional reactive programming-style layers via the futures-signals crate which is also used by the dominator framework.

Events

One of the active bits of design work going on in Gloo right now is how to craft our event targets and listeners layer. Events are used across most of the Web APIs, so it is very important we get this design right, as it will sit underneath many of our other crates. While we haven’t 100% nailed down the design yet, I really like where we are headed.

On top of web_sys::Event and web_sys::EventTarget::add_event_listener_with_callback, we are building a layer for adding and removing event listeners and managing their lifetimes from Rust via RAII-style automatic cleanup upon drop.

We can use this API to make idiomatic Rust types that attach event listeners that automatically get removed from the DOM when the type is dropped:

use futures::sync::oneshot;
use gloo::events::EventListener;

// A prompt for the user.
pub struct Prompt {
    receiver: oneshot::Receiver<String>,

    // Automatically removed from the DOM on drop!
    listener: EventListener,
}

impl Prompt {
    pub fn new() -> Prompt {
        // Create an `<input>` to prompt the user for something and attach it to the DOM.
        let input: web_sys::HtmlInputElement = unimplemented!();

        // Create a oneshot channel for sending/receiving the user's input.
        let (sender, receiver) = oneshot::channel();

        // Attach an event listener to the input element.
        let listener = EventListener::new(&input, "input", move |_event: &web_sys::Event| {
            // Get the input element's value.
            let value = input.value();

            // Send the input value over the oneshot channel.
            sender.send(value)
                .expect_throw(
                    "receiver should not be dropped without first removing DOM listener"
                );
        });

        Prompt {
            receiver,
            listener,
        }
    }
}

// A `Prompt` is also a future, that resolves after the user input!
impl Future for Prompt {
    type Item = String;
    type Error = ();

    fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
        self.receiver
            .poll()
            .map_err(|_| {
                unreachable!(
                    "we don't drop the sender without either sending a value or dropping the whole Prompt"
                )
            })
    }
}

On top of that layer, we are using Rust’s trait system to design a higher-level, static events API that will make the events casting safe and statically-checked, and make sure you don’t have typos in the event types that you listen to:

use gloo::events::{ClickEvent, on};

// Get an event target from somewhere.
let target: web_sys::EventTarget = unimplemented!();

// Listen to the "click" event, know that you didn't misspell the event as
// "clik", and also get a nicer event type!
let click_listener = on(&target, move |e: &ClickEvent| {
    // The `ClickEvent` type has nice getters for the `MouseEvent` that
    // `"click"` events are guaranteed to yield. No need to dynamically cast
    // an `Event` to a `MouseEvent`.
    let (x, y) = event.mouse_position();

    // ...
});

These event APIs are still works in progress and have some kinks to work out, but I’m very excited for them, and we hope to get a lot of mileage out of them as we build other Gloo crates that internally use them.

Get Involved!

Let’s build Gloo together! Want to get involved?