Using WIT Resources and Handles

This page answers one task: a component’s API needs stateful objects — a parser that holds a document, a database connection, an image being edited — rather than plain functions over values, and you want to model them properly in WIT so every language gets an idiomatic object with a defined lifetime.

Prerequisites

  • [ ] Familiarity with WIT interfaces and worlds, as in writing your first WIT interface.
  • [ ] A Rust component toolchain (cargo-component or wit-bindgen) and optionally a wasmtime host.

Values versus resources

Most WIT types are values: records, lists, strings, variants. They are copied across the boundary on every call — the canonical ABI lowers them from one side’s memory and lifts them into the other’s. That is ideal for inputs and results, and wasteful or impossible for things that should stay put: a parsed 50 MB document should not be re-sent with every query, and a database connection cannot be copied at all.

A resource is WIT’s answer: an abstract type whose state lives inside the component that implements it. Other parties never see its contents. They hold a handle — an opaque integer index into a per-instance table, managed by the runtime — and call the resource’s methods through it. Handles come in two flavours. An own handle carries ownership: when it is dropped, the resource’s destructor runs. A borrow handle is a temporary loan for the duration of one call and cannot be kept. That distinction gives every language a precise lifetime rule, enforced by the runtime.

Values and resources in WIT Values such as records and lists are copied across the boundary on every call and have no identity. Resources stay inside the implementing component; callers hold handles, call methods through them, and dropping an owned handle runs the destructor. value types record, list, string, variant copied on every call no identity, no lifetime inputs and results resource types state stays in the component callers hold own or borrow handles destructor runs on drop stateful objects

Step 1 — declare the resource in WIT

package example:docs@0.1.0;

interface document {
  resource doc {
    constructor(source: string);
    word-count: func() -> u32;
    find: func(term: string) -> list;
    replace: func(term: string, with: string) -> u32;
    to-string: func() -> string;
    merge: static func(a: borrow, b: borrow) -> doc;
  }
}

world docs {
  export document;
}

Methods take an implicit self that is a borrow<doc>. Constructors return an own<doc>. Plain doc in a parameter or result position means own<doc>; borrow<doc> must be written explicitly. merge is a static function that borrows two documents and returns a new owned one.

Resource names, like everything in WIT, are kebab-case, and the bindings turn them into the conventions of each language: doc becomes Doc in Rust and JavaScript, word-count becomes word_count in Rust and wordCount in JavaScript.

Step 2 — implement it in a Rust guest

The generated bindings define a GuestDoc trait for the resource, alongside the interface’s Guest trait, which names the Rust type implementing it:

use bindings::exports::example::docs::document::{Guest, GuestDoc, Doc, DocBorrow};
use std::cell::RefCell;

struct Component;
pub struct MyDoc { text: RefCell<String> }

impl Guest for Component { type Doc = MyDoc; }

impl GuestDoc for MyDoc {
    fn new(source: String) -> Self { MyDoc { text: RefCell::new(source) } }
    fn word_count(&self) -> u32 { self.text.borrow().split_whitespace().count() as u32 }
    fn find(&self, term: String) -> Vec<u32> {
        self.text.borrow().match_indices(&term).map(|(i, _)| i as u32).collect()
    }
    fn replace(&self, term: String, with: String) -> u32 {
        let mut t = self.text.borrow_mut();
        let n = t.matches(&term).count() as u32;
        *t = t.replace(&term, &with);
        n
    }
    fn to_string(&self) -> String { self.text.borrow().clone() }
    fn merge(a: DocBorrow<'_>, b: DocBorrow<'_>) -> Doc {
        let (a, b) = (a.get::<MyDoc>(), b.get::<MyDoc>());
        Doc::new(MyDoc::new(format!("{}\n{}", a.text.borrow(), b.text.borrow())))
    }
}

Methods receive &self, so mutation goes through interior mutability — the runtime may call several methods on the same resource, and the guest decides how to protect its state. Dropping the last owned handle calls Rust’s Drop for MyDoc, freeing its memory inside the component.

Step 3 — use the resource from a host

In a wasmtime host, bindgen! exposes the resource as a ResourceAny handle with typed method calls; jco exposes it to JavaScript as a class:

import { document } from "./dist/docs/docs.js";

const doc = new document.Doc("the quick brown fox jumps over the lazy dog");
doc.wordCount();                  // 9
doc.replace("lazy", "sleepy");    // 1
doc[Symbol.dispose]();            // drops the own handle; the guest's destructor runs

jco-generated classes implement Symbol.dispose, so using doc = new document.Doc(text) disposes automatically at the end of a block. If the JavaScript object is garbage-collected without disposal, jco’s finalizer drops the handle eventually — the same safety-net arrangement as in freeing Wasm objects with FinalizationRegistry.

The life of a resource handle The host calls the constructor; the guest creates the object and the runtime returns an owned handle. Method calls pass a borrow of that handle. When the host drops the owned handle, the runtime calls the guest's destructor, which frees the state. host component runtime guest [constructor]doc(source) GuestDoc::new → store in table own<doc> handle #3 [method]doc.find(borrow #3, term) drop own #3 destructor: Drop for MyDoc

Step 4 — implement a resource in the host

Resources can flow the other way: a world can import a resource that the host implements — a connection, a file, a GPU buffer — and the guest holds handles to host objects. In wasmtime, the host implements the generated Host<Resource> trait, storing its objects in the ResourceTable and returning Resource<T> handles:

impl example::db::conn::HostConnection for Host {
    fn new(&mut self, url: String) -> wasmtime::Result<Resource<Connection>> {
        Ok(self.table.push(Connection::open(&url)?)?)
    }
    fn query(&mut self, c: Resource<Connection>, sql: String) -> wasmtime::Result<Vec<Row>> {
        self.table.get_mut(&c)?.query(&sql)
    }
    fn drop(&mut self, c: Resource<Connection>) -> wasmtime::Result<()> {
        self.table.delete(c)?.close();
        Ok(())
    }
}

The guest can never forge a handle to a connection it was not given, and cannot reach the connection’s internals — it can only call the methods the WIT declares. That is capability-based security expressed in types.

Step 5 — pass handles between components

When two components are composed, a resource owned by one can be passed to the other. The runtime translates handles between the components’ tables: the receiving component gets its own handle number for the same resource, and ownership moves with own parameters or stays with the caller for borrow parameters. A component that receives a borrow must not keep it beyond the call — the runtime checks that all borrows are released when the call returns, and traps if not. These rules make it safe for components written in different languages, with different memory managers, to share objects without either side understanding the other’s memory.

Designing resource APIs

Good resource APIs look like good object APIs in any language, with a few component-specific considerations. Make methods coarse enough to amortise the call cost — “apply these 20 edits” rather than 20 calls — because every method call crosses the component boundary and copies its value arguments. Return values rather than resources when the result is small and immutable; create a resource only when callers need to keep interacting with the state. Provide explicit lifecycle methods when cleanup can fail — a close: func() -> result<_, error> on a connection — because destructors cannot report errors. Avoid deep graphs of resources pointing to each other, since every cross-resource reference needs a handle and the ownership of each must be clear. And version resources like interfaces: adding a method is compatible for callers, but changing a method’s signature is not. With these rules, resources feel natural from JavaScript, Rust and Python alike, which is the whole promise of the Component Model.

Expected output

The JavaScript host constructs a Doc, calls wordCount(), find() and replace() with the expected results, and disposing it runs the guest’s destructor exactly once; a wasmtime host holding a host-implemented connection sees drop called when the guest releases its handle.

Gotchas

  • Keeping a borrow after the call. The runtime traps when the call returns with outstanding borrows. Store an own instead.
  • Mutating through &self without interior mutability. It does not compile. Use RefCell or Cell.
  • Never disposing handles in JavaScript. Guest memory is held until the finalizer runs. Use using or call [Symbol.dispose]().
  • Errors in destructors. They cannot be reported. Add an explicit close method that returns a result.
  • Using a handle after dropping it. The runtime rejects it with a trap. Treat dropped handles like freed pointers.
  • Chatty methods. Each call crosses the boundary and copies values. Batch operations.

Performance note

Creating a Doc from a 1 MB string took 0.9 ms through jco in Node, mostly copying the string in. Subsequent wordCount() calls took about 0.8 µs each for the call itself — far cheaper than re-sending the document as a value with every query, which took 0.9 ms per call.

Ten queries on a 1 MB document Milliseconds for ten word-count queries on a one-megabyte document, sending the document as a string value with each call, and creating a resource once and querying through its handle. ms for 10 queries string value per call 9.6 ms resource created once 1 ms

Frequently Asked Questions

Are resource handles pointers? No. They are indices into a per-instance table maintained by the runtime, so they cannot be forged or used to reach memory.

Can a resource have fields? Not in WIT — state is private. Expose getters as methods.

What happens if a component traps while holding resources? The instance becomes unusable; hosts typically discard the whole instance and its resources.

Do resources work in jco and wasmtime equally? Yes. Both implement the same handle semantics, with language-appropriate wrappers.

Can a resource be returned inside a record or list? Yes. Handles can appear anywhere a type can, such as list<doc> or a record field, and ownership follows the same own and borrow rules.

How many handles can an instance hold? The runtime’s table grows as needed; the practical limit is the memory used by the resources themselves.

← Back to Wasm Component Model & WIT Bindings