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 thecargo.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:
- Generate the WebAssembly module
- 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
.
-
The
wasm-pack
first checks for the installed rust compiler. If installed whether the rust compiler is greater than version 1.30. -
Then
wasm-pack
checks for the crate configuration. Whether the library indicates that we are generating a dynamic library. -
Finally,
wasm-pack
validates if there is any wasm-target available for building. If thewasm32-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 theReadme
,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 publicrestricted
- 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 = "wasm_pack_world"
version = "0.1.0"
authors = ["Sendil Kumar Nellaiyapen <sendilkumarn@uber.com>"]
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.69"
[dev-dependencies]
wasm-bindgen-test = "0.2"
Now we can add our own test and verify. Add the following test in the tests/lib.rs
,
#![cfg(target_arch = "wasm32")]
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.