Wasm Component Model By Example
It’s time to get some practice after answering what the Wasm component model is.
You can find the code at GitHub
Preparations
Before we start, prepare the environment:
- You need the latest version of Rust.
- Install cargo component:
cargo install cargo-component
- Install wasm-tools
cargo install wasm-tools
- Install a Wasm runtime which supports the component model, like wasmtime:
curl https://wasmtime.dev/install.sh -sSf | bash
Overview of the application
The application is simple: A component string-operations which exports operations on strings. In this example it’s a function that takes one string parameter and returns its length.
string-operations will be a component.
To consume and use the component, an application will be created. This will be called app.
So, in a directory create first the project directory for string-operations and then for app:
cargo component new string-operations --lib
cargo component new app
We’ll see later what the difference between these two is.
The string-operations component
Wasm components are described by the WIT IDL. You can read more about them in the official guide. For this post it’s enough to know that .wit files describe which imports a component needs and which exports it provides.
A component is referred by the term world.
The world.wit
For the string-operations component the wit/world.wit looks like following:
package dkwr:[email protected];
interface len {
len: func(s: string) -> u32;
}
world string-operations {
export len;
}
The component must export an interface to be used by other components.
This describes how the component interacts with other components.
The Cargo.toml
The Cargo.toml contains information about the component for implementing, using and building it.
The full definition of how a Cargo.toml can look like can be found in the repository of cargo-component.
For string-operations you need to define which WIT is targeted:
[package.metadata.component.target]
path = "<path>"
world = "<world>"
In this example this leads to:
[package.metadata.component.target]
path = "wit"
world = "string-operations"
More about Cargo.toml table fields
The WIT resolution is quite confusing and I just write what I know in this section. For other cases, take a look into the design specification of the repository.
The code from above is used for a local .wit file. But there are also ways to target a WIT from a registry.
When you want to distribute a component, you need to define metadata like the name. For example:
[package.metadata.component]
package = "dkwr:string-operations"
Also nice to know: It’s not needed name the .wit file world.wit as cargo-component is scanning the whole path for .wit files.
The implementation
The implementation happens inside of string-operations/src/lib.rs:
#[allow(warnings)]
mod bindings;
use crate::bindings::exports::dkwr::stringoperations::len::Guest;
struct Component;
impl Guest for Component {
fn len(s: String) -> u32 {
s.len().try_into().unwrap()
}
}
bindings::export!(Component with_types_in bindings);
What happens here is, that cargo component generates a bindings.rs file with interfaces and the Guest trait for len. That needs to be imported and implemented.
cargo component defines in a nicer way:
To be able to use a WebAssembly component from any particular programming language, bindings must be created by translating a WebAssembly component’s interface to a representation that a specific programming language can understand.
Building the component
Build the component with cargo:
(cd string-operations && cargo component build --release)
Using a component in app
app will be using the component by importing the WIT definition and use that information in Rust code.
The world.wit
This is how the world.wit looks like for app:
package dkwr:[email protected];
world app {
import dkwr:stringoperations/[email protected];
}
This defines the world app and imports the len interface from the package stringoperations.
The Cargo.toml
As in string-operations, define the path of the .wit files and the package name. (Where the package is optional, but we add it for completeness.)
[package.metadata.component]
package = "dkwr:app"
[package.metadata.component.target]
path = "wit"
world = "app"
Furthermore, the locations of the component dependencies need to be added. This is done in [package.metadata.component.target.dependencies]:
[package.metadata.component.target.dependencies]
"dkwr:stringoperations" = { path = "../string-operations/wit" }
Note: Check the documentation to see how to add dependencies from registries.
The implementation of app
app was created without the --lib flag, which means, that it’s an application and not a library. So it has a main.rs instead a lib.rs file where the implementation belongs.
As in the string-operations component, cargo component creates bindings with the help of the properties and the WIT file.
These can be used to import the interface and the function. See the use crate expression.
The rest of the function is Rust code as usual: Reading the given args, calling len and printing the result.
#[allow(warnings)]
mod bindings;
use crate::bindings::dkwr::stringoperations::len::len;
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() != 2 {
println!("Usage: {} <string>", args[0]);
return;
}
let input_string = &args[1];
let result = len(input_string);
println!("{}", result);
}
Building and composing app
To build app, execute the same command as before:
(cd app && cargo component build --release)
Now, to get a fully functioning Wasm binary, wasm-tools is needed to compose the two Wasm components into one.
But, before this can be done, the string_operations.wasm file needs to be renamed to use kebab-case (don’t ask me why this is needed):
(cd string-operations/target/wasm32-wasi/release/; mv string_operations.wasm string-operations.wasm)
Composition is done with wasm-tools compose.
The syntax is:
wasm-tools compose path/to/component.wasm -d path/to/dep1.wasm -d path/to/dep2.wasm -o composed.wasm
So, execute the following:
(wasm-tools compose app/target/wasm32-wasi/release/app.wasm -d string-operations/target/wasm32-wasi/release/string-operations.wasm -o out.wasm)
Running app
Now, it’s time to execute the Wasm binary. Use wasmtime for this and pass a String to this:
$ wasmtime out.wasm "Hello, World"
12
Conclusion
This post showed how to create a Wasm component and use it from a host. All by the help of WIT IDL and cargo component. In the last step the outputs need to be composed by wasm-tools compose.
There are more concepts, like composing more complex components, using registries or the command component. But for the beginning this should be helpful and a good starting point to understand these concepts.