Components
A Wado program compiles to a Component Model component. This chapter says how Wado values cross a component boundary, how a component waits, and how Wado declarations link to Component Model definitions and resources. Which world a component targets, and what its entry point is, is in Worlds and Entry Points.
Type Mapping at Component Boundaries
A Wado value that crosses a component boundary is lowered to a Component Model value, and lifted back on the other side, by the Canonical ABI.
The table below is the Wado↔CM correspondence, read in both directions: Wado→CM when generating a component's exported interface, and CM→Wado when importing an external component (use { Iface } from "./c.wasm" with { type: "wasm" }, see Wasm Module and Component Imports). CM types are written in their WIT spelling.
| Wado Type | CM Type at Boundary | Notes |
|---|---|---|
bool |
bool |
Boolean value |
char |
char |
Unicode scalar value |
i8, i16, i32, i64 |
s8, s16, s32, s64 |
Signed integers |
u8, u16, u32, u64 |
u8, u16, u32, u64 |
Unsigned integers |
i128, u128 |
record { low, high } |
Prelude structs, so each crosses as its own record |
f32, f64 |
f32, f64 |
Floating point |
String |
string |
UTF-8 string |
List<T> |
list<T> |
Dynamic array |
TreeMap<K, V> |
map<K, V> |
K is bool, char, String, or an integer; a repeated key takes the last pair's value |
[T1, T2, ...] |
tuple<T1, T2, ...> |
Tuple types |
Option<T> |
option<T> |
Optional value |
Result<T, E> |
result<T, E> |
Result type; result<ok> and bare result are the payload-elided forms |
struct { ... } |
record { ... } |
Record |
enum { ... } |
enum { ... } |
Enumeration without payloads |
variant { ... } |
variant { ... } |
Variant/sum type with payloads |
flags { ... } |
flags { ... } |
Bit flags |
type T = U |
type t = U |
Newtype, as a named alias of its base; a stdlib one too (ByteList is byte-list) |
resource |
resource |
Resource handle; owned and borrowed handles both map here |
Stream<T> |
stream<T> |
Component Model async stream |
Future<T> |
future<T> |
Component Model async future |
A function type has no row: it crosses only as a callback.
f16, bf16 (Half Precision) and
v128 have no Component Model type, so they do not cross a component boundary.
Neither does a SIMD lane type such as f32x4, since each is a newtype over
v128. An export fn whose signature names any of them is a compile error.
Every export fn is checked, not only a world's entry point, because each one
lands on the component's surface. A component that carries half precision data
exports its bits, as a List<u16>.
Rationale: WEP: Half-Precision Primitives.
Callbacks
A function type crosses the Component Model boundary in one place: as a
parameter of a #[cm] import, where it is a callback. Anywhere else it is a
compile error: in an export fn's parameters or result, in an import's result,
and inside a type that crosses, as a field or a payload (Option<fn(..)>
included).
#[cm("example:demo/target", linearity = "unrestricted")]
resource Target {
#[cm("example:demo/target#listen")]
#[cm_params("self", "listener")]
fn listen(&self, listener: fn mut(i32));
}
test {
let target = mock_target();
let mut seen: List<i32> = [];
with Target => &mut EchoTarget {} do {
target.listen(|v| seen.push(v));
}
assert seen == [7]; // `EchoTarget` calls the listener with 7
}
- A callback's parameters are scalars and handles, and it returns nothing. Any
other function type on a
#[cm]declaration is a compile error. - The closure stays in the guest. The call registers it and passes a
u32key in its place. One closure value has one key. - The host calls the closure back through the
wado:callback/callbackinterface, which the component exports. It holds one function per argument shape, namedcallfollowed by one word per argument: the argument's primitive type, orhandlefor a handle (call,call-i32,call-handle). Each takes the key first, is lifted synchronously, and returns nothing.wado witlists the interface among the world's exports. - The host may call a callback during an import call, which reenters the
component, or while
runis suspended. - A callback runs as a task of its own. It may perform an effect a world import backs.
- Where a Wado handler answers the
#[cm]interface instead of the host, the handler receives the closure itself and may call it.
Rationale: WEP: The Web Interface for Wado.
Concurrency Model
Wado follows the Component Model's concurrency model. It has no await: a
wait blocks the current task until the value is ready, so a function that waits
is not itself async.
Async Imports
A Component Model async func import is an interface operation declared
async fn op(...) -> AsyncCall<T>
(Async Operations). Calling it starts the
call and returns an AsyncCall<T> at once. The caller decides when to wait:
.wait()blocks until the call returns, then yields itsT..cancel()abandons the call..join(&set)adds the call to aWaitableSet, so one wait covers several calls and streams.
A function or closure that starts an async call also holds the effects these methods declare, so it may later wait on that call without declaring them. A closure is a body of its own: a call it starts grants nothing to the function around it.
use { Client, Request, Response, ErrorCode } from "wasi:http";
fn fetch(req: Request) -> Result<Response, ErrorCode> with Client {
let call = Client::send(req); // the request starts; nothing waits yet
// ... work here runs while the host handles the request ...
return call.wait(); // blocks until the response arrives
}
test {
let mut mock = MockClient {};
let response = with Client => &mut mock do { fetch(new_request()) };
assert response matches { Ok(_) };
}
An AsyncCall<T> is used once: after wait or cancel it must not be touched
again. join only registers the call with the set, so the caller still owes the
wait or cancel that ends it.
Handling an Async Operation
A handler method for an async operation is a
plain fn that returns T, and it resumes with a T. The caller still
receives an AsyncCall<T>, one that has already completed, so its .wait()
returns the value at once.
impl Client for MockClient {
fn send(&mut self, request: Request) -> Result<Response, ErrorCode> {
resume Result::<Response, ErrorCode>::Ok(canned_response());
}
}
test {
let mut mock = MockClient {};
with Client => &mut mock do {
let call = Client::send(new_request());
assert call.wait() matches { Ok(_) }; // completed already
};
}
Rationale: WEP: Generic AsyncCall<T>.
Async Exports
An export async fn uses the Component Model async calling convention. Its
body delivers the result with task return and may
keep running afterwards, for example to write a response's trailers.
Streams and Futures
Stream<T> and Future<T> are unbuffered channels. A write blocks until the
other end reads, and a read blocks until the other end writes, so the two ends
must be driven by different tasks.
Linking and Resources
Attribute Syntax for Component Model Linking
#[cm(...)] links a Wado definition to the Component Model definition it stands for:
// Link an effect interface to a CM interface
#[cm("example:cli/console@0.1.0")]
pub interface Console {
#[cm("example:cli/console@0.1.0#write-via-stream")]
fn write_via_stream(data: Stream<u8>) -> Future<Result<(), ErrorCode>>;
}
// Link a resource to a CM resource
#[cm("example:cli/terminal-output@0.1.0#terminal-output")]
pub resource TerminalOutput;
// Link an enum to a CM enum, and each case to its WIT case
#[cm("example:cli/types@0.1.0#error-code")]
pub enum ErrorCode {
#[cm("io")]
Io,
#[cm("illegal-byte-sequence")]
IllegalByteSequence,
#[cm("pipe")]
Pipe,
}
test {
assert ErrorCode::IllegalByteSequence != ErrorCode::Pipe; // the CM name changes no Wado name
}
#[cm_params("name", ...)] on an operation gives the CM-side names of its parameters. Without it, each parameter's CM name is its Wado name in kebab-case.
Resource Linearity
A #[cm(...)] resource may declare what may be done with its handle: linearity = "affine" or linearity = "unrestricted". Omitting the field reads as "affine", and a resource without #[cm(...)] is affine.
An affine resource is move-only and carries a drop obligation (see Resource Ownership). An unrestricted one owns nothing, so it is an ordinary copyable value. Assigning or passing one leaves the original usable, and nothing is dropped at the end of a scope.
The representation follows from the linearity. An affine resource crosses the Component Model boundary as an own / borrow handle, an unrestricted one as a plain f64 the host interprets (see Handle Encoding). as converts an unrestricted handle to or from f64, keeping every bit, or upcasts it to a resource it extends. No other cast accepts one, so an affine resource and an unrestricted one never convert into each other.
An unrestricted resource is not a Component Model resource. Its operations are plain CM functions that take the handle as an ordinary parameter, so a #[cm(...)] name written in the [constructor]T, [method]T.m or [static]T.m form is an error on one. classes = "..." (see Resource Inheritance) is accepted only beside linearity = "unrestricted".
Resource Ownership
An affine resource is move-only. Assigning it, passing it by value, returning it, placing it in an aggregate, and calling a method that takes self by value each move it. Using a binding after it has moved is a compile error.
pub resource Counter {
fn bump(&self);
fn consume(self);
}
fn eat(c: Counter) { }
fn misuse(c: Counter, d: Counter) {
eat(c); // moves `c`
c.bump(); // ERROR: resource `c` used after it was moved
d.consume(); // a by-value `self` moves `d` too
}
The check follows control flow. A move on one branch of an if or match counts after the branches meet, unless that branch diverges. A move inside a loop body is a use after move on the next iteration. A new let of the name, or an assignment to it, makes it usable again.
A value that holds an affine resource, directly or through a field, element or payload, is move-only in the same way. It moves as a whole: a resource is not moved out of it on its own, so a consuming method (below) is how one is taken out.
A method consumes a resource through a bare self receiver, and borrows it through &self or &mut self (see Method Receiver: self by Value). Option and Result take self by value in unwrap, expect, unwrap_or, unwrap_err and expect_err, so extracting a resource consumes the container and leaves the resource with one owner.
No Move Out of a Borrow
Returning a resource reached through a & or &mut parameter, &self included, is a compile error. A borrow leaves its referent with its owner, so the returned resource would have a second owner. The rule covers a field read, a dereference, a match binding over the borrowed value, and a let bound from any of these. A resource the function produces itself may be returned.
struct Holder { f: Fields }
impl Holder {
fn peek(&self) -> Fields {
return self.f; // ERROR: cannot move resource `Fields` out of a borrow
}
}
impl Holder {
fn into_fields(self) -> Fields {
return self.f; // OK: the holder is consumed
}
}
test {
let holder = Holder { f: Fields::new() };
assert holder.into_fields().copy_all().is_empty();
}
Drop
An owned resource that has not moved is dropped when its scope ends, on every path out of it. A move suppresses that drop, so each handle is dropped exactly once. An imported resource drops through the Component Model's resource.drop. A value holding resources drops each of them, a struct's fields in declaration order. A resource value discarded as a statement (Fields::new();, let _ = ...) is dropped there.
A resource's fn drop(self) consumes its receiver, so the scope does not drop it again, and a use after it is a use after move.
A trap does not unwind, so it runs no drops.
Handles at the Boundary
| Wado position | CM handle |
|---|---|
By-value R parameter or result |
own<R> |
&R or &mut R parameter, &self or &mut self |
borrow<R> |
Bare self receiver |
own<R>, transferring the receiver |
A reference crosses a Component Model import only as a borrowed resource handle. An import taking any other reference, such as &String, is a compile error.
Rationale: WEP: Resource Ownership.
Resource Inheritance
resource Child extends Parent declares that a child handle is usable wherever the parent is. The clause stands between the resource's name and its body, and names one resource as the parent. Both resources must declare linearity = "unrestricted", because an upcast copies the handle and an affine one may not be copied. Single inheritance only, and a cycle is an error.
#[cm("example:ui/target", linearity = "unrestricted", classes = "0..=1")]
resource Target {
#[cm("example:ui/target#add-listener")]
fn add_listener(&self, kind: String);
}
#[cm("example:ui/widget", linearity = "unrestricted", classes = "1..=1")]
resource Widget extends Target {
#[cm("example:ui/widget#label")]
fn label(&self) -> Option<String>;
}
fn use_it(w: Widget) {
w.add_listener("click"); // inherited, no cast
let t: Target = w; // upcast is implicit
}
test {
let w = 137438953472.0 as Widget; // class 1, object 0
let t: Target = w;
assert t == w && t matches { _: Widget };
}
A generic resource takes no part in extends, on either side.
Subtyping
extends induces Child <: Parent. The relation is reflexive and transitive, and resources extends does not relate are incomparable.
- The upcast is implicit wherever a value, a
return, or a&Treferent is expected, and where branches of aniformatchmeet. - A type parameter is solved to the most specific type, and the upcast happens later, at a use. So a constructor's payload is not upcast:
Option::Some(el)againstOption<Node>is anOption<Element>, and is writtenOption::Some(el as Node). &mut Tis invariant, and so is every generic type, a tuple's elements, and a struct's fields. A write through any of them could install a parent where a child is required. There is no variance annotation.- A function type is invariant in its parameters and its result.
- Narrowing back to a child is never implicit. It is written as a type pattern.
let r: Option<HtmlInputElement> = Option::Some(input);
let n: Option<Node> = r; // ERROR: Option is invariant
let r: Option<HtmlInputElement> = Option::Some(input);
let n: Option<Node> = r.map(|el| el as Node); // OK: each element is upcast
assert n matches { Some(_: HtmlInputElement) };
Methods
A method call resolves statically. recv.m() gathers every m declared along recv's extends chain and every m of a trait impl that applies to it. None is an error, and one resolves the call, upcasting the receiver to the resource that declares it. Two or more is ambiguous: write Declaring::method(&value) or Trait::method(&value) to pick one.
- A child may not redeclare a method it inherits.
- Static methods (no
&self) are not inherited. Selfin an inherited method names the resource that declares it, not the receiver's type.
Handle Encoding
An unrestricted handle is a number the host mints: an integer-valued f64 below 2^53, equal to class * 2^37 + index. The class takes 16 bits and the index into the host's object table takes 37. The host hands out one handle per object, and tags each object with the class of the nearest ancestor of its runtime type that the program declares. A type the program does not name therefore reads as its nearest named ancestor.
classes = "lo..=hi" on the #[cm(...)] numbers the classes of an extends tree. A resource's own class is lo, and the resources extending it hold the rest of the range:
- A child's range lies inside its parent's, above the parent's own class.
- Sibling ranges do not overlap.
- A tree declares
classeson every resource or on none. - A range may leave gaps, which stand for classes the program does not declare.
A type pattern narrowing to T tests whether the handle's class lies in T's range. A negative value, an infinity and a NaN lie in no range. A type pattern narrows only to a resource that declares classes.
Traits on Handles
Eq: every unrestricted resource isEq, so a type holding one derivesEqtoo.==and!=compare two handles when one type extends the other. Equal handles name one object (see Handle Encoding). Handles compare by bits, so a NaN handle equals itself and-0.0differs from0.0.Ord: none. Handles have no order.Inspect:${x:?}renders the dynamic type the handle's class names, as Inspect Output states.SerializeandDeserialize: a resource, and a struct or variant that holds one, cannot derive either. A handle means something only inside its running instance. A hand-written impl may serialize what it reads from the host object.
Rationale: WEP: Resource Inheritance and Narrowing.
