-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #140 from itowlson/resources-rust
Working with records and resources in Rust
- Loading branch information
Showing
1 changed file
with
263 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -275,3 +275,266 @@ fn main() { | |
$ wasmtime run ./my-composed-command.wasm | ||
1 + 1 = 579 # might need to go back and do some work on the calculator implementation | ||
``` | ||
|
||
## Using user-defined types | ||
|
||
[User-defined types](../design/wit.md#user-defined-types) map to Rust types as follows. | ||
|
||
| WIT type | Rust binding | | ||
|------------|--------------| | ||
| `record` | `struct` with public fields corresponding to the record fields | | ||
| `variant` | `enum` with cases corresponding to the variant cases | | ||
| `enum` | `enum` with cases corresponding to the enum cases, with no data attached | | ||
| `resource` | [See below](#using-resources) | | ||
| `flags` | Opaque type supporting bit flag operations, with constants for flag values | | ||
|
||
For example, consider the following WIT: | ||
|
||
```wit | ||
interface types { | ||
enum operation { | ||
add, | ||
sub, | ||
mul, | ||
div | ||
} | ||
record expression { | ||
left: u32, | ||
operation: operation, | ||
right: u32 | ||
} | ||
eval: func(expr: expression) -> u32; | ||
} | ||
``` | ||
|
||
When exported from a component, this could be implemented as: | ||
|
||
```rust | ||
impl Guest for Implementation { | ||
fn eval(expr: Expression) -> u32 { | ||
// Record fields become public fields on a struct | ||
let (l, r) = (expr.left, expr.right); | ||
match expr.operation { | ||
// Enum becomes an enum with only unit cases | ||
Operation::Add => l + r, | ||
Operation::Sub => l - r, | ||
Operation::Mul => l * r, | ||
Operation::Div => l / r, | ||
} | ||
} | ||
} | ||
``` | ||
|
||
## Using resources | ||
|
||
[Resources](../design/wit.md#resources) are handles to entities that live outside the component, for example in a host, or in a different component. | ||
|
||
### Example | ||
|
||
In this section, our example resource will be a [Reverse Polish Notation (RPN)](https://en.wikipedia.org/wiki/Reverse_Polish_notation) 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. The resource in WIT looks like this: | ||
|
||
```wit | ||
package docs:[email protected]; | ||
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 using `cargo component`: | ||
|
||
1. Create a library component as shown in previous sections, with the WIT given above. | ||
|
||
2. Define a Rust `struct` to represent the calculator state: | ||
|
||
```rust | ||
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 to `self`. But our implementation of that trait will need to mutate the stack. So we need a type that allows for interior mutability, such as `RefCell<T>` or `Arc<RwLock<T>>`. | ||
3. The generated bindings (`bindings.rs`) for an exported resource include a trait named `GuestX`, where `X` is the resource name. (You may need to run `cargo component build` to regenerate the bindings after updating the WIT.) For the calculator `engine` resource, the trait is `GuestEngine`. Implement this trait on the `struct` from step 2: | ||
|
||
```rust | ||
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! | ||
} | ||
} | ||
``` | ||
|
||
4. We now have a working calculator type which implements the `engine` contract, but we must still connect that type to the `engine` resource type. This is done by implementing the generated `Guest` trait. For this WIT, the `Guest` trait contains nothing except an associated type. You can use an empty `struct` to implement the `Guest` trait on. Set the associated type for the resource - in our case, `Engine` - to the type which implements the resource trait - in our case, the `CalcEngine` `struct` which implements `GuestEngine`. Then use the `export!` macro to export the mapping: | ||
|
||
```rust | ||
struct Implementation; | ||
impl Guest for Implementation { | ||
type Engine = CalcEngine; | ||
} | ||
|
||
bindings::export!(Implementation with_types_in bindings); | ||
``` | ||
|
||
This completes the implementation of the calculator `engine` resource. Run `cargo component build` 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. | ||
|
||
1. Create a command component as shown in previous sections. | ||
|
||
2. Add a `wit/world.wit` to your project, and write a WIT world that imports the RPN calculator types: | ||
|
||
```wit | ||
package docs:rpn-cmd; | ||
world app { | ||
import docs:rpn/[email protected]; | ||
} | ||
``` | ||
|
||
3. Edit `Cargo.toml` to tell `cargo component` about the new WIT file and the external RPN package file: | ||
|
||
```toml | ||
[package.metadata.component] | ||
package = "docs:rpn-cmd" | ||
|
||
[package.metadata.component.target] | ||
path = "wit" | ||
|
||
[package.metadata.component.target.dependencies] | ||
"docs:rpn" = { path = "../wit" } # or wherever your resource WIT is | ||
``` | ||
|
||
4. The resource now appears in the generated bindings as a `struct`, with appropriate associated functions. Use these to construct a test app: | ||
|
||
```rust | ||
#[allow(warnings)] | ||
mod bindings; | ||
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}"); | ||
} | ||
``` | ||
|
||
You can now build the command component and [compose it with the `.wasm` component that implements the resource.](../creating-and-consuming/composing.md). You can then run the composed command with `wasmtime run`. | ||
|
||
### 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. | ||
|
||
1. Use `wasmtime::component::bindgen!` to specify the WIT you are a host for: | ||
|
||
```rust | ||
wasmtime::component::bindgen!({ | ||
path: "../wit" | ||
}); | ||
``` | ||
|
||
2. Tell `bindgen!` how you will represent the resource in the host via the `with` field. This can be any Rust type. For example, the RPN engine could be represented by a `CalcEngine` struct: | ||
|
||
```rust | ||
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. | ||
3. If the representation type isn't a built-in type, define it: | ||
|
||
```rust | ||
struct CalcEngine { /* ... */ } | ||
``` | ||
|
||
4. As a host, you will already be implementing a `Host` trait. You will now need to implement a `HostX` trait (where `X` is the resource name) _on the same type_ as the `Host` trait: | ||
|
||
```rust | ||
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 `self` reference 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 type `wasmtime::component::Resource`. | ||
|
||
5. Add a `wasmtime::component::ResourceTable` to the host: | ||
|
||
```rust | ||
struct MyHost { | ||
calcs: wasmtime::component::ResourceTable, | ||
} | ||
``` | ||
|
||
6. In your resource method implementations, use this table to store and access instances of the resource representation: | ||
|
||
```rust | ||
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. | ||
} | ||
``` |