-
Notifications
You must be signed in to change notification settings - Fork 110
Description
Recently in building up a WIT description for a debugger interface in Wasmtime (current draft, implemented and in use in a WIP branch), I have run into a somewhat awkward interaction between variants, resources, and borrowing.
Consider the problem of modeling a Wasm value in WIT: the standard approach in a language with sum types (variants) to build a reflection-like interface (or interpreter implementation or whatever) is to define the "any type" value like
variant value {
I32(i32),
I64(i64),
// ...
}
This works great for primitive types. But now let's introduce values that can refer to more heavyweight things, such as GC object references; these are likely resources, because we need to represent ownership of the GC root.
variant value {
I32(i32),
I64(i64),
GCRef(gc-ref),
// ...
}
resource gc-ref {
get-field: func(index: u32) -> value;
// ...
clone: func() -> gc-ref; // build `Clone` on top of the resource's linear type.
}
Now consider an API that takes these values, perhaps to make a function call (in the example WIT above, see inject-call):
resource funcref {
call: func(args: list<value>);
}
This seems like a reasonable-enough model until one realizes: a variant containing any resources is linear-typed (cannot be cloned). So one is left with a few different options to make a usable API:
- Define a "clone" ad-hoc function for
value; - Define a "clone" helper in userspace, destructure the variant, call the ad-hoc "clone" for each resource-owning arm;
- Avoid use of variants and build ad-hoc resources with
kind: func() -> kindandunwrap_i32/unwrap_i64/ ... methods; - Somehow "borrow" the values for the duration of the call.
The last option is the natural approach in languages with linear types, like Rust: in Rust, I can define
enum Value {
I32(i32),
I64(i64),
GCRef(Arc<ObjectRoot>), // or whatever
// ...
}
and pass in &Value to any function.
Unfortunately, in the resource model in the CM, borrowing is intrinsically tied to resources only, and a variant containing a resource cannot be borrowed. Because of this, in the WIT above, the true linear-typed approach ("clone everywhere") became way way too awkward, and because borrowing is not an option, I had to avoid variants entirely. This is awkward from an ergonomics PoV, and is also not great performance-wise: it means that I have a bunch of hostcalls to construct resources that represent (for example) "the constant value I32(42)", and I get to use them exactly once.
Is there a path by which we can consider supporting borrows of variants?