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.
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.
Before you start setting up your environment, make sure you have the following installed on your computer.
wasm-pack
CLIWhy 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.
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.
wasm-pack new hello-wasm cd hello-wasm
The generated project is essentially a Rust library with 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
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 -m http.server 8000
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.
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:
wasm-bindgen
, which generates bindings and glue code between Wasm and JS.web-sys
, which provides bindings for web APIs.js-sys
, which provides bindings for JavaScript’s standard, built-in objects, including their methods and properties.wasm_bindgen_futures
, which facilitates conversion between JavaScript promises to Rust futures.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>
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.
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) }
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:
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.
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.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the 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 and mobile apps, recording literally everything that happens on your Rust application. 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 — start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
4 Replies to "Getting started with WebAssembly and Rust"
Great tutorial to get started as well as good links to understand the details even better. Many thanks to the author!
Thanks for posting this, I was able to get my wasm code running in a few minutes! Last time I tried to get wasm working, I spent a million years trying to figure out how webpack works and then for some reason upgrading from webpack 4 to webpack 5 or something terrible like that.
Is it possible to use webpack 5 with wasm-bindgen now? If so, I’d love to see an example of that configuration.
Looks like I *tried* to upgrade to webpack 5 but ultimately failed. So, I don’t know if it’s actually possible or not.