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:
- A
hello_world
module was generated (the name comes fromhello-world.wai
) - A
HelloWorld
trait was defined with anadd()
method that matchesadd()
fromhello-world.wai
(note:HelloWorld
ishello-world
in PascalCase) - The
__wai_bindgen_hello_world_add()
shim expects aHelloWorld
type to be defined in the parent module (that's thesuper::
bit), and thatsuper::HelloWorld
type must implement theHelloWorld
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 i32
s 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'swasm32-unknown-unknown
, and we'd writewasi
if we were compiling towasm32-wasi
), and - The location of our
hello-world.wai
exports, plus the version ofwai-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:
- Define a
*.wai
file with your interface - Create a new Rust crate and add
wai-bindgen
as a dependency - Implement the trait defined by
wai_bindgen_rust::export!("hello-world.wai")
- Add
[package.metadata.wapm]
table to yourCargo.toml
- 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
andinstance.add
to see their signatures - Add an
add_floats
function tohello-world.wai
which will add 32-bit floating point numbers (f32
)