Hello, World!

Like all good tutorials, let's start with WIT Pack's equivalent of "Hello, World!" - a library that adds two numbers together.

By the end, you should know how to define a simple WIT interface and implement it in Rust. We will also publish the package to WAPM and use it from JavaScript.

You can check WAPM for the package we'll be building - it's called wasmer/hello-world.

Installation

You will need to install several CLI tools.

  • The Rust toolchain so we can compile Rust code (curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh)
  • the wasm32-unknown-unknown target so Rust knows how to compile to WebAssembly (rustup target add wasm32-unknown-unknown)
  • The Wasmer runtime so we can interact with WAPM (curl https://get.wasmer.io -sSfL | sh)
  • the cargo wapm sub-command for publishing to WAPM (cargo install cargo-wapm)

Once you've installed those tools, you'll want to create a new account on wapm.io so we have somewhere to publish our code to.

Running the wapm login command will let you authenticate your computer with WAPM.

The WIT File

We want to start off simple for now, so let's create a library that just adds two 32-bit integers.

First, let's create a new Rust project and cd into it.

$ cargo new --lib tutorial-01
$ cd tutorial-01

(you can remove all the code in src/lib.rs - we don't need the example boilerplate)

Now we can add a hello-world.wai file to the project. The syntax for a WIT file is quite similar to Rust.

// hello-world.wai

/// Add two numbers
add: func(a: u32, b: u32) -> u32

This defines a function called add which takes two u32 parameters (32-bit unsigned integers) called a and b, and returns a u32.

You can see that normal comments start with a // and doc-comments use ///. Here, we're using // hello-world.wai to indicate the text should be saved to hello-world.wai.

One interesting constraint from the WIT format is that all names must be written in kebab-case. This lets wai-bindgen convert the name into the casing that is idiomatic for a particular language in a particular context.

For example, if our WIT file defined a hello-world function, it would be accessible as hello_world in Python and Rust because they use snake_case for function names, whereas in JavaScript it would be helloWorld.

Writing Some Rust

Now we've got a WIT file, let's create a WebAssembly library implementing the hello-world.wai interface.

The wai-bindgen library uses some macros to generate some glue code for our WIT file, so add it as a dependency.

$ cargo add wai-bindgen-rust

Towards the top of your src/lib.rs, we want to tell wai-bindgen that this crate exports our hello-world.wai file.

#![allow(unused)]
fn main() {
// src/lib.rs

wai_bindgen_rust::export!("hello-world.wai");
}

(note: hello-world.wai is relative to the crate's root - the folder containing your Cargo.toml file)

Now let's run cargo check to see what compile errors it shows.

$ cargo check
error[E0412]: cannot find type `HelloWorld` in module `super`
 --> src/lib.rs:1:1
  |
1 | wai_bindgen_rust::export!("hello-world.wai");
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ not found in `super`
  |

This seems to fail because of something inside the wai_bindgen_rust::export!() macro, but we can't see what it is.

The cargo expand tool can be really useful in situations like these because it will expand all macros and print out the generated code.

To use cargo expand, you'll need to make sure it's installed (cargo install cargo-expand) and that you have the nightly toolchain available (rustup toolchain install nightly).

$ cargo expand
mod hello_world {
    #[export_name = "add"]
    unsafe extern "C" fn __wai_bindgen_hello_world_add(arg0: i32, arg1: i32) -> i32 {
        let result = <super::HelloWorld as HelloWorld>::add(arg0 as u32, arg1 as u32);
        wai_bindgen_rust::rt::as_i32(result)
    }
    pub trait HelloWorld {
        /// Add two numbers
        fn add(a: u32, b: u32) -> u32;
    }
}

There's a lot going on in that code, and most of it isn't relevant to you, but there are a couple of things I'd like to point out:

  1. A hello_world module was generated (the name comes from hello-world.wai)
  2. A HelloWorld trait was defined with an add() method that matches add() from hello-world.wai (note: HelloWorld is hello-world in PascalCase)
  3. The __wai_bindgen_hello_world_add() shim expects a HelloWorld type to be defined in the parent module (that's the super:: bit), and that super::HelloWorld type must implement the HelloWorld trait

From assumption 3, we know that the generated code expects us to define a HelloWorld type. We've only got 1 line of code at the moment, so it shouldn't be surprising to see our code doesn't compile (yet).

We can fix that by defining a HelloWorld type in lib.rs. Adding two numbers doesn't require any state, so we'll just use a unit struct.

#![allow(unused)]
fn main() {
pub struct HelloWorld;
}

Looking back at assumption 3, our code still shouldn't compile because we haven't implemented the HelloWorld trait for our HelloWorld struct yet.

$ cargo check
error[E0277]: the trait bound `HelloWorld: hello_world::HelloWorld` is not satisfied
 --> src/lib.rs:1:1
  |
1 | wai_bindgen_rust::export!("hello-world.wai");
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `hello_world::HelloWorld` is not implemented for `HelloWorld`

The fix is pretty trivial.

#![allow(unused)]
fn main() {
impl hello_world::HelloWorld for HelloWorld {
    fn add(a: u32, b: u32) -> u32 { a + b }
}
}

If the naming gets a bit confusing (that's a lot of variations on hello-world!) try to think back to that output from cargo expand. The key thing to remember is the HelloWorld type is defined at the root of our crate, but the HelloWorld trait is inside a hello_world module.

Believe it or not, but we're done writing code for now. Your crate should now compile 🙂

Compiling To WebAssembly

At the moment, running cargo build will just compile our crate to a Rust library that will work on your current machine (e.g. x86-64 Linux), so we'll need to cross-compile our code to WebAssembly.

Rust makes this cross-compilation process fairly painless.

First, we need to install a version of the standard library that has already been compiled to WebAssembly.

$ rustup target add wasm32-unknown-unknown

We'll go into target triples a bit more when discussing WASI, but wasm32-unknown-unknown basically means we want generic 32-bit WebAssembly where the OS is unknown (i.e. we know nothing about the underlying OS, so we can't use it).

Next, we need to tell rustc that we want it to generate a *.wasm file.

By default, it will only generate a rlib (a "Rust library"), so we need to update Cargo.toml so our crate's crate-type includes a cdylib (a "C-compatible dynamic library").

# Cargo.toml

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

Now, we should be able to compile our crate for wasm32-unknown-unknown and see a *.wasm file.

$ cargo build --target wasm32-unknown-unknown
$ file target/wasm32-unknown-unknown/debug/*.wasm
target/wasm32-unknown-unknown/debug/tutorial_01.wasm: WebAssembly (wasm) binary module version 0x1 (MVP)

The wasmer CLI also has an inspect command which can be useful for looking at our *.wasm file.

$ wasmer inspect target/wasm32-unknown-unknown/debug/tutorial_01.wasm
Exports:
  Functions:
    "add": [I32, I32] -> [I32]

You'll notice that, besides a bunch of other stuff, we're exporting an add function that takes two i32s and returns an i32.

This matches the __wai_bindgen_hello_world_add() signature we saw earlier.

Publishing to WAPM

Now we've got a WebAssembly binary that works, let's publish it to WAPM!

The core component in a WAPM package is the wapm.toml file. This acts as a "manifest" which tells WAPM which modules are included in the package, and important metadata like the project name, version number, and repository URL.

You can check out the docs for a walkthrough of the full process for packaging an arbitrary WebAssembly module.

However, while we could create this file ourselves, most of the information is already available as part of our project's Cargo.toml file. The cargo wapm sub-command lets us automate a lot of the fiddly tasks like compiling the project to wasm32-unknown-unknown, collecting metadata, copying binaries around, and so on.

To enable cargo wapm, we need to add some metadata to our Cargo.toml.

# Cargo.toml

[package]
...
description = "Add two numbers"

[package.metadata.wapm]
namespace = "wasmer"  # Replace this with your WAPM username
abi = "none"
bindings = { wai-bindgen = "0.1.0", exports = "hello-world.wai" }

Something to note is that all packages on WAPM must have a description field.

Other than that, we use the [package.metadata] section to tell cargo wapm a couple of things:

  • which namespace we are publishing to (all WAPM packages are namespaced)
  • The ABI being used (none corresponds to Rust's wasm32-unknown-unknown, and we'd write wasi if we were compiling to wasm32-wasi), and
  • The location of our hello-world.wai exports, plus the version of wai-bindgen we used

Now we've updated our Cargo.toml, let's do a dry-run to make sure the package builds.

$ cargo wapm --dry-run
Successfully published package `wasmer/hello-world@0.1.0`
[INFO] Publish succeeded, but package was not published because it was run in dry-run mode

If we dig around the target/wapm/ directory, we can see what cargo wapm generated for us.

$ tree target/wapm/tutorial-01
target/wapm/tutorial-01
├── tutorial_01.wasm
├── hello-world.wai
└── wapm.toml

0 directories, 3 files

$ cat target/wapm/tutorial-01/wapm.toml
[package]
name = "wasmer/tutorial-01"
version = "0.1.0"
description = "Add two numbers"
repository = "https://github.com/wasmerio/wasmer-pack-tutorial"

[[module]]
name = "tutorial-01"
source = "tutorial_01.wasm"
abi = "none"

[module.bindings]
wai-exports = "hello-world.wai"
wai-bindgen = "0.1.0"

This all looks correct, so let's actually publish the package!

$ cargo wapm

If you open up WAPM in your browser, you should see a new package has been published. It'll look something like wasmer/tutorial-01.

Using the Package from Python

Let's create a Python project that uses the bindings to double-check that 1+1 does indeed equal 2.

First, create a new virtual environment and activate it.

$ python -m venv env
$ source env/bin/activate

Now we can ask the wapm CLI to pip install our tutorial-01 package's Python bindings.

$ wapm install --pip wasmer/tutorial-01
...
Successfully installed tutorial-01-0.1.0 wasmer-1.1.0 wasmer-compiler-cranelift-1.1.0

Whenever a package is published to WAPM with the bindings field set, WIT Pack will automatically generate bindings for various languages in the background. All the wapm CLI is doing here is asking the WAPM backend for these bindings - you can run the query yourself if you want.

The tutorial_01 package exposes a bindings variable which we can use to create new instances of our WebAssembly module. As you would expect, the object we get back has our add() method.

# main.py

from tutorial_01 import bindings

instance = bindings.hello_world()
print("1 + 1 =", instance.add(1, 1))

Let's run our script.

$ python ./main.py
1 + 1 = 2

Conclusions

Hopefully you've got a better idea for how to create a WebAssembly library and use it from different languages, now.

To recap, the process for publishing a library to WAPM is:

  1. Define a *.wai file with your interface
  2. Create a new Rust crate and add wai-bindgen as a dependency
  3. Implement the trait defined by wai_bindgen_rust::export!("hello-world.wai")
  4. Add [package.metadata.wapm] table to your Cargo.toml
  5. Publish to WAPM

We took a bit longer than normal to get here, but that's mainly because there were plenty of detours to explain the "magic" that tools like wai-bingen and cargo wapm are doing for us. This explanation gives you a better intuition for how the tools work, but we'll probably skip over them in the future.

Some exercises for the reader:

  • If your editor has some form of intellisense or code completion, hover over things like bindings.hello_world and instance.add to see their signatures
  • Add an add_floats function to hello-world.wai which will add 32-bit floating point numbers (f32)