Introducing Smithy — A WebAssembly framework for Rust

I’m extremely excited to announce the 0.0.2 release of Smithy, a web development framework for Rust! While it is a very pre-alpha version, it should be functional enough for others to start playing around with. Please, get your feet wet and provide feedback.

In this post, we’ll make the case for Smithy and take a whirlwind tour of its internal workings. We’ll learn how Smithy creates components and manages their lifecycles, before diving into some advanced topics and gotchas.

What is Smithy?

Smithy is a framework for writing WebAssembly applications entirely in Rust. Its goal is to allow you to do so using ergonomic, idiomatic Rust, without giving up any of the compiler’s safety guarantees.

You can see an example site, written entirely in Rust, at: https://smithy-todolist.robertbalicki.com. You can see the source code here: https://github.com/rbalicki2/smithy_todolist

Please also see this README to learn how to build and deploy your first Smithy site.

A simple example

A basic click counter is as follows:

#[wasm_bindgen]
pub fn start(div_id: String) {
let doc = web_sys::window().unwrap().document().unwrap();
let root_element: web_sys::Element =
doc.get_element_by_id(&div_id).unwrap();
  let mut count: u32 = 0;
let app = smithy::smd!(
<div on_click={|_| count = count + 1}>
Click count: <b>{count}</b>
</div>
);
smithy::mount(Box::new(app), root_element);
}

That’s it?

Yup! That’s all the code it takes to write a simple Smithy app!

Check out this readme for detailed instructions on how to run your Smithy app locally.

The important parts are:

  • You exported a public function that was annotated with #[wasm_bidgen]
  • You created a smithy app using the smd! macro
  • You passed this app to smithy::mount

Whirlwind tour

Wow! That’s simple. How does the smd! work under the hood?

At it’s simplest, the smd! macro returns a Box<FnMut(Phase) -> PhaseResult that can execute the app's behavior at various times, called phases. The most important of these are the rendering and event handling phases.

If we expand the above macro (by passing smithy the "smd-logs" feature), we see that it expands into:

let app = smithy::types::SmithyComponent(Box::new(move |phase| match phase {
smithy::types::Phase::Rendering => {
/* return a representation of the rendered DOM */
},
smithy::types::Phase::UiEventHandling(ui_event_handling) => {
/* increment count */
},
/* other phases */
}));

(This is a simplified representation, of course.)

What does smithy::mount do with app?

Once you pass your app to smithy::mount, the framework takes over. It keeps track of which phase you are in, and executes the appropriate code.

pub enum Phase<'a> {
Rendering,
RefAssignment(Vec<usize>),
PostRendering,
UiEventHandling((&'a crate::UiEvent, &'a Path)),
WindowEventHandling(&'a crate::WindowEvent),
}

When Smithy enters a particular phase, app(phase) is called. The phases are as follows:

  • Rendering: First, Smithy will ask the app for the "virtual DOM" representing how that app would like to be rendered. Smithy then writes that to the DOM.
  • RefAssignment: Next, Smithy will take any ref's passed to dom nodes (as ref={ref /* &mut Option<web_sys::HtmlElement> */}) and assign them.
  • PostRendering: Next, Smithy executes any code that must happen after the DOM has been updated, or after you have a reference to the actual DOM node. Examples include modifying the value of an input in response to changes in state.
  • At this point, Smithy waits for events. Smithy can respond to DOM events (such as clicks, in the UiEventHandlingphase) and window events (such as hash change events, in the WindowEventHandling phase.) After an event occurs, Smithy will re-render the app.

The smd! macro is nothing but a way of re-arranging your code into the above five phases. For example:

let div_ref: Option<web_sys::HtmlElement> = None;
let app = smd!(
on_hash_change={|hash_change_event| {
// this on_hash_change event handler is used in the
// WindowEventHandling phase
}};
post_render={|| {
// this is executed during the PostRendering phase
}};
// the following two lines get used during the Rendering phase
<div
class="some-class"
on_click={|mouse_event| {
// this on_click handles is used in the UiEventHandling phase
}}
// The following line is used in the RefAssignment phase
ref={div_ref}
/>
)
Mutable and immutable references
Notice that in the original example (<div on_click={|_| count = count + 1}>{ count }</div>), there was a mutable reference to count (in |_| count = count + 1) and an immutable reference ({count}). This would normally not be allowed by the compiler.
However, when these statements are re-arranged by the smd! macro, they end up in separate match arms. Thus, we never run afoul of the borrow checker.

Conclusion

There you have it! That’s a very brief explanation of how Smithy works under the hood. Go try it out!

Advanced topics

What is valid smd! syntax?

Here is an smd! macro invocation that shows off many facets of the supported syntax:

smd!(
on_hash_change=|e: web_sys::HashChangeEvent| {
// window events are handled at the beginning of your smd! macro
// and are followed by a semi colon
};
post_render=|| {
// your post_render callback goes in the same place
};
// Afterward, you can have as many dom nodes,
// as much text, and as many
// interpolated values as you'd like.
<div>
// Child nodes are supported
<b>Hello world</b>
// Are are self closing tags
<div />
// Attributes work like you'd expect
<div class="foo" />
</div>
You can include text.
// Interpolating components is simple.
// Just stick whatever you want between
// curly brackets. (You may need to interpolate
// a mutable reference, though)
{ &mut child_component }
// You can also render components in-line.
// They're just regular functions!
{ another_module::render_component() }
// You can interpolate other values, too
{ 42 }
// You can include multiple statements inside the curlies, too!
{
let message = "Hi there!";
message
}
<non standard up elements are allowed too />
)

Why does the smd! macro create a Box<FnMut> instead of a struct?

Imagine if the above had turned into a struct. A fictionalized and simplified version of this could be:

let app = StructComponent(DomElement::Node {
node_type: "div",
event_handlers: EventHandlers { on_click: |_| count = count + 1 },
children: vec![
DomElement::String(count.to_string()),
]
}

Nice and simple, right?

But notice that in this struct, we have both a mutable reference to count (via the on_click event_handler) and an immutable reference (as DomElement::String(count.to_string())). The compiler will prevent this from being compiled.

Early attempts at Smithy behaved like this.

How are Dom elements represented?

When app(phase::Rendering) is called, a PhaseResult::Rendering(Node) is returned.

pub enum Node {
Dom(HtmlToken),
Text(String),
Vec(Vec<Node>),
Comment(Option<String>),
}
pub struct HtmlToken {
pub node_type: String,
pub children: Vec<Node>,
pub attributes: Attributes,
}

This Node is then turned into a Vec<CollapsedNode>, which is a similar struct, but with all adjacent String nodes concatenated and which has no Node::Vec elements.

How does Smithy know what to update? Does it re-render the entire DOM tree?

No. Smithy will calculate the diff between the Vec<CollapsedNode>'s from the last and current rendering phases, and only update the DOM where it has actually changed.

How does interpolation work?

Interpolation is when you include another value in curly brackets. For example:

let coolest_spaceship = "x-wing";
smd!({ coolest_spaceship })

Anything: other components, values, function calls, etc. can be interpolated in this way, as long as the return value implements smithy::Component.

Many common types have smithy::Component implemented for them already.

What gotchas are there with interpolation?

The smd! macro cannot look within a set of curly brackets to discover what type of item we are interpolating. As far as smd! is concerned, { plane } is a black box. Since it doesn't know whether it's a component (which can respond to events) or a simple value (which usually does not), { plane } will be repeated many more times in the expanded macro.

Thus, in the interest of file size, it may be better to include less text in an interpolation, when possible.

Better:

let plane = match model {
// many more lines...
};
smd!({ plane })

Worse:

smd!({
let plane = match model {
// many more lines...
};
plane
})

More interpolation gotchas

The following will compile:

fn outer<'a>() -> SmithyComponent<'a> {
let mut count = 0;
smd!({ inner(count) })
}
fn inner<'a>(&'a mut count: i32) -> SmithyComponent<'a> {
smd!(<div on_click={|_| count = count + 1}>Increase count</div>)
}

As will the following:

fn outer<'a>() -> SmithyComponent<'a> {
let mut count = 0;
smd!(
{ count }
<div on_click={|_| count = count + 1}>
Increase count
</div>
)
}

But, the following will not compile:

fn outer<'a>() -> SmithyComponent<'a> {
let mut count = 0;
smd!({ count }{ inner(&mut count) })
}
fn inner<'a>(count: &'a mut i32) -> SmithyComponent<'a> {
smd!(<div on_click={|_| *count = *count + 1}>Increase count</div>)
}

The compiler will complain that you cannot move out of captured variable in an FnMut closure. What gives? If the first two compiled, so should the third!

However, the smd! macro cannot split the interpolated value ({ inner(&mut count )}) by phase. Thus, it will need to be evaluated in the Rendering phase, along with { count }, and referenced simultaneously in the same Node. This violates the borrow checker's rules.

Okay, so what do I do?

In situations like that, I would recommend wrapping the data in an Rc<RefCell<T>>, as in the following:

fn outer<'a>() -> SmithyComponent<'a> {
let count = 0;
let count = Rc::new(RefCell::new(count));
let count_2 = count.clone();
smd!({ *count.borrow() }{ inner(&count_2) })
}
fn inner<'a>(count: &'a Rc<RefCell<usize>>) -> SmithyComponent<'a> {
smd!(<div on_click={|_| {}}>Increase count</div>)
}

This is a huge usability concern and I hope to fix this in a future version of Smithy!

How do I call child components?

Components are interpolated either as values:

let child_component = render_child_component(params);
smd!(<div>{ &mut child_component }</div>)

or by calling them in the smd! macro:

smd!(<div>{ render_child_component(params) }</div>)

What are some gotchas when using child components?

There is a subtle error in the following code:

fn main() -> impl Component {
smd!(<div>{ render_child_component() }</div>)
}
fn render_child_component() -> impl Component {
let input_ref: Option<web_sys::HtmlElement> = None;
smd!(
post_render={|| {
if Some(el) = input_ref {
// N.B. this will never be executed!
}
}};
<input
ref={input_ref}
/>
)
}

The error is that render_child_component is called every phase change. Thus, input_ref is set to None at the beginning of the RefAssignment phase, and again at the beginning of the PostRendering phase. Thus, the inside of the if Some(el) = input_ref block will never be called!

The solution to this is to store the ref at a higher level and pass it in as a parameter to render_child_component.

What about futures?

You can transform a future into an UnwrappedPromise (see for example, this). An UnwrappedPromise will then notify smithy to re-render when the future completes or errors out. UnwrappedPromise's can be used in a match statement, as well.

Does Smithy compile on stable Rust?

No, but I would love for it to be able to one day. There are a few proc-macro-related features that would need to stabilize, first.

I’m currently developing Smithy on rustc 1.32.0-nightly (6f93e93af 2018-11-14).

I want to be involved!

I’d love to hear from you! Tweet at me @statisticsftw!