Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Part 1: Hello, Components

WebAssembly started as a way to run code in the browser — a low-level virtual machine that takes linear memory and a handful of numeric types. That’s fine for C and C++, where everything is bytes anyway. But if you want to pass a string, a struct, or an error across the Wasm boundary, you’re on your own: manually write bytes into memory, pass a pointer, and hope you got the layout right.

The Component Model fixes this. It adds a type system to Wasm itself — strings, records, enums, variants, resources — and a standard way to pass them between modules and hosts. No pointer arithmetic. No unsafe. The types work — no manual plumbing required.

The type system does more than make host↔plugin calls nicer. It makes components composable — you can wire two components together through a shared WIT interface, and the type checker confirms they fit before you ever run them. That’s why it’s called a component model, not merely a better FFI. We’ll build up to composition in Parts 5 and 6; for now, let’s see the basics.

Here’s what that looks like in practice. Instead of reconstructing strings with from_raw_parts inside unsafe blocks, we skip the plumbing and write the logic directly. Let’s build a plugin that takes a string and returns its length — the only code we write is the actual logic.

Version compatibility. This tutorial uses wasmtime 29 and wit-bindgen 0.41. Both have seen significant releases since, but the concepts (WIT files, generated bindings, resources, composition) are stable — only the Rust API surface shifts. If you’re on a newer wasmtime, the two biggest changes happened at v30 (WasiView split into IoView + WasiView) and v36 (WasiCtxView return type, p2 linker). See the Migration Guide for before/after code. Each part also includes version-specific notes where things are most likely to break.

Check your setup before you start:

rustup target list --installed | grep wasip2   # should show wasm32-wasip2
wasm-tools --version                           # should print a version number

If the target is missing, rustup target add wasm32-wasip2 will get you there. If wasm-tools isn’t found, install it with cargo install wasm-tools (or your system’s package manager — the wasm-tools --version command above will confirm it works regardless of how you installed it). You’ll also need wasm-component-ld for linking — covered in the build step below (automatic with Rust 1.85+).

The Interface

The Component Model uses WIT files to define the contract between host and plugin. Think of it as a language-agnostic interface definition — like a .proto file, but for Wasm. A WIT world defines that contract: what the component exports (functions the host can call) and imports (functions the host must provide).

Create a workspace for this part:

mkdir -p part-1/plugin/wit

Write the WIT file at part-1/plugin/wit/world.wit:

package example:plugin;

world example-plugin {
    export length: func(s: string) -> u32;
}

This says: our plugin exports a function called length that takes a string and returns a u32. That’s the whole interface. No pointers, no memory layout conventions, no “write the length to byte index 1” handshake.

The Plugin

Create the plugin project:

cd part-1/plugin
cargo init --lib

Update Cargo.toml:

[package]
name = "example-plugin"
version = "0.1.0"
edition = "2021"

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

[dependencies]
wit-bindgen = "0.41"

The cdylib crate type tells cargo to produce a C dynamic library, which is what the Wasm target expects. And wit-bindgen generates the bindings from our WIT file.

Now the actual code — src/lib.rs:

#![allow(unused)]
fn main() {
wit_bindgen::generate!({
    world: "example-plugin",
});

struct ExamplePlugin;

impl Guest for ExamplePlugin {
    fn length(s: String) -> u32 {
        s.len() as u32
    }
}

export!(ExamplePlugin);
}

Let’s break this down. wit_bindgen::generate! reads the wit/ directory, finds our world, and generates a Guest trait matching the exports we defined. We implement Guest for our type — that’s where the actual logic lives. Then export! registers our implementation as the component’s exports.

Without the Component Model, you’d need a separate _length wrapper function with unsafe pointer arithmetic. Here, we implement a trait.

Build the Plugin

Before we build, you need the WASI target and a component linker. The setup check at the top of this part covers both — if you ran those commands, you’re ready. One additional dependency: wasm-component-ld for linking components. If you’re using Rust 1.85 or later, this is handled automatically — cargo build --target wasm32-wasip2 pulls it in as a dependency. If you’re on an older toolchain:

cargo install wasm-component-ld

Now build:

cargo build --target wasm32-wasip2

The output is at target/wasm32-wasip2/debug/example_plugin.wasm. This is a full component — not a core Wasm module. You can inspect it:

wasm-tools component wit target/wasm32-wasip2/debug/example_plugin.wasm

You should see the same world definition we wrote in our WIT file.

The Host

Now let’s build a host that loads and runs this component.

cd part-1/host
cargo init

Cargo.toml:

[package]
name = "example-host"
version = "0.1.0"
edition = "2021"

[dependencies]
wasmtime = "29"
wasmtime-wasi = "29"
anyhow = "1"

The host also needs the WIT file so wasmtime::component::bindgen! can generate typed bindings at compile time. The bindgen! macro takes a path argument that points to the WIT directory — we’ll use path: "../plugin/wit" to reference the plugin’s WIT directly, so there’s no need to copy the file.

Now src/main.rs:

use anyhow::Result;
use wasmtime::{Engine, Store};
use wasmtime::component::{Component, Linker};
use wasmtime_wasi::{WasiCtxBuilder, WasiCtx, WasiView, ResourceTable};

wasmtime::component::bindgen!({
    world: "example-plugin",
    path: "../plugin/wit",
});

struct State {
    wasi: WasiCtx,
    table: ResourceTable,
}

impl WasiView for State {
    fn ctx(&mut self) -> &mut WasiCtx { &mut self.wasi }
    fn table(&mut self) -> &mut ResourceTable { &mut self.table }
}

fn main() -> Result<()> {
    let engine = Engine::default();
    let mut linker = Linker::new(&engine);

    // Add WASI support — the plugin targets wasm32-wasip2 which requires WASI
    wasmtime_wasi::add_to_linker_sync(&mut linker)?;

    let state = State {
        wasi: WasiCtxBuilder::new().build(),
        table: ResourceTable::new(),
    };
    let mut store = Store::new(&engine, state);

    let bytes = std::fs::read("../plugin/target/wasm32-wasip2/debug/example_plugin.wasm")?;
    let component = Component::new(&engine, bytes)?;

    let instance = linker.instantiate(&mut store, &component)?;

    let plugin = ExamplePlugin::new(&mut store, &instance)?;

    let result = plugin.call_length(&mut store, "supercalifragilisticexpialidocious")?;
    println!("length: {result}");

    Ok(())
}

The WasiView trait connects WASI context to the store. Since our plugin targets wasm32-wasip2, it needs WASI available — even though our plugin doesn’t use any WASI features directly, the component runtime requires the WASI plumbing to be present.

If you’re on wasmtime 30+, the WasiView trait split into IoView + WasiView. See the Migration Guide for the updated trait signatures. This tutorial’s code targets wasmtime 29.

Run it:

cargo run
# length: 34

What Just Happened

Without the Component Model, getting to this point required:

  1. Getting the module’s context
  2. Getting memory(0) from the context
  3. Getting a view of that memory
  4. Looping over memory cells to write string bytes
  5. Binding the _length function with raw type signatures
  6. Calling it with (ptr, len) as i32/u32
  7. Reconstructing the result

Now it’s:

#![allow(unused)]
fn main() {
plugin.call_length(&mut store, "supercalifragilisticexpialidocious")?;
}

The Component Model and wit-bindgen handled all the memory management. We described our interface in WIT, and both sides got generated, type-safe bindings. The host passes a Rust string. The plugin receives a Rust string. The result is a Rust u32.

That’s the entire point of this rewrite: the plumbing goes away.

Next: Part 2 — Passing Structured Data Across the Wasm Boundary — We can pass strings back and forth. But real plugins work with more than text — they work with structured data. Records, enums, and flags in WIT map to Rust structs, and the Component Model handles the serialization automatically. No unsafe, no pointer arithmetic, no praying the byte offsets are correct.