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

Component Model — typed strings and structs

wasmtime-badge wasmtime-wasi-badge wit-bindgen-badge cat-wasm-badge

Core WebAssembly function parameters are limited to i32, i64, f32, and f64. The WebAssembly Component Model removes that restriction by adding a language-neutral interface description language called WIT. Strings, lists, records, and variants all become first-class types that cross the guest/host boundary without manual pointer arithmetic.

This recipe requires two extra tools:

cargo install cargo-component
rustup target add wasm32-wasip2

Define the interface in WIT

A WIT world declares what a component exports and imports. This one exports a single greet function that takes and returns a string.

package example:greeter@0.1.0;

world greeter {
    export greet: func(name: string) -> string;
}

Implement the guest

wit_bindgen::generate! reads the WIT file at compile time and emits a Guest trait. The concrete type Component implements it, and export! wires it up as the component’s entry point.

wit_bindgen::generate!({ world: "greeter" });

struct Component;

impl Guest for Component {
    fn greet(name: String) -> String {
        format!("Hello, {name}!")
    }
}

export!(Component);

Build the guest into a .wasm component:

cd crates/wasm_component_guest
cargo component build --release --target wasm32-wasip2

Run it from the host

The host uses wasmtime::component::bindgen! — the same WIT file, read at compile time — to generate a typed Greeter wrapper. Config enables the component model, wasmtime_wasi::p2::add_to_linker_sync wires up WASI Preview 2 (required even for guests that don’t use WASI directly), and Greeter::instantiate produces a handle whose call_greet method accepts a &str and returns a String.

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

bindgen!({
    world: "greeter",
    path: "../wasm_component_guest/wit/world.wit",
});

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

impl WasiView for State {
    fn ctx(&mut self) -> WasiCtxView<'_> {
        WasiCtxView {
            ctx: &mut self.wasi,
            table: &mut self.table,
        }
    }
}

fn main() -> Result<()> {
    let mut config = Config::new();
    config.wasm_component_model(true);
    let engine = Engine::new(&config)?;

    let component = Component::from_file(
        &engine,
        "crates/wasm_component_guest/target/wasm32-wasip2/release/greeter.wasm",
    )?;

    let mut linker: Linker<State> = Linker::new(&engine);
    wasmtime_wasi::p2::add_to_linker_sync(&mut linker)?;

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

    let greeter = Greeter::instantiate(&mut store, &component, &linker)?;
    let message = greeter.call_greet(&mut store, "WebAssembly")?;
    println!("{message}");
    Ok(())
}

Run from the repo root (after building the guest):

cargo run --manifest-path crates/wasm_component_host/Cargo.toml
# Hello, WebAssembly!