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.
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.
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
borrowafter the call. The runtime traps when the call returns with outstanding borrows. Store anowninstead. - Mutating through
&selfwithout interior mutability. It does not compile. UseRefCellorCell. - Never disposing handles in JavaScript. Guest memory is held until the finalizer runs. Use
usingor 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.
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.
Related
- Understanding the canonical ABI — how values and handles are lowered.
- Building a component in Rust with cargo-component — the guest project.
- Running components in wasmtime — hosts that implement resources.
- Exporting Rust structs as JavaScript classes — the wasm-bindgen equivalent.
← Back to Wasm Component Model & WIT Bindings