Building a simple component (Rust)
Rust has first-class support for WebAssembly core and WebAssembly components via the available targets in the toolchain:
wasm32-unknown-unknown(WebAssembly core)wasm32-wasip1(WASI P1)wasm32-wasip2(WASI P2)
note
To use the targets above, ensure that they are enabled via the Rust toolchain (e.g. rustup).
For example, to add the wasm32-wasip2 target (rustup toolchain list can be used to show all available toolchains):
rustup target add wasm32-wasip2
With built-in support, Rust code (and the standard library) can compile to WebAssembly with native tooling:
cargo build --target wasm32-wasip2
warning
While in the past the use of cargo-component was recommended,
the project is in the process of being deprecated as native tooling can be used directly.
1. Setup
Install wasm-tools to enable introspection and manipulation of WebAssembly binaries:
cargo install --locked wasm-tools
Install wasmtime, a fast and secure runtime for WebAssembly binaries:
curl https://wasmtime.dev/install.sh -sSf | bash
2. Creating a WebAssembly project in Rust
Create a new project in Rust with cargo new:
$ cargo new --lib adder
Creating library `adder` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
When building Rust WebAssembly projects, it is possible to create either a binary crate or a library crate that can compile to WebAssembly (producing a "command" component or a "reactor" component respectively), in this case opt for a reactor component.
note
The distinction between a command component and a reactor component isn't important yet, but they can be considered similar to the difference between a binary and a shared library.
One thing that we will have to add to our Rust project is in Cargo.toml, setting the crate-type:
[lib]
crate-type = ["cdylib"]
As we are building a reactor component (the "equivalent" of a library of functions), we must use
the cdylib (stands for "c dynamic library") crate type that Rust provides.
3. Adding the add Interface
We will create a component in Rust that implements the add interface exported by
the adder world world in the docs:adder WebAssembly Interface types (WIT) package.
Create a file called wit/world.wit and fill it with the following content:
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
The (WIT) types in this file represent the interface of our component must satisfy (the adder world).
We say that our component "exports" the add interface (which itself contains a single function add).
Working with these types is similar to other Interface Definition Language (IDL) toolchains (e.g. protobuf), in that we will need some language level bindings that make the interface easy to implement.
4. Generating Bindings for the adder Interface
While the Rust toolchain can compile WebAssembly binaries natively, it cannot (yet) automatically
generate bindings that match our intended (WIT) interface types (wit/world.wit).
We can use wit-bindgen to generate bindings:
cargo add wit-bindgen
note
The command above should be run from inside the adder directory that was created by
cargo new so as to be sure to add wit-bindgen to the dependencies of the right project. Alternatively, you can directly add wit-bindgen to the dependencies section of the Cargo.toml.
It is also possible to use wit-bindgen as a binary via the wit-bindgen-cli
crate, but here we will focus on a code-first binding build approach.
Once you have wit-bindgen as a part of your Rust project (i.e. in Cargo.toml), we can use it to generate Rust code bindings for our WIT interface. Update your src/lib.rs file to look like the following:
#![allow(unused)] fn main() { mod bindings { //! This module contains generated code for implementing //! the `adder` world in `wit/world.wit`. //! //! The `path` option is actually not required, //! as by default `wit_bindgen::generate` will look //! for a top-level `wit` directory and use the files //! (and interfaces/worlds) there-in. wit_bindgen::generate!({ path: "wit/world.wit", }); } }
Here we create a module called bindings that contains the code output by the wit_bindgen::generate macro.
Various structs, interfaces, enums and more might be generated by wit_bindgen, so it's often desirable to sequester those new
types to a module that can be referred to later.
At present, the code won't do much, but that's because we haven't added our implementation yet.
5. Implementing the adder world via the generated Guest Trait
We can fill in functionality of the component by implementing bindings::Guest trait in src/lib.rs.
Your code should look something like the following:
mod bindings {
//! This module contains generated code for implementing
//! the `adder` world in `wit/world.wit`.
//!
//! The `path` option is actually not required,
//! as by default `wit_bindgen::generate` will look
//! for a top-level `wit` directory and use the files
//! (and interfaces/worlds) there-in.
// The line below will be expanded as Rust code containing
wit_bindgen::generate!({
path: "wit/adder/world.wit",
});
// In the lines below we use the generated `export!()` macro re-use and
use super::AdderComponent;
export!(AdderComponent);
}
/// Struct off of which the implementation will hang
///
/// The name of this struct is not significant.
struct AdderComponent;
impl bindings::exports::docs::adder::add::Guest for AdderComponent {
fn add(x: u32, y: u32) -> u32 {
x + y
}
}
There are a few points of note in the code listing above:
- The
AdderComponentstruct is introduced, but is only useful as an implementer of theGuesttrait. - The
bindings::exports::docs::adder::add::Guesttrait mirrors thedocs:adder/addinterface that is exported. - Given (1) and (2),
AdderComponentimplements (in the WIT sense) theadderworld, via the generated bindings. - The
export!()macro is generated bywit_bindgen::generate!macro, and does important setup.export!is easiest used from inside thebindingsmodule, but we need to refer to thesuper::AdderComponentstruct
note
To dive into the code generated by the wit_bindgen::generate! macro, you can use the cargo-expand crate
6. Building a Component
Now, let's build our component, using the native Rust toolchain to build a WASI P2 component.
cargo build --target=wasm32-wasip2
This performs a debug build, which produces a WebAssembly component to target/wasm32-wasip2/debug/adder.wasm:
du -hs target/wasm32-wasip2/debug/adder.wasm
3.3M target/wasm32-wasip2/debug/adder.wasm
3 megabytes is large for a WebAssembly component for a compiled language like Rust. Let's compile in release mode, performing more optimizations:
cargo build --target=wasm32-wasip2 --release
After compiling in release mode, we get a much smaller binary:
$ du -hs target/wasm32-wasip2/release/adder.wasm
16K target/wasm32-wasip2/release/adder.wasm
Note that you can use many of the optimization options normally available with the Rust toolchain to control binary output.
warning
Building with --release removes all debug-related information from the resulting .wasm file.
When prototyping or testing locally, you might want to avoid --release to
obtain useful backtraces in case of errors (for example, with
wasmtime::WasmBacktraceDetails::Enable).
Note: the resulting .wasm file will be considerably larger (likely 4MB+).
7. Inspecting the built component
Now that we have a WIT binary, we can introspect it using WebAssembly component tooling.
For example, we can wasm-tools to output the WIT package of the component, because WebAssembly
components are self-documenting, and contain this information:
wasm-tools component wit target/wasm32-wasip2/release/adder.wasm
The command above should produce the output below:
package root:component;
world root {
export docs:adder/add@0.1.0;
}
package docs:adder@0.1.0 {
interface add {
add: func(x: u32, y: u32) -> u32;
}
}
8. Running the adder Component
To verify that our component works, let's run it from a Rust application that knows how to run a
component targeting the adder world.
The application uses wasmtime to generate Rust "host"/"embedder" bindings,
bring in WASI worlds, and execute the component.
With the component-docs repository cloned locally, run the following:
$ cd examples/example-host
$ cargo run --release -- 1 2 ../add/target/wasm32-wasip1/release/adder.wasm
1 + 2 = 3
With this, we have successfully built and run a basic WebAssembly component with Rust 🎉