Let's look at how Wasm + Rust unlocks really exciting potential for the future of frontend web development. It's not a replacement for JavaScript — it enhances and complements it.
This article assumes the following:
According to the official documentation, WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable target for compilation of high-level languages like C/C++/Rust, enabling deployment on the web for client and server applications.
Let's break this down into simpler grammar. Essentially all that means is that Wasm is:
As many devs will agree, it's way harder to achieve significant milestones when you're coding directly in assembly language, and JavaScript is a pretty great language to write most of the code required by any software we can dream of. So why do we need this WebAssembly to start with?
Now that we're up to speed with what Wasm is, the next is to set up your machine for Wasm development with Rust. We'll work through how to set up the toolchain for compiling Rust programs to WebAssembly and integrate them into JavaScript. After which we'll build a very basic app that asks users to input two numbers they wish to add.
Make sure you have the standard Rust toolchain installed. You can find the installation here.
To proceed, make sure you have Rust 1.30 or newer. Use the command below to check your Rust version.
rustc -V
or
rustc --version
Note: The above command depends on your version.
Next we install wasm-pack
, which is a one-stop shop for building, testing, and publishing Rust-generated WebAssembly. For demonstration purposes, I'll be using a Unix machine (Mac and Linux Distributions). If you are on a Windows machine, you can download an installer for wasm-pack
here.
Run the command below in your terminal and follow the prompt to install it on your machine.
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -s Sf | sh
You should have this output:
Next, we install cargo-generate
, which helps to get up and running quickly with a new Rust project by leveraging a preexisting git repository as a template. Install with this command:
cargo install cargo-generate
Note : If this fails, a typical reason is that pkg-config isn't installed on your machine. You can fix this by running the following command for Linux machines. Use brew if you're on a Mac.
sudo apt install pkg-config
Next up is npm. It is a JavaScript package manager. We will use it to install and run a JavaScript bundler and development server. Follow these instructions to install npm. If you have npm already installed, make sure it's up to date with this command.
npm install npm@latest -g
We'll see how to build our first Rust and WebAssembly program: a Web page that alerts "Hello, World!"
First, we clone a project template which comes pre-configured with the same defaults, so you can quickly build, integrate, and package your code for the web.
Clone the template with this command:
cargo generate --git https://github.com/rustwasm/wasm-pack-tem plate
This should prompt you for the new project's name. We will use rust-calculate.
rust-calculate
Enter into the new project directory
cd rust-calculate
Depending on when you're reading this, its content should look like this:
Let's have a look at some of these files in detail.
rust-calculate/Cargo.toml
The Cargo.toml
file specifies dependencies and metadata for cargo
, Rust's package manager and build tool. This one comes pre-configured with a wasm-bindgen
dependency, a few optional dependencies and the crate-type properly initialized for generating .wasm
libraries.
rust-calculate/src/lib.rs
The src/lib.rs
file is the root of the Rust crate that we are compiling to WebAssembly. It uses wasm-bindgen
to interface with JavaScript. It imports the window.alert
JavaScript function, and exports the greet
Rust function, which alerts a greeting message.
mod utils;
use wasm_bindgen::prelude::*;
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
extern {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
alert("Hello, rust-calculate!");
}
rust-calculate/src/utils.rs
The src/utils.rs
module provides common utilities to make working with Rust compiled to WebAssembly easier.
wasm-pack
is used to orchestrate the following build steps:
wasm32-unknown-unknown
target installed via rustup
..wasm
binary via cargo
.wasm-bindgen
to generate the JavaScript API for using our Rust-generated WebAssembly. To do all of that, run this command inside the project directory:
wasm-pack build
When the build has completed, we can find its artifacts in the pkg
directory, and it should have these contents:
The README.md
file is copied from the main project, but the others are completely new.
rust-calculate/pkg/rust_calculate_bg.wasm
The .wasm file is the WebAssembly binary that is generated by the Rust compiler from our Rust sources. It contains the compiled-to-wasm versions of all of our Rust functions and data. For example, it has an exported "greet" function.
rust-calculate/pkg/rust_calculate.js
The .js
file is generated by wasm-bindgen
and contains JavaScript glue for importing DOM and JavaScript functions into Rust and exposing a nice API to the WebAssembly functions to JavaScript. For example, there is a JavaScript greet
function that wraps the greet
function exported from the WebAssembly module. Right now, this glue isn't doing much, but when we start passing more interesting values back and forth between Wasm and JavaScript, it will help shepherd those values across the boundary.
import * as wasm from './rust_calculate_bg.wasm'
/**
*/
export function greet() {
wasm.greet();
}
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
let cachegetUint8Memory = null;
function getUint8Memory() {
if (cachegetUint8Memory === null || cachegetUint8Memory.buffer !== wasm.memory.buffer) {
cachegetUint8Memory = new Uint8Array(wasm.memory.buffer);
}
return cachegetUint8Memory;
}
function getStringFromWasm(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory().subarray(ptr, ptr + len));
}
export const __wbg_alert_264313fba916676f = function(arg0, arg1) {
alert(getStringFromWasm(arg0, arg1));
};
rust-calculate/pkg/rust_calculate.ts
The .d.ts
file contains TypeScript type declarations for the JavaScript glue. If you are using TypeScript, you can have your calls into WebAssembly functions type checked, and your IDE can provide autocompletions and suggestions! If you aren't using TypeScript, you can safely ignore this file.
/* tslint:disable */
/**
*/
export function greet(): void;
rust-calculate/pkg/package.json
The package.json(https://docs.npmjs.com/files/package.json)
file contains metadata about the generated JavaScript and WebAssembly package. This is used by npm and JavaScript bundlers to determine dependencies across packages, package names, versions, and a bunch of other stuff. It helps us integrate with JavaScript tooling and allows us to publish our package to npm.
{
"name": "rust-calculate",
"collaborators": [
"Obinna Ekwuno <obinnacodes@gmail.com>"
],
"version": "0.1.0",
"files": [
"rust_calculate_bg.wasm",
"rust_calculate.js",
"rust_calculate.d.ts"
],
"module": "rust_calculate.js",
"types": "rust_calculate.d.ts",
"sideEffects": "false"
}
To take our rust-calculate package and use it in a Web page, we use the create-wasm-app JavaScript Project Template .
Run this command inside the rust-calculate project directory:
npm init wasm-app www
The command creates a new rust-calculate/www directory, the contents are:
Once again, let's take a closer look at some of these files.
rust-calculate/www/package.json
This package.json
comes pre-configured with webpack
and webpack-dev-server
dependencies, as well as a dependency on hello-wasm-pack
, which is a version of the initial wasm-pack-template
package that has been published to npm.
rust-calculate/www/weback.config.js
This file configures webpack and its local development server. It comes pre-configured, and you shouldn't have to tweak this at all to get webpack and its local development server working.
rust-calculate/www/index.html
This is the root HTML file for the web page. It doesn't do much other than load bootstrap.js
, which is a very thin wrapper around index.js
.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello wasm-pack!</title>
</head>
<body>
<noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>
<script src="./bootstrap.js"></script>
</body>
</html>
rust-calculate/www/index.js
The index.js
is the main entry point for our web page's JavaScript. It imports the hello-wasm-pack
npm package, which contains the default wasm-pack-template
's compiled WebAssembly and JavaScript glue, then it calls hello-wasm-pack
's greet
function.
import * as wasm from "hello-wasm-pack";
wasm.greet();
First, ensure that the local development server and its dependencies are installed by running npm install
within the rust-calculate/www
subdirectory:
npm install
This command only needs to be run once, and will install the webpack
JavaScript bundler and its development server.
Note: webpack is not required for working with Rust and WebAssembly, it is just the bundler and development server we've chosen for convenience here. Parcel and Rollup should also support importing WebAssembly as ECMAScript modules. You can also use Rust and WebAssembly without a bundler if you prefer!
www
Rather than use the hello-wasm-pack
package from npm, we want to use our local rust-calculate package. This will allow us to incrementally develop our simple demo app.
Open up rust-calculate/www/package.json
and edit the "dependencies"
to include a "rust-calculate": "file:../pkg"
entry
{
// ...
"dependencies": {
"rust-calculate": "file:../pkg", // Add this line!
// ...
}
}
Next, modify rust-calculate/www/index.js
to import rust-calculate instead of the hello-wasm-pack
package:
import * as wasm from "rust-calculate";
wasm.greet();
Since we declared a new dependency, we need to install it:
npm install
Congratulations. Our web page is now ready to be served locally!
Next, open a new terminal for the development server. Running the server in a new terminal lets us leave it running in the background, and doesn't block us from running other commands in the meantime. In the new terminal, run this command from within the wasm-game-of-life/www directory:
npm run start
Navigate your web browser to http://localhost:8080/ and you should be greeted with an alert message:
Anytime you make changes and want them reflected on http://localhost:8080/, just re-run the wasm-pack build
command within the rust-calculate directory.
Next we edit the index.html file to reflect a new simple UI for the demo app we want to build. The file includes some simple CSS styles and markup.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Rust Calculate</title>
<style>
#rust-calculate{
margin-top: 10%;
margin-left: 30%;
}
#rstitle{
/* text-align: center; */
color: rgb(9, 44, 44);
}
#answerDisplay{
width: 500px;
height: 30px;
border: 1px solid rgb(43, 145, 114);
color: rgb(43, 145, 114);
}
.rsInput{
border-radius: 50px;
padding: 5px;
color: darkslategray;
border: 1px solid darkslategray;
outline: none;
}
#addBtn{
outline: none;
background-color: rgb(9, 148, 51);
padding: 5px;
border: 1px solid rgb(9, 148, 51);
width: 100px;
border-radius: 50px;
color: #ffffff;
margin-left: 2%;
}
</style>
</head>
<body>
<noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>
<section id="rust-calculate">
<h1 id="rstitle">RUST CALCULATE</h1>
<p id="answerDisplay"></p>
<input type="number" class="rsInput" id="firstValue">
<input type="number" class="rsInput" id="secondValue">
<button id="addBtn">ADD</button>
</section>
<script src="./bootstrap.js"></script>
</body>
</html>
The webpack-server automatically reloads and your page should look like this:
Next we create a rust function to perform our addition. Open the lib.rs located in rust-calculate/src/lib.rs. Change the greet function to:
pub fn add(x: i32, y: i32) -> i32 {
let z = x + y;
return z;
}
The function is pretty straightforward. It takes two numbers and returns their sum.
We now re-run our build command to bundle our new rust code to Wasm. The build automatically overwrites the previous one in the pkg directory so leave it as it is. Run this command in the rust-calculate directory:
wasm-pack build
Once it's done, take a look at the rust_calculate.js file and you should see our new add function:
export function add(x, y) {
const ret = wasm.add(x, y);
return ret;
}
Now we can now use it in our index.js file found in rust-calculate/www/index.js.
Update the index.js code to:
import * as wasm from "rust-calculate";
document.querySelector('#addBtn').addEventListener('click', () => {
let x = document.querySelector('#firstValue').value;
let y = document.querySelector('#secondValue').value;
if (!x && !y) {
alert("please input values")
return
}
let z = wasm.add(x, y)
document.querySelector('#answerDisplay').innerHTML = z
})
The code is fairly straightforward.
This imports our generated Wasm binary from the pkg directory.
import * as wasm from "rust-calculate";
Then a click event listener is attached to the button.
document.querySelector('#addBtn').addEventListener('click', () =>
We then get the values from the input fields.
let x = document.querySelector('#firstValue').value;
let y = document.querySelector('#secondValue').value;
Next we check to make sure the fields are not empty before we pass it to our add function.
if (!x && !y) {
alert("please input values")
return
}
Now we pass in our values in values to the add function, which we access with wasm.
let z = wasm.add(x, y)
Finally, we then update our UI with the result of the calculation.
document.querySelector('#answerDisplay').innerHTML = z
To learn more about how to build with Wasm + Rust, here's an amazing resource. It works you through the very basics to advanced concepts like debugging, optimizations, etc.
In this article we looked at how Wasm + Rust unlocks really exciting potential for the future of frontend web development. It doesn't replace JavaScript — it enhances and complements it. As you go forth to explore this new frontier, may the force be with you. Happy coding.
Writer of all things technical and inspirational, developer and community advocate. In a love-love relationship with JavaScript, a web accessibility nerd.