WebAssembly (abbreviated WASM) is a binary instruction format for a stack-based virtual machines. WASM is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications.
Why use WebAssembly (WASM)?
WASM allows developers to build high-speed web apps in the language of their choice. However it would be a wrong notation that WebAssembly is only for web applications. It can be used for other non-web based applications as well
WebAssembly is not going to replace JavaScript or any other language for that matter. It is just a binary format that promises near native performance on any target. It can be compilation target for any programming language.
In this article we will cover how to compile Rust code to WebAssembly. We can write some Rust code, call JavaScript functions from it or expose functions to JavaScript for further use.
Use cases for WebAssembly
There are two major use cases for Rust and WebAssembly:
- Entire Application: Build an entire web application using Rust and compile it to WebAssembly
- Partial application: Build partial application using Rust and compile it to WebAssembly. Then use it with an existing JavaScript frontend.
In this article we are going to cover latter use case for illustration. If you are interested to build complete web application using rust, check out yew project.
To build a wasm app we will be using package wasm-pack
to build a package. wasm-pack
is a tool for building JavaScript packages in Rust. This implementation will contain WebAssembly and JavaScript code, so to run that package users will not need Rust installed. Since final product would be WebAssembly and JavaScript, users will not even know its built using Rust.
Environment Setup
To build package we would need and environment setup. Let’s go through what all softwares and dependencies needed.
Rust
First we would definitely need Rust installed. If you do not have it setup already, please head to Rust Installation page and install by following instructions. This installs a tool called rustup
, which lets you manage multiple versions of Rust. By default, it installs the latest stable Rust release, which you can use for general Rust development.
rustup
installs rustc
, the Rust compiler. It also installs cargo
, package manager for Rust, rust-std
, standard libraries of Rust’s, and documentations rust-docs
.
Note: Pay attention to the
post-install
note about needing cargo’s bin directory in your system PATH. This is added automatically, but you must restart your terminal for it to take effect.
wasm-pack
Now to trans-compile our code into WebAssembly; we need an additional tool, a wasm-pack package. Once installed and added as dependency to our project, this will help compile the code to WebAssembly, as well as produce code structure to be used in browser.
To download and install it, enter the following command into your terminal:
cargo install wasm-pack
Building our WebAssembly package
Now let us create a package in Rust. Open a command prompt, navigate to a folder where you would like to create this package and then run following command in command prompt
cargo new --lib first-wasm
//=> Created library `first-wasm` project
If you list content of present working directory, you should notice a folder named first-wasm
. This is where a rust package has been created. Now go into first-wasm
folder and list its contents. If you are on unix based OS, you may use tree
command, like following
tree
//+-- Cargo.toml
//+-- src
// +-- lib.rs
Let’s dissect what we are looking at. Cargo.toml
is package file, contains or relay important information about package (i.e config, structure, dependencies and how to build). If you are already familiar with Gemfile
in Bundler in Ruby or package.json
in NodeJS’s npm. It is similar to that.
Next we are going to look at src/lib.rs
that cargo
has generated for us.
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
It is just an assertion test or a placeholder. Hence we are not going to use this. You can go ahead and remove this code.
Let’s write our WASM code in Rust
Let us write some Rust code that would transpile to WASM. Put following content into src/lib.rs
. Make sure that you removed existing code.
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern {
pub fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
Let’s dissect the code above. It has three major parts that we will discuss here. We will not be going into Rust dictionary and syntax, for that please read Rust By Example Book
Using wasm-bindgen
to communicate between Rust and JavaScript
The first line in our Rust code is following:
use wasm_bindgen::prelude::*;
Here the use
keyword, imports code from library into your code. In Rust, libraries are called crates
. In this case, use
is importing everything from the wasm_bindgen::prelude
module. We use these features in the next section. wasm-bindgen
is coming from a wasm-pack
package to create a bridge between Rust code and JavaScript. It allows JavaScript to call Rust API and/or Rust function with required parameters. In short, it generates bindings between JavaScript and Rust
We use wasm-bindgen
’s functionality in our package. In fact, that’s the next section.
Calling external JavaScript functions from Rust
In our code, the next four lines looks like following:
#[wasm_bindgen]
extern {
pub fn alert(s: &str);
}
The call inside the #[]
is called an attribute
, and it modifies the next statement. In our case the statement is question is extern
. This tells Rust that we want to call some externally defined functions. The attribute points to where to find those functions, in our case it is wasm-bindgen
and it handles the binding.
The next line defines a public function in Rust, named alert
, which takes a string as argument and assign it to s
As you already may know, alert
function is available in JavaScript and we are calling it from Rust using wasm-bindgen
Whenever you want to call any JavaScript functions, you just add them to this file, and wasm-bindgen
will take care of setting everything up for you.
Beware that, not everything is supported yet, but we’re working on it. At the time of writing this article this functionality is still Work-in-Progress.
Producing Rust functions that JavaScript can call
Now look at remaining lines of code, below:
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
Once again, we see the #[wasm_bindgen]
attribute. In this case, it’s not modifying an extern block, but a public function fn
; this means that we want this Rust function to be able to be called by JavaScript.
This is opposite of extern
; these are the functions that we are exposing to JavaScript front-end from Rust via WebAssembly.
This function is named greet
, and takes one argument name
, a string. It then calls the alert function we asked for in the extern
block above. It passes a call to the format!
macro, which lets us interpolate strings.
The format!
macro takes two arguments in this case, a format string, and a variable to put in it. The format string is the "Hello, {}!"
bit. It contains {}
, where variables will be interpolated. The variable we’re passing is name
, the argument to the function, so if we call greet("Pankaj")
we should see "Hello, Pankaj!"
.
This is passed to alert()
, so when we call this function we will see an alert box with "Hello, Pankaj!"
in it. Now since our library is written, let’s build it.
Compiling our code to WebAssembly
Sigh!, Now we came to last part. We get to compile our code to WebAssembly and then get to use in our front-end code. Let’s open Cargo.toml
and change its content with below code:
[package]
name = "first-wasm"
version = "0.1"
authors = ["Your Name <you@example.com>"]
description = "A sample project with wasm-pack"
license = "MIT/Apache-2.0"
repository = "https://github.com/yourgithubusername/first-wasm"
edition = "2023"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "v0.2"
Fill in values with values specific to you (i.e github repo url). The big part to add is the [package]
config. The [lib]
part tells Rust to build a cdylib
version of our package. For more, consult the Cargo
and Rust Linkage
documentation.
The last section is the [dependencies]
section. Here’s where we tell Cargo
what version of wasm-bindgen
we want to depend on; in this case, that’s any 0.2.z
version (but not 0.3.0 or above).
Building the package
Now that we’ve got everything set up, let’s build the package. Run following command to build package
wasm-pack build --target web
Command above tells wasm-pack
to build current folder targeting web (or browser) use. --target
is specified as web. We can also build it for nodejs
. On first run, it may take a lot of time; as it needs to do number of things. To learn about them in detail, check out this blog post on Mozilla Hacks. In short, wasm-pack
build:
- Compiles Rust code to WebAssembly.
- Runs
wasm-bindgen
on that WebAssembly, generating a JavaScript file that wraps up that WebAssembly file into a module the browser can understand. - Creates a
pkg
directory and moves that JavaScript file and your WebAssembly code into it. - Reads your
Cargo.toml
and produces an equivalentpackage.json
. - Copies your
README.md
(if you have one) into the package.
After the build completes, you must have your package under folder named pkg
A digression about code size
If we look at the size of code generated, it can go in hundreds of kilobytes. This is because we have not optimized it yet. Since this is not in the scope of this article, we are going to skip it for now. But if you are curious, you shall have a look at wasm-shrink
crate. Further reading is available on Rust WebAssembly Working Group’s documentation on optimizing .wasm
further
Using the package on the web
Now, since we have our hands on final produce of this .wasm
package. Let’s run that in browser
Create a folder, copy over pkg
folder as generated by wasm-pack
under folder you just created. Create an index file named index.html
; with following contents:
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>first-wasm example</title>
</head>
<body>
<script type="module">
import init, { greet } from "./pkg/first_wasm.js";
init().then(() => {
greet("WebAssembly");
});
</script>
</body>
</html>
Now, open this index.html
file in browser. There are multiple ways to do it. Like use [http-server] npm package or use python static server for a folder, you can run following command
python -c "import SimpleHTTPServer;SimpleHTTPServer.test()"
The script in this file will import the js glue code, initialize the wasm module, and call the greet function we wrote in rust.
Note: Make sure to use an up-to-date web server that supports the
application/wasm
MIME type. Older web servers might not support it yet.
Load index.html
from the web server (if you used the Python example, browse at http://localhost:8000
). An alert box appears on the screen, with Hello, WebAssembly!
in it. We’ve successfully called from JavaScript into Rust, and from Rust into JavaScript. Yay!
Making our package available to npm
NPM, is very popular package management used to manage dependencies for Node.JS. Presently npm
is most preferred way to share your libraries to world out there, be it front-end or backend. If you wish to use the WebAssembly module, we created; with npm, we’ll need to make a few changes.
We will start by recompiling our Rust package with target bundler
option:
wasm-pack build --target bundler
Install Node.js and npm
Since we have built our WebAssembly package for npm
. We need to have Node.JS and NPM installed (if not already installed)
To get Node.JS and NPM, head to Node.JS Download Page and follow instruction. Once done, check your installation with node
command. Fire up a terminal window and run following commands
node -v
npm -v
Next, we are going to link our package to npm
. Once linked it will be available for installation in local system. To do this, we shall run following command in terminal.
cd ./pkg
npm link
We now have an npm package, that is written in Rust, trans-compiled to WebAssembly. Now this package is available to users via npm. Once part of project, it can be called from within our application. User need not have Rust installed for this to work on their systems. The package code included is WebAssembly code not the Rust source.
Using the npm package on the web
Now let’s use our npm package in a website. There are many bundling tools out there to manage packages for front-end. We would be using webpack
to show how to use our npm package.
Fire up a new terminal and make a directory using following command:
mkdir -p myProject
cd myProject
npm link first-wasm
Now to convert this folder into Node.JS project, run npm init
and give your input on prompt. Once done you should see a package.json
file. Now run following commands in terminal:
npm install first-wasm --save
npm install webpack --save-dev
npm install webpack-cli --save-dev
npm install webpack-dev-server --save-dev
Now if you open package.json
file in your favorite editor, you should see something like following
"dependencies": {
"first-wasm": "^0.1.0"
},
"devDependencies": {
"webpack": "^4.25.1",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.10"
}
Now please add webpack-dev-server
to your serve
script. This will help us bringing up a server to serve our files.
"scripts": {
"serve": "webpack-dev-server"
....snipped
}
Huff, Now we need to configure webpack. To do that we shall create webpack.config.js
and put in following code in it:
const path = require("path");
module.exports = {
entry: "./index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "index.js",
},
mode: "development",
};
Since our webpack config is telling webpack to enter from index.js
. Let’s create an index.js
file and toss in following content in it:
import("./node_modules/first-wasm/first_wasm.js").then( ( script ) => {
script.greet("WebAssembly with npm");
});
This is not the best practices advised to import and use packages, however since this is just to showcase our WebAssembly package, it should be okay.
Once it’s loaded, it calls the greet
function from that module, passing “WebAssembly” as a string.
Note how there’s nothing special here, yet we’re calling into Rust code. As far as the JavaScript code can tell, this is just a normal module.
Finally, we need to modify the HTML file; open the index.html file and replace the current contents with the following:
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>first-wasm example</title>
</head>
<body>
<script src="./index.js"></script>
</body>
</html>
Phew! we are almost done. Let’s run npm install
and then fire up our file server by running following commands:
npm install
npm run serve
Command above shall start a web server at http://localhost:8080
. Open that in web browser and an alert box shall appear on the screen; with Hello, WebAssembly with npm!
. We’ve successfully built WebAssembly with Rust, trans-compiled it to WebAssembly and converted it to npm package and then used it in a Node.JS project. Yay!
Conclusion
We finally reached end of this article. We hope you have found it useful so far and learnt something new today. There’s lots of exciting work going on in WebAssembly space. So stay tuned. See you later
Further Reading
About The Author
I am Pankaj Baagwan, a System Design Architect. A Computer Scientist by heart, process enthusiast, and open source author/contributor/writer. Advocates Karma. Love working with cutting edge, fascinating, open source technologies.
To consult Pankaj Bagwan on System Design, Cyber Security and Application Development, SEO and SMO, please reach out at me[at]bagwanpankaj[dot]com
For promotion/advertisement of your services and products on this blog, please reach out at me[at]bagwanpankaj[dot]com
Stay tuned <3. Signing off for RAAM