Carlos Chacin Learning in my free time (https://github.com/Hermitter). I make tools, guides, documentation, and live tutorials.

Getting started with WebAssembly and Rust

8 min read 2518

Getting Started With WebAssembly and Rust

I initially picked up Rust because of the fantastic work the team has done to support and push WebAssembly. The official documentation is a great resource for building an example project.

This guide will serve as an introduction to WebAssembly and a tutorial on how to set up and work in a Rust Wasm environment.

To follow along, you should have a basic understanding of Rust and web development in general.

Understanding WebAssembly

WebAssembly is a binary instruction format that most browsers support. It enables languages such as Rust, C, C++ Go, etc., to be compiled and run in a web browser.

Wasm is not a replacement for JavaScript. Think of it as a way to offload computationally heavy tasks to a more suitable language. It enables you to port existing projects and libraries to the web without rewriting them in JS, improving performance as a result.

If you’re interested in taking the Wasm specification outside the web, the WebAssembly System Interface (WASI) may be of interest to you.

Downloading the tools

Before you start setting up your environment, make sure you have the following installed on your computer.

  • Rust (ideally the most up-to-date version)
  • A modern web browser (Internet Explorer doesn’t count)
  • Your favorite text editor
  • A way to host a simple web server to avoid the browser nagging about cross-origin requests being blocked; if you have Python installed, there’s a nifty command for this
  • wasm-pack CLI

Why do you need wasm-pack? According to Mozilla, “wasm-pack is a tool for assembling and packaging Rust crates that target WebAssembly. These packages can be published to the npm Registry and used alongside other packages. This means you can use them side-by-side with JS and other packages, and in many kinds of applications.”

Rust crates are similar to packages and libraries for other languages. Crates work directly with Rust’s build system and package manager, Cargo.

Creating and building a Wasm package

We’ll use the wasm-pack CLI to create a new Wasm project. This should be familiar to you if you’ve ever created a Rust project via the cargo CLI.

We made a custom demo for .
No really. Click here to check it out.

wasm-pack new hello-wasm
cd hello-wasm

The generated project is essentially a Rust library with boilerplate Wasm code.

Rust Library Boilerplate WASM Code

Looking at the commands available in wasm-pack, it’s clear that there’s a focus on creating and publishing npm packages.

WASM-PACK SUBCOMMANDS:
    build      πŸ—οΈ  build your npm package!
    help       Prints this message or the help of the given subcommand(s)
    login      πŸ‘€  Add an npm registry user account! (aliases: adduser, add-user)
    new        πŸ‘ create a new project with a template
    pack       🍱  create a tar of your npm package but don't publish!
    publish    πŸŽ†  pack up your npm package and publish!
    test       πŸ‘©β€πŸ”¬  test your wasm!

While this is great for projects that have bundlers (Webpack, Rollup, etc.), we’re aiming for a simple setup where we can import our Wasm binary in an HTML file. Thankfully, wasm-pack‘s build has an argument to target other environments.

-t, --target <target>        Sets the target environment. [possible values: bundler, nodejs, web, no-modules]
                                [default: bundler]

The web target is exactly what we want! Once built, the output will contain multiple files that serve as the glue code between Wasm and JS. By the end of it, our Wasm binary will be exposed through a JavaScript module.

wasm-pack build --target web

Running Wasm in the browser

With the Rust code compiled to Wasm, we can now call it from JavaScript.

Create a file called index.html in the root of the project and add the following.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My Wasm Project</title>
  </head>

  <body>
    <script type="module">
      // Importing WASM as a JS module requires us to call an init function provided by the default export.
      // This is planned to be changed in the future.
      import { default as wasm, greet } from "./pkg/hello_wasm.js";

      wasm().then((module) => {
        // The boiler plate project comes with a `greet` function that calls:
        // `alert("Hello, hello-wasm!");`
        greet();
      });
    </script>
  </body>
</html>

For security reasons, browsers often don’t allow you to import local resources from file://. This prevents us from importing the Wasm binary, but a simple solution is to start a quick web server.

If you have Python installed, you can use one of the following commands.

  • Python 3: python -m http.server 8000
  • Python 2: python -m SimpleHTTPServer 8000

We now have Rust code ready to run in a browser. Visit http://localhost:8000 to see it live. It should be a blank page with an alert.

WASM Project Running in Browser

Rust and JavaScript interop

For the following code examples, you can edit the index.html and src/lib.rs files to play around with the new changes. Remember to build each time you edit your Rust code.

Now that you know how to build a project, it’s time to learn how to work with it. By default, Rust can’t directly communicate with JavaScript or web APIs. This functionality is enabled through wasm-bindgen. The project itself consists of multiple crates and a CLI tool. We’ve actually been using its CLI through wasm-pack.

Crates from wasm-bindgen include:

Exporting common types

By importing wasm_bindgen::prelude::*, we’re given powerful abstractions for talking to JS. Here are some I want to highlight:

  • #[wasm_bindgen]: Macro that automatically handles most of our bindings between Rust and JS.
  • JsValue: A representation for a data owned by JS.
  • UnwrapThrowExt: A trait extension for Option<T> and Result<T, E>.

Assuming you’re editing src/lib.rs, the examples below show how to receive and return values with Wasm. Note that styling variables with an underscore (such as _c) is a Rust convention expressing that it’s unused.

use wasm_bindgen::prelude::*;

// JS doesn't have a chars type which means:
// - The _c argument is the first char of a JS string.
// - The char returned will be a JS string.
#[wasm_bindgen]
pub fn char_example(_c: char) -> char {
    'πŸš€'
}

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

// str cannot be used as a return type.
// This is because we can't return borrowed references with the wasm_bindgen macro.
#[wasm_bindgen]
pub fn str_example(s: &str) -> String {
    format!("Hello {}", s)
}

#[wasm_bindgen]
pub fn number_example(n: i32) -> i32 { // assume the same for u32, usize, etc.
    n+100
}

#[wasm_bindgen]
pub fn bool_example(_b: bool) -> bool {
    true
}

// `Box<[JsValue]>` are the representation for a JS array object.
// When it comes to Js Arrays:
// - They are iterable.
// - Can contain multiple types by being of type JsValue (strictly typed arrays exist for numbers).
// - Don't really support N-dimensional arrays and are expensive to work with.
#[wasm_bindgen]
pub fn mixed_array_example(array: Box<[JsValue]>) -> Box<[JsValue]> {
    for value in array.iter() {
        // compute things...
    }

    vec![
        "Hello".into(),
        512.into(),
        JsValue::NULL,
        JsValue::UNDEFINED,
        61.20.into(),
    ]
    .into_boxed_slice()
}

// Typed arrays are only available for number types.
// For example, the function below will return a JS Int32Array type.
#[wasm_bindgen]
pub fn typed_array_example(_array: Box<[i32]>) -> Box<[i32]> {
    vec![1, 2, 3, 4, 5, 6, 7].into_boxed_slice()
}

// When it comes to Option:
// - Some returns the value inside.
// - None returns a JS undefined.
#[wasm_bindgen(catch)]
pub fn option_example() -> Option<i32> {
    None
}

// When it comes to Result
// - Result<T, JsValue> is the only supported signature. T must be convertible to a JsValue.
// - #[wasm_bindgen(catch)] must be used when returning a result.
// - Err will be equivalent to a JS thrown error.
// - Ok will return the value inside.
#[wasm_bindgen]
pub fn result_example() -> Result<i32, JsValue> {
    // With the wasm prelude imported, we can convert most common types by calling .into()
    Err("Look Pa, I'm throwing a JS error!".into())
}

Once the project is built again, these functions can be directly used in our web page. Feel free to play around and print the values returned.

<script type="module">
  import * as wasm from "./pkg/hello_wasm.js";

  wasm.default().then((module) => {
    wasm.char_example("R");
    wasm.string_example("Jane");
    wasm.str_example("John");
    wasm.number_example(28);
    wasm.mixed_array_example([2, null, undefined, "Hola", true]);
    wasm.typed_array_example([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
    wasm.option_example();
    wasm.result_example();
  });
</script>

Exporting structs and enums

Enums and structs are fairly simple exports. Although there are some restrictions, the syntax should still feel like day-to-day Rust.

use wasm_bindgen::prelude::*;

// When it comes to Enums:
// - They  are C styled.
// - JS represents them through an object with a number for each variant.
#[wasm_bindgen]
pub enum ExampleEnum {
    Yes,
    No,
}

#[wasm_bindgen]
pub fn verify_enum_choice(choice: ExampleEnum) -> bool {
    match choice {
        ExampleEnum::Yes => true,
        ExampleEnum::No => false,
    }
}

// When it comes to Structs:
// - Cannot contain lifetimes or type parameters.
// - Each field value must impl the Copy trait.
#[wasm_bindgen]
pub struct ExampleStruct {
    pub value: i32,
}

// For struct impl, we have the option for struct methods and type-level functions.
// JS handles structs by creating a JS object with a pointer (i.o.w. we can use references!).
#[wasm_bindgen]
impl ExampleStruct {
    pub fn new(value: i32) -> ExampleStruct {
        ExampleStruct { value }
    }

    pub fn read_method(&self) -> i32 {
        self.value
    }

    pub fn write_method(&mut self, value: i32) {
        self.value = value;
    }

    pub fn transfer_ownership(self) -> ExampleStruct {
        self
    }
}

Now we can import the struct and enum directly into our JS code. Once a struct is instantiated, we can call its Wasm-compatible methods.

<script type="module">
  import * as wasm from "./pkg/hello_wasm.js";

  wasm.default().then((module) => {
    // Enum \\
    let rustEnum = wasm.ExampleEnum.Yes;
    console.log(wasm.verify_enum_choice(rustEnum));

    // Struct \\
    let rustStruct = wasm.ExampleStruct.new(6);
    console.log(rustStruct); // this will contain a pointer
    console.log(rustStruct.value);

    rustStruct.write_method(200);
    console.log(rustStruct.read_method());

    let newRustStruct = rustStruct.transfer_ownership();
    rustStruct.read_method(); // rustStruct is now invalid. An error will throw.
  });
</script>

Below is an output image of the script above to demonstrate how these types work. Notice how the enums and structs are represented in JS.

Structs and Enums Represented in JavaScript

Manually creating bindings

The wasm-bindgen crate provides powerful tooling for talking to JS. However, there are no direct bindings for the JS standard or web APIs. That’s where web-sys and js-sys come in. Both crates have ready-made bindings that provide a standard interface to work with.

Of course, these crates don’t cover everything, so we need to create our own bindings for anything that’s missing. A common example is wrapping over custom JS classes and functions. When you first opened src/lib.rs, you might’ve noticed a binding was created for the alert() function.

Before we walk through how to create our own custom bindings, let’s look over the JS code for which we want Rust to bind.

<!--
  For simplicity, we'll add a class declaration to the global namespace.
  The goal, from rust, is to create this class, call a method, and get/set a property.
-->
<script>
  class Coordinate {
    constructor(x, y) {
      this.x = x;
      this.y = y;
    }

    printValues() {
      return `(x:${this.x} y:${this.y})`;
    }
  }
</script>

<script type="module">
  import * as wasm from "./pkg/hello_wasm.js";

  wasm.default().then((module) => {
    wasm.manual_bindings_example();
  });
</script>

On top of binding the Coordinates class, below are some examples of how to do the same for various JS functions.

use wasm_bindgen::prelude::*;

// Although we're using what's in the global namespace, we can also import from other modules.
// #[wasm_bindgen(module = "./bar")]
// extern "C" {}

// Binding JS involves a bit of boilerplate because we have to specify each name
// and signature to bind.
#[wasm_bindgen]
extern "C" {
    // Bindings must be named as their JS equivalent
    fn alert(s: &str);

    // A different name can be specified as long as the original name is passed to the macro.
    #[wasm_bindgen(js_name = prompt)]
    fn ask(s: &str) -> String;

    // Methods can be from any js namespace.
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);

    // Using a different name allows us to specify various signatures.
    #[wasm_bindgen(js_namespace = console, js_name = log)]
    fn log_num(n: i32);

    //* JS Class example *\\
    // The process is a little verbose because create a binding for
    // each part of the class we want (class name, constructor, methods, setters, getters).
    type Coordinate;

    #[wasm_bindgen(constructor)]
    fn new(x: i32, y: i32) -> Coordinate;

    // methods must match the naming in the class declaration.
    #[wasm_bindgen(method)]
    fn printValues(this: &Coordinate) -> String;

    // getters are named as the property we want.
    #[wasm_bindgen(getter, method)]
    fn x(this: &Coordinate) -> i32;

    // setters are named the same as getters but with a `set_` prefix.
    #[wasm_bindgen(setter, method)]
    fn set_x(this: &Coordinate, x: i32);
}

#[wasm_bindgen]
pub fn manual_bindings_example() {
    alert("Hey buddy!");
    log(&ask("Tell me about your day!"));

    let coordinates = Coordinate::new(-4, 15);
    log_num(coordinates.x()); // prints -4

    coordinates.set_x(coordinates.x() * 2);
    log(&coordinates.printValues()); // prints (-8, 15)
}

Custom Bindings

Interacting with web APIs

The majority of essential web APIs are provided and documented by the web-sys crate. After specifying what APIs we want, we can do lots of things, including:

  • Edit HTML elements
  • Add event listeners
  • Print to the web console
  • Draw on a canvas

Since the list is pretty extensive, we’ll just focus on how to set up and use this create.

Add the following to the project’s Cargo.toml and then specify each web API you want to use.

[dependencies]
web-sys = { version = "0.3.39", features = ['console'] }

use wasm_bindgen::prelude::*;
use web_sys::console;

#[wasm_bindgen]
pub fn print_things() {
  // console has multiple log_x functions that represent how many items are being printed.
  // log_x takes in a reference to a JsValue so we need to convert the values we want to print.
  console::log_1(&"Printing from Rust!!".into());
  console::log_2(&"Numbers: ".into(), &1234.into());
}

To learn more, check out the following web-sys resources.

It’s also worth researching the js-sys crate if you find you need to import types and functions from standard JS.

Closing thoughts

Although we couldn’t possibly cover everything in a single blog post, I hope this guide will help you jump-start your journey into WebAssembly. I highly encourage going though the wasm-bindgen docs to explore more in-depth examples and important topics, such as transmitting arbitrary data between JavaScript and Rust.

Keep in mind that the Rust WebAssembly ecosystem is still growing and it’s only going to get better from here.

LogRocket: Full visibility into production Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps β€” .

Carlos Chacin Learning in my free time (https://github.com/Hermitter). I make tools, guides, documentation, and live tutorials.

Leave a Reply