Please note: This text is outdated now as wasm support in Rust has made big improvements since this was written. Please see my newer post about setting up a Rust server / frontend project.
This is a very short walk-through to get a Wasm Rust project up and running. No intention is put on giving an in-depth overview or a thorough list of references. The best more detailed guide is the Rust and WebAssembly “book”, in particular the Game of Life tutorial. Helpful is also the wasm-bindgen documentation.
I played around with various setup options and for the time being I liked this one as this integrates with webpack, provides a life reload workflow and makes JS / Rust interop easy.
This has been tested on 2021-02-15 with macOS 11.1 and Rust stable 1.50. I will likely update this over time.
Prerequisites
The setup uses wasm-pack and the webpack wasm-pack-plugin via node.js. In order to follow the steps below you will need:
- Wasm-pack, to install: https://rustwasm.github.io/wasm-pack/installer/
- Node.js, to install either use the official download, your favorite package manager, or nvm.
Creating the project
The source code of the hello-wasm project assembled here can be found on github.
- Create the project folder:
cargo new hello-wasm --lib
- Your
Cargo.toml
should look like
[package]
name = "hello-wasm"
version = "0.1.0"
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
console_error_panic_hook = "0.1.6" # log panics
console_log = "0.2.0" # connects log to the js console
js-sys = "0.3.47" # bindings to js default objects
log = "0.4.14" # logging
wasm-bindgen = "0.2.69" # js - rust interop
# bindings to DOM objects
[dependencies.web-sys]
version = "0.3.47"
features = ['console', 'Window', 'Document', 'Element', 'HtmlElement']
- Create a subdirectory
static/
that will contain files that serve as the entry point to webpack and to your app. - Create
static/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
</head>
<body>
<div id="app"></div>
</body>
</html>
- Create
static/index.js
. This file will eventually load files from thestatic/wasm/
subdirectory which is where the compiled wasm code will be placed into.
import("./wasm").catch(console.error);
- Crate a
webpack.config.js
file in the project root directory
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const webpack = require("webpack");
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
module.exports = {
entry: "./index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "index.js",
},
plugins: [
new HtmlWebpackPlugin({ template: "index.html", title: "hello-wasm" }),
new WasmPackPlugin({ crateDirectory: path.resolve(__dirname, "."), outDir: "static/wasm" }),
// needed for MS Edge support
new webpack.ProvidePlugin({
TextDecoder: ["text-encoding", "TextDecoder"],
TextEncoder: ["text-encoding", "TextEncoder"],
}),
],
};
- Create a
package.json
file
{
"scripts": {
"build": "webpack --mode production --context static",
"serve": "webpack-dev-server --host 0.0.0.0 --context static",
"clean": "npx rimraf target static/wasm"
}
}
- Install the npm dependencies. Note that because of some SNAFU we need an older version of webpack right now. JavaScript churn in action.
$ npm install --save-dev \
@wasm-tool/wasm-pack-plugin \
html-webpack-plugin@4 \
text-encoding \
webpack@4 \
webpack-cli@3 \
webpack-dev-server
- Lastly, let’s use some Rust code that will use the DOM bindings to modify an element:
use log::{info, Level};
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{Document, HtmlElement, Window};
pub fn window() -> Window {
web_sys::window().expect("no global `window` exists")
}
pub fn document() -> Document {
window().document().expect("no global `document` exists")
}
#[wasm_bindgen(start)]
pub fn main() -> Result<(), JsValue> {
console_error_panic_hook::set_once();
console_log::init_with_level(Level::Debug).expect("init logger");
info!("Up and running");
let app = document()
.query_selector("#app")
.expect("querySelector call")
.expect("cannot find app element")
.dyn_into::<HtmlElement>().expect("app HtmlElement cast");
app.set_inner_text("Hello from Rust!");
Ok(())
}
The DOM bindings are provided by the web-sys crate. Note that there is a large set of features that provide control over what DOM bindings will be provided. Compare that to the features added to Cargo.toml
in step 2.
- Run the development server:
npm run serve
. At localhost:8080 you should now see:

When modifying the Rust code you should immediately see the page reload. This allows for a very nice development flow.
- To package static files for “production” run
npm run build
. This will compile the Rust code in release mode and minify the JavaScript. It produces files in thedist/
subdirectory. You can take these and server some from a HTTP server like nginx.