/rust-wasm-workshop

An introduction to WebAssembly with Rust

Primary LanguageJavaScript

WebAssembly with Rust

Note: The formatting of this document (expandable solutions, internal links, etc.) has only been verified on GitHub. (Sorry).

Setup

You need:

Troubleshooting

Resources

In the Rust community there tends to be "books" for everything. So before we get started I'll mention a selection of useful resources depending on which part you want to tinker with:

Let's get started

To begin with, just clone this repository. Or if you prefer, set it up from scratch. Now, try to build and run it:

cd mycrate_web
npm install
npm start -- --open

A not very exciting web page should pop up in your web browser. Congratulations! You have built your first WebAssembly-containing web page!

What am I looking at?

The app is split into three parts:

mycrate_core

mycrate_core is just a completely normal Rust library, with no adaptations for WebAssembly. It could be published to crates.io, used in a command line application or a web server. Anything you would use Rust without WebAssembly for. If you already have a Rust library you would like to use in a web page you might slot it in here.

This module is here primarily to show that you can adapt your code to WebAssembly while keeping your existing code completely general.

mycrate_wasm

mycrate_wasm imports mycrate_core and adapts the interface for use with WebAssembly. For the simple, early examples you probably don't want to wrap and adapt all that much. So since hot reloading is slightly more stable in this folder, you may want to just make your edits here and ignore mycrate_core until you have something to wrap/adapt.

When we build, the code in mycrate_wasm/src is compiled into a WebAssembly module, and packaged into an npm-package in mycrate_wasm/pkg by wasm-pack.

Have a look at the files in mycrate_wasm/pkg. In particular, you have:

  • The actual .wasm-file, index_bg.wasm
  • A JS wrapper, index.js, which is what you will actually import. Right now it is pretty simple, but it can get complex pretty fast.
  • TypeScript declarations, index.d.ts.
  • A package.json file. The metadata here is taken from mycrate_wasm/Cargo.toml, so if you actually want to publish the generated NPM package, you should edit the Cargo.toml accordingly.

mycrate_web

This is our actual web app / page. It imports the npm-package from mycrate_wasm/pkg.

If you look at mycrate_web/src/index.js you can see it makes a call to a function in the WebAssembly module and logs the result to the console. Open the console for our page in your browser and make sure the result is logged.

The WebAssembly module has to be loaded asynchronously, with an import(...)-expression. Since it is actually a completely different file format, there is no way to bundle the whole thing together. So in this simple example, our code is wrapped in an async function so that we can just say await import(...).

Your first edit

While building and running WebAssembly is cool and all, we should probably start doing some actual editing.

As a start, I think our WebAssembly code should be slightly better than our normal Rust code. Exactly 1 better in fact.

Edit the add function in mycrate_wasm so that it adds 1 to the result. Make sure the number in the console is now 1 bigger.

Hello Bob!

Now, let's make the form on the page do something!

1: Add a function create_greeting to mycrate_wasm which when called from JS creates a greeting. E.g. create_greeting("Bob") -> "Hello Bob". (Hint: format!)

2: Enable the button once the WebAssembly is ready, and when the button is clicked create a greeting with the value of the input field, and show it in an alert(...). (Hint: document.querySelector, onclick)

Expand to see a possible solution

In mycrate_wasm/src/lib.rs

#[wasm_bindgen]
pub fn create_greeting(name: String) -> String {
    format!("Hello {}", name)
}

(Not ideal Rust, but let's ignore that for now).

In mycrate_web/src/index.js

  const {create_greeting} = await import("../../mycrate_wasm/pkg");

  const button = document.querySelector("button");
  const input = document.querySelector("input");
  button.onclick = () => alert(create_greeting(input.value));
  button.disabled = false;

Calling JS from Rust

Now, as great as that was, perhaps we should do the alert call inside the Rust as well?

We can do this by declaring an external function and letting wasm_bindgen bind it for us.

#[wasm_bindgen]
extern {
    fn alert(msg: String);
}
A note for those who already know Rust

alert can also be declared to take a string slice, like this:

#[wasm_bindgen]
extern "C" {
    fn alert(s: &str);
}
Expand to see a possible solution
// ...
#[wasm_bindgen]
extern {
    fn alert(msg: String);
}

#[wasm_bindgen]
pub fn greet(name: String) {
    alert(create_greeting(name))
}
  const {greet} = await import("../../mycrate_wasm/pkg");
  // ...
  button.onclick = () => greet(input.value));

Note. Jumping between JS and WebAssembly is actually pretty expensive, relatively speaking, so it is not something you want to do in a performance-critical part of your code.

Deploying the application

So, you have a working application and want to show it to the world.

  1. Go to the mycrate_web folder
  2. Run npm run build
  3. See that dist folder? Those are just static files you can deploy to any old hosting provider (like Firebase or Netlify).
  4. You are done. No backend or anything like that. Just share that URL.

Now what?

Where to go from here depends a lot on what you know already, and what you want to do.

I don't know any Rust

Unfortunately, writing WebAssembly with Rust adds a layer of complexity (albeit small) on top of writing plain Rust. So to begin with, I would suggest trying at least the guessing game tutorial in the Rust book.

I already know a little Rust

Maybe you should try to implement the guessing game in WebAssembly?

If you want to follow a longer guide the rustwasm Game of Life tutorial covers a lot of ground, including testing and debugging.

I already have my own Rust code that I want to use in a JS app, publish as a library to NPM or create an interface for

As mentioned earlier, mycrate_core could be replaced by any Rust crate, provided it does not rely on any non-Rust code or OS APIs.

However, if your crate does depend on something that is not compatible with WebAssembly, don't give up yet. You may still be able to adapt it through the use of feature flags / conditional compilation.

This particular repository is stripped down a bit to make it easy to understand and easy to expand. So if you want to build a real, production-ready project there are some adaptions you may want to make.

For some examples I can answers questions about I have my own two WebAssembly projects: json_typegen and svg-halftone.

I want to create a web app entirely in Rust

You may want to have a look at Yew, a framework similar to React and Elm which can be used to write applications entirely in Rust.

Hmm, I'm still not sure...

The aforementioned wasm-bindgen book has a lot of interesting examples to draw inspiration from.

How about drawing directly to a canvas, from Rust? Or even loading a WebAssembly module from WebAssembly?

Write WebAssembly? Nah, I want to run WebAssembly

Sure, thing. Have a look at the WebAssembly spec. (Also of interest to those who just like some heavy programming language theory) and have a look at this list of existing engines.