Jan 19, 2021

Rust And WebAssembly - wasm-pack

wasm-pack

JavaScript is the most loved language. The introduction of Node propelled the success of the JavaScript world on the server side. Node made easier for billions of developers to develop and share libraries. WebAssembly modules require JavaScript to seamlessly interoperate inside the JavaScript engine. Together JavaScript and WebAssembly makes the web faster and smaller.

Getting Started...

The wasm-pack tool seeks to be a one-stop shop for building and working with rust- generated WebAssembly that you would like to interop with JavaScript, in the browser or Node.js. - wasm-pack website

Why do you need wasm-pack?

The wasm-pack makes it easy to build and pack the Rust and WebAssembly based projects. Once packed the module is ready to be shared with the world via the npm registry, just like millions (or even billions) of JavaScript libraries out there.

How to use wasm-pack?

The wasm-pack is available as a cargo library. If you are following this book, then you might have already installed the Cargo. To install the wasm-pack run the following command.

$ cargo install wasm-pack

The above command download, compile, and install the wasm-pack. Once installed the wasm-pack command is available. To check whether the wasm-pack is installed correctly, run

$ wasm-pack --version
wasm-pack 0.9.1

Let us see how to use wasm-pack to build and pack the Rust and WebAssembly projects.

Write some code ✍️

Let us create a new project using Cargo.

$ cargo new --lib wasm_pack_world
  Created library `wasm_pack_world` package

The cargo new --lib command generates a library with a name wasm_pack_world. Open the project in your favourite editor. Open the cargo.toml file and add the wasm-bindgen dependency in [dependencies] segment.

[package]
name = "wasm_pack_world"
version = "0.1.0"
authors = ["Sendil Kumar <sendilkumarn@live.com>"]
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2.69"

Note the [lib] segment in the cargo.toml. Refer here for more information.

Next open the src/lib.rs file and replace the contents with the following:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn get_answer() -> i32 {
    42
}

We first import the wasm_bindgen library using use wasm_bindgen::prelude::*. Next we define a simple function get_answer that returns 42 (the universal answer). We added #[wasm-bindgen] annotation to the function.

In the previous example, we have used cargo to create the WebAssembly module. While cargo build --target wasm32-unknown-unknown converts Rust code into WebAssembly module, but they cannot generate binding file and cannot understand the #[wasm_bindgen] annotation.

The Binding JavaScript file helps to translate the value between JavaScript and WebAssembly.

The wasm-bindgen-cli helps to generate the binding JavaScript file. The binding file is kind of translator that translates value between JavaScript and WebAssembly.

The wasm-pack comes with a build option that does the following two steps:

  1. Generate the WebAssembly module
  2. Generate binding JavaScript file

The wasm-pack build generates the WebAssembly module and the binding file.

$ wasm-pack build

How it works...

This is what happens, when we run wasm-pack build.

  1. The wasm-pack first checks for the installed rust compiler. If installed whether the rust compiler is greater than version 1.30.

  2. Then wasm-pack checks for the crate configuration. Whether the library indicates that we are generating a dynamic library.

  3. Finally, wasm-pack validates if there is any wasm-target available for building. If the wasm32-unknown-unknown target is not available. wasm-pack will download and add the target.

Once the environment is ready, wasm-pack then starts compiling the module and build them.

When the build successfully finished, it creates a pkg directory. Inside the pkg, it will pipe the output of the wasm-bindgen.

pkg
├── package.json
├── wasm_pack_world.d.ts
├── wasm_pack_world.js
├── wasm_pack_world_bg.d.ts
├── wasm_pack_world_bg.wasm
└── wasm_pack_world_bg.wasm.d.ts

Now, this pkg folder can be bundled and shared like any other JavaScript module.

Note that the wasm-pack command generates a package.json file. The package.json is inside the pkg folder.

{
  "name": "wasm_pack_world",
  "collaborators": [
    "sendilkumarn <sendilkumarn@live.com>"
  ],
  "version": "0.1.0",
  "files": [
    "wasm_pack_world_bg.wasm",
    "wasm_pack_world.js",
    "wasm_pack_world.d.ts"
  ],
  "module": "wasm_pack_world.js",
  "types": "wasm_pack_world.d.ts",
  "sideEffects": false
}

Note: The wasm-pack copies over the Readme, LICENSE file if we have one. To basically make it easier to have shared documentation between the Rust and WebAssembly version.

The wasm_pack_world.js consist of all the necessary import and export of the wasm modules.

import * as wasm from "./wasm_pack_world_bg.wasm";
export * from "./wasm_pack_world_bg.js";

The wasm-pack also generates the necessary type definition *.d.ts.


Build Using wasm-pack

The wasm-pack definitely shortens the build process. It checks whether wasm-bindgen-cli is installed. If it is not installed, it installs the required wasm-bindgen-cli using cargo (under the hoods wasm-pack still uses cargo and wasm-bindgen).

Let us explore further what are the options provided by the wasm-pack and how we can use them.

Path

The pkg directory generated by the wasm-pack contains all the necessary build artifacts that you will need to share. The wasm-pack build command expected to be run inside a Rust project and it expects Cargo.toml file to be present in the directory in which it is executed. You can send in the path information to the wasm-pack and then wasm-pack will run its build inside the path passed in. It is important that the path should have Cargo.toml.

$ wasm-pack build some/other/path/with/Cargo.toml

--out-dir

Similar to wasm-bindgen, wasm-pack supports --out-dir to specify the output directory to generate the build artifacts. By default, the build artifacts are generated into the pkg folder. We can customise the output directory with the --out-dir.

$ wasm-pack build --out-dir path/to/store/the/output/artifacts

--out-name

By default, the files generated are named based on the project name. But we can customise the output file name with the --out-name option.

$ wasm-pack build --out-name some_other_name

The generated output files, will be named some_other_name.

pkg
├── package.json
├── some_other_name.d.ts
├── some_other_name.js
├── some_other_name_bg.d.ts
└── some_other_name_bg.wasm

build-mode

By default, wasm-pack will check for the presence of wasm-bindgen-CLI and installs it. But we can override that, if we already have the CLI installed globally.

The wasm-pack has a mode flag and it accepts three arguments as follows:

normal: the default option.

$ wasm-pack build --mode normal

force: It forces the build. It even skips the rustc version check.

$ wasm-pack build --mode force

no-install: It does not install the wasm-bindgen-CLI and uses the globally available wasm-bindgen CLI.

$ wasm-pack build --mode no-install

profiling

The wasm-pack provides options to make the binary more optimal for our needs. Let us explore them further.

Profiling the generated binaries is the most important step in WebAssembly applications.

Isn't that a common thing across all the software that we deliver?

We need complete debugging information, during the development. Then we need to optimise the binaries, remove all the debugging information, remove unwanted assertions, compact the source code and make it as small as possible before we deliver it.

The wasm-pack provides the following three options for profiling:

  • dev
  • profiling
  • release

--dev

The dev profile adds debug assertions, information about the debug, and applies no optimisation on the binaries generated. As the name specifies, it is more suitable for the development purpose.

During the runtime, to assert any value is of the specified format or as expected we will have to assert!(). This ensures that we will not have any weird runtime errors.

Instead of assert, we can have debug_assert!() to ensure whether a condition is true or false. But they are expensive than compared to the assert!() both in terms of time and performance. They are helpful during development. We cannot afford them in the production.

The debug information is an option defined at the kernel level. When enabled, this instructs the compiler to add in some debug information in the resulting binary. Abstractly they are nothing but some additional data that will be included in the binary and used to relate to the binary code which was being executed.

Obviously adding these two data in the binary reduces the performance and bloats up the release binary, but they are extremely helpful during development.

--profiling

The profiling profile adds only the debug information to the source code. It applies certain optimisation on the binaries but does not include the debug assertions.

--release

The release profile focus on achieving maximum optimisation, reducing binary size by removing debug information and making it run faster by removing unnecessary assertions. Thus the time take to compile is longer but the resulting binary is small and optimised.


--target

We have seen that the wasm-bindgen supports various targets. We can instruct wasm-pack to generate the output artifact for those targets via the --target flag.

--target nodejs - for node.
--target bundler - for running it with bundlers like Webpack and Parcel.
--target web - for running it in modern browsers as an ECMAScript module.
--target no-modules - for running it in browsers just like any other JavaScript.

Note: The WebAssembly still do not support ECMAScript modules, this means even when we pass on --target web flag, the WebAssembly module has to be manually instantiated and loaded.


Pack

The most important thing for a library developer is pack and publish the artifacts.

The wasm-pack helps to build, pack and publish the Rust and WebAssembly based projects into NPM registry as an npm package. We have seen how wasm-pack makes it simpler to build the Rust into WebAssembly binary along with the binding JavaScript file using wasm-bindgen. Let us explore further how wasm-pack helps to pack and publish.

The wasm-pack provides a pack flag to pack the artifacts that were generated using wasm-pack build command. Although it is not necessary to use wasm-pack to build the binaries it generates all the boilerplate things that we will need to pack the artifacts into a Node module.

In order to pack the built artifacts using wasm-pack, we have to run the following command with reference to the pkg (or the directory with which we generated our build artifacts):

$ wasm-pack pack pkg

We can run the command by passing in the project_folder/pkg as its argument. By default, wasm-pack pack command search for the pkg directory in the current working directory where it is running.

The wasm-pack pack command, first identifies whether the folder provided is a pkg directory or contains a pkg directory as its immediate child. If the check passes, then wasm-pack invokes the npm pack command underneath to pack the library into an NPM package.

To bundle the npm package all we need is a valid package.json file. That file is generated by the wasm-pack build command.

We run the pack command inside the wasm_pack_world example and check what happens.

$ wasm-pack pack
npm notice
npm notice 📦  wasm_pack_world@0.1.0
npm notice === Tarball Contents ===
npm notice 332B package.json
npm notice 767B wasm_pack_world_bg.wasm
npm notice 120B wasm_pack_world.d.ts
npm notice 186B wasm_pack_world.js
npm notice === Tarball Details ===
npm notice name:          wasm_pack_world
npm notice version:       0.1.0
npm notice filename:      wasm_pack_world-0.1.0.tgz
npm notice package size:  698 B
npm notice unpacked size: 1.4 kB
npm notice shasum:        c8d64ea76edfb27863c93286e92ac7a8150d96c8
npm notice integrity:     sha512-GFoTMM4x41A5w[...]FuIdd4Q5JV5Ig==
npm notice total files:   4
npm notice
wasm_pack_world-0.1.0.tgz
[INFO]: 🎒  packed up your package!

As you can see here, the pack command creates a tarball package with the contents inside the pkg folder with the help of npm pack command.

publish

Once we have packed our application, the next step is to publish it. In order to publish the tarball generated the wasm-pack has a publish flag.

$ wasm-pack publish
Your package hasn't been built, build it? [Y/n]

If you answer yes to the question then it asks for you to input the folder in which you want to generate the build artifacts. We can give any folder name or use the default.

$ wasm-pack publish
Your package hasn't been built, build it? yes
out_dir[default: pkg]:

Then it asks your target, i.e., target to which the build should be generated. You can choose between the various options here as discussed in the build recipe.

$ wasm-pack publish
Your package hasn't been built, build it? yes
out_dir[default: pkg]: .
target[default: browser]:
> browser
 nodejs
 no-modules

Based on the option provided, it generates the artifact in the folder specified. Once the artifacts are produced, they are then ready to be published using npm publish. For npm publish to work correctly we need to be authenticated. You can authenticate to the npm either by using npm login or wasm-pack login. The wasm-pack login command invoke the npm login command and then creates a session.

$ wasm-pack login
Username: sendilkumarn
Password: *************
login succeeded.

The wasm-pack publish command supports two options they are:

  • -a or --access - To determine the access level of the package to be deployed. The access accepts either public or restricted.
    • public - makes the package public
    • restricted - makes the package internal.
  • -t or --target - To support various targets to which the build is produced.

Test with browsers or Node

Testing should be an integral part of any software development. It is important to have a proper testing environment and measures to ensure the quality of the products that we create.

So far we haven't seen much information with respect to testing. The rustwasm group created wasm-bindgen-test crate to assist with testing the WebAssembly applications. The wasm-pack provides a wrapper over the wasm-bindgen-test library and makes it easy to test the WebAssembly applications that we generate. In order to test the Rust and WebAssembly application using wasm-pack, we can use:

$ wasm-pack test

Options & Flags

The WebAssembly application being a part of the Web platform means that it has to support a wide range of browsers or environments.

Similarly, any testing library attached to it should support that wide range of options.

The wasm-pack test command supports following options for browser testing:

  • --chrome
  • --firefox
  • --safari

The above flags require the particular driver to be installed and included in the path. The --chrome and --firefox option will download the driver if not present but the --safari option cannot. We can also specify the driver location using the following options respectively

  • --chromedriver <path/to/driver>
  • --geckodriver <path/to/driver>
  • --safaridriver <path/to/driver>

For the --safari option, it is mandatory to have the --safaridriver and specify the path of the safari driver.

You can specify whether the browsers should run with a UI or not, using the --headless option. We can run the test with --node flag, that runs the test in the node environment. It is essential to test the artifacts work perfectly in the release mode and the functionality did not fail when we do code optimisation. We can achieve that with -r or --release option. You can specify whether we want to download a local copy of wasm-bindgen-CLI or use the existing one using the --mode option. Additionally, since we will be using cargo test underneath, the wasm-pack test command will also accept the arguments that we generally use along with cargo test.

wasm-pack test

The wasm-pack test invokes cargo build --tests command. Based on the mode provided it will either download the wasm-bindgen-cli dependency and install it or use it from the path.

The wasm-pack test command expects either a browser or node option to be present. That is it has to be either (chrome | firefox | safari) or node as one of the option. If it is not present, then it will throw an error.

The wasm-pack test command run the necessary test based on the option passed in. For Node, the wasm-pack test command invoke the cargo test with target wasm32-unknown-unknown. For browsers, it first checks the availability of the driver and installs them if it is not. Then it spins up the respective browser and then runs the test using wasm-bindgen test runner. Add wasm_bindgen_test library as a dependency in the Cargo.toml.

[package]
name = &quot;wasm_pack_world&quot;
version = &quot;0.1.0&quot;
authors = [&quot;Sendil Kumar Nellaiyapen &lt;sendilkumarn@uber.com&gt;&quot;]
edition = &quot;2018&quot;

[lib]
crate-type = [&quot;cdylib&quot;]

[dependencies]
wasm-bindgen = &quot;0.2.69&quot;

[dev-dependencies]
wasm-bindgen-test = &quot;0.2&quot;

Now we can add our own test and verify. Add the following test in the tests/lib.rs,

#![cfg(target_arch = &quot;wasm32&quot;)]

extern crate wasm_bindgen_test;
use wasm_bindgen_test::*;

use wasm_pack_world::get_answer;

#[wasm_bindgen_test]
fn pass_answer() {
 let actual = get_answer();
 assert_eq!(42, actual)
}

We first import the library. Then we annotate the method with #[wasm_bindgen_test] macro. This enables all the necessary configurations for the tests to execute.

$ wasm-pack test --node
  Finished test [unoptimized + debuginfo] target(s) in 0.04s
  Running target/wasm32-unknown-unknown/debug/deps/wasm_pack_world-7723ee9099032a71.wasm
  Running target/wasm32-unknown-unknown/debug/deps/lib-2f76d97dee4a3887.wasm
running 1 test

test lib::pass_answer ... ok

test result: ok. 1 passed; 0 failed; 0 ignored

Additional sources to explore 🔎

If you are starting with Rust newly, check out the source code at here the best place to learn Rust for beginners. The source code is awesomely structured and logged.

To know more about the wasm-pack check Check out the amazing documentation site from the wasm-pack team here

If you are curious like me, then the Debug information will be something that you might be interested to know more about. Check out this

Check out more about wasm-bindgen-test here. We will cover them more in detail later.


Up Next