Using WIT Resources (Rust)
Resources are handles to entities that live outside the component (i.e. in a host, or other component).
An example stack-based Reverse Polish Notation (RPN) calculator
In this section, our example resource will be a Reverse Polish Notation (RPN) calculator. (Engineers of a certain vintage will remember this from handheld calculators of the 1970s.)
A RPN calculator is a stateful entity: a consumer pushes operands and operations onto a stack maintained within the calculator, then evaluates the stack to produce a value.
In WIT, the resource looks like the following:
package docs:rpn@0.1.0;
interface types {
enum operation {
add,
sub,
mul,
div
}
resource engine {
constructor();
push-operand: func(operand: u32);
push-operation: func(operation: operation);
execute: func() -> u32;
}
}
world calculator {
export types;
}
Implementing and exporting a resource in a component
To implement the calculator in Rust:
-
Create a library component as shown in previous sections, with the WIT given above.
-
Define a Rust
structto represent the calculator state:#![allow(unused)] fn main() { use std::cell::RefCell; struct CalcEngine { stack: RefCell<Vec<u32>>, } }Why is the stack wrapped in a
RefCell? As we will see, the generated Rust trait for the calculator engine has immutable references toself. But our implementation of that trait will need to mutate the stack. So we need a type that allows for interior mutability, such asRefCell<T>orArc<RwLock<T>>. -
The generated bindings (
bindings.rs) for an exported resource include a trait namedGuestX, whereXis the resource name. For the calculatorengineresource, the trait isGuestEngine. Implement this trait on thestructfrom step 2:#![allow(unused)] fn main() { mod bindings { use super::Component; wit_bindgen::generate!(); export!(Component); } use bindings::exports::docs::rpn::types::{GuestEngine, Operation}; impl GuestEngine for CalcEngine { fn new() -> Self { CalcEngine { stack: RefCell::new(vec![]) } } fn push_operand(&self, operand: u32) { self.stack.borrow_mut().push(operand); } fn push_operation(&self, operation: Operation) { let mut stack = self.stack.borrow_mut(); let right = stack.pop().unwrap(); // TODO: error handling! let left = stack.pop().unwrap(); let result = match operation { Operation::Add => left + right, Operation::Sub => left - right, Operation::Mul => left * right, Operation::Div => left / right, }; stack.push(result); } fn execute(&self) -> u32 { self.stack.borrow_mut().pop().unwrap() // TODO: error handling! } } } -
We now have a working calculator type which implements the
enginecontract, but we must still connect that type to theengineresource type. This is done by implementing the generatedGuesttrait. For this WIT, theGuesttrait contains nothing except an associated type. You can use an emptystructto implement theGuesttrait on. Set the associated type for the resource - in our case,Engine- to the type which implements the resource trait - in our case, theCalcEnginestructwhich implementsGuestEngine. Then use theexport!macro to export the mapping:#![allow(unused)] fn main() { // ... bindings & CalcEngine impl code ... struct Component; impl bindings::Guest for Component { type Engine = CalcEngine; } bindings::export!(Component); }
This completes the implementation of the calculator engine resource. Run cargo build --target=wasm32-wasip2 to create a component .wasm file.
Importing and consuming a resource in a component
To use the calculator engine in another component, that component must import the resource.
-
Create a runnable component as shown in previous sections.
-
Add a
wit/component.witto your project, and write a WIT world that imports the RPN calculator types:package docs:rpn-cmd; world app { import docs:rpn/types@0.1.0; } -
Create a
wkg.tomlfile to enable retrieving the relevant WIT files fordocs:rpn(which contains theengineresource):[overrides] "docs:rpn" = { path = "../path/to/docs-rpn/wit" }After doing this, you can run
wkg wit fetchto ensure all WIT is available locally. -
The resource now appears in the generated bindings as a
struct, with appropriate associated functions. Use these to construct a test app:mod bindings { use super::Component; wit_bindgen::generate!(); export!(Component); } use bindings::docs::rpn::types::{Engine, Operation}; fn main() { let calc = Engine::new(); calc.push_operand(1); calc.push_operand(2); calc.push_operation(Operation::Add); let sum = calc.execute(); println!("{sum}"); }
Building the component as is creates a WebAssembly component with an unsatisfied import -- namely the docs:rpn/types import.
After building the component, it must be composed with a .wasm component that implements the resource.. After composition creates a component with no unsatisfied imports, the composed command component can be run with wasmtime run.
Alternatively, a host that can provide the docs:rpn/types import (and related resource) can also be used to run the component
in it's "incomplete" state (as the host will "complete" the componnt by providing the expected import).
Implementing and exporting a resource implementation in a host
If you are hosting a Wasm runtime, you can export a resource from your host for guests to consume.
Hosting a runtime is outside the scope of this book, so we will give only a broad outline here. This is specific to the Wasmtime runtime; other runtimes may express things differently.
-
Use
wasmtime::component::bindgen!to specify the WIT you are a host for:#![allow(unused)] fn main() { wasmtime::component::bindgen!({ path: "../wit" }); } -
Tell
bindgen!how you will represent the resource in the host via thewithfield. This can be any Rust type. For example, the RPN engine could be represented by aCalcEnginestruct:#![allow(unused)] fn main() { wasmtime::component::bindgen!({ path: "../wit", with: { "docs:rpn/types/engine": CalcEngine, } }); }If you don't specify the host representation for a resource, it defaults to an empty enum. This is rarely useful as resources are usually stateful.
-
If the representation type isn't a built-in type, define it:
#![allow(unused)] fn main() { struct CalcEngine { /* ... */ } } -
As a host, you will already be implementing a
Hosttrait. You will now need to implement aHostXtrait (whereXis the resource name) on the same type as theHosttrait:#![allow(unused)] fn main() { impl docs::rpn::types::HostEngine for MyHost { fn new(&mut self) -> wasmtime::component::Resource<docs::rpn::types::Engine> { /* ... */ } fn push_operand(&mut self, self_: wasmtime::component::Resource<docs::rpn::types::Engine>) { /* ... */ } // etc. } }Important: You implement this on the 'overall' host type, not on the resource representation! Therefore, the
selfreference in these functions is to the 'overall' host type. For instance methods of the resource, the instance is identified by a second parameter (self_), of typewasmtime::component::Resource. -
Add a
wasmtime::component::ResourceTableto the host:#![allow(unused)] fn main() { struct MyHost { calcs: wasmtime::component::ResourceTable, } } -
In your resource method implementations, use this table to store and access instances of the resource representation:
#![allow(unused)] fn main() { impl docs::rpn::types::HostEngine for MyHost { fn new(&mut self) -> wasmtime::component::Resource<docs::rpn::types::Engine> { self.calcs.push(CalcEngine::new()).unwrap() // TODO: error handling } fn push_operand(&mut self, self_: wasmtime::component::Resource<docs::rpn::types::Engine>) { let calc_engine = self.calcs.get(&self_).unwrap(); // calc_engine is a CalcEngine - call its functions } // etc. } }