Create and Compose WebAssembly Components: A Hands-On Example with Rust
Create and Compose WebAssembly Components: A Hands-On Example with Rust
The WebAssembly Component Model is an emerging standard that makes WebAssembly modules more interoperable and composable across languages and runtimes. Instead of passing raw memory buffers and function pointers between modules, the component model introduces a higher-level system of interfaces, imports, and exports defined using WIT (WebAssembly Interface Types).
In this article, we’ll walk through a concrete example: building a simple decider program in Rust that chooses between two numbers. The project is split into two components:
- Library component (
first
) – implements adecide
interface. - Command component (
decider
) – imports thedecide
interface and runs it as a CLI program.
Finally, we’ll compose the two into a single runnable WebAssembly artifact and execute it with Wasmtime.
Tooling Setup
We’ll use the Bytecode Alliance toolchain:
- cargo-component: scaffolding, bindings, and building Rust components
- wasm-tools: inspecting and validating Wasm modules
- wac: wiring components together (plugging imports into exports)
- wasmtime: runtime for Wasm components
Install them:
cargo install --locked cargo-componentcargo install --locked wasm-toolscargo install --locked wac-clibrew install wasmtime
Step 1: Library Component (first
)
First, we’ll create a library component that provides a decide
function.
# from the root foldercargo component new first --lib && cd first
Defining the Interface
Edit wit/world.wit
to define the decide
interface:
package component:first;
interface decide { decide: func(x: u32, y: u32) -> u32;}
world first { export decide;}
Implementing in Rust
Update the bindings:
cargo component bindings
Then edit src/lib.rs
:
#[allow(warnings)]mod bindings;
use bindings::exports::component::first::decide::Guest;
struct Component;
impl Guest for Component { fn decide(x: u32, _y: u32) -> u32 { x }}
Build and inspect:
cargo component build --releasewasm-tools component wit target/wasm32-wasip1/release/first.wasm
Step 2: Command Component (decider
)
Next, we’ll create a command component that imports the decide
function and uses it in a CLI program.
cargo component new decider && cd decider
Define the World
Create wit/world.wit
:
package component:decider;
world decider { import component:first/decide;}
Update Cargo.toml
to wire the dependency:
[package.metadata.component.target]path = "wit"
[package.metadata.component.target.dependencies]"component:first" = { path = "../first/wit" }
Add CLI Parsing
Add clap:
cargo add clap -F derive
Update bindings:
cargo component bindings
Then modify src/main.rs
:
#[allow(warnings)]mod bindings;
use bindings::component::first::decide::decide;use clap::Parser;
#[derive(Parser)]struct Command { x: u32, y: u32,}
impl Command { fn run(self) { let result = decide(self.x, self.y); println!("{} or {} was decided to {result}", self.x, self.y); }}
fn main() { Command::parse().run()}
Build and check:
cargo component build --releasewasm-tools component wit target/wasm32-wasip1/release/decider.wasm
Step 3: Composition
Now we connect the command component (decider
) to the library component (first
) using wac plug
.
# from the root folderwac plug \ decider/target/wasm32-wasip1/release/decider.wasm \ --plug first/target/wasm32-wasip1/release/first.wasm \ -o final.wasm
Step 4: Run the Final Component
Finally, run it with Wasmtime:
wasmtime run final.wasm 2 1
Output:
2 or 1 was decided to 2
Conclusion
This small project demonstrates how to:
- Define a WIT interface (
decide
). - Implement it in Rust with
cargo-component
. - Import and use it in another component.
- Compose components with
wac plug
. - Run the result in Wasmtime.
The WebAssembly Component Model brings structured composition and language interoperability to Wasm. Even in this toy example, we see how easily a reusable library component (first
) can be cleanly integrated into another program (decider
)—a foundation for building polyglot, portable systems.