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() -> kind and unwrap_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?
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
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.
Now consider an API that takes these values, perhaps to make a function call (in the example WIT above, see
inject-call):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:
value;kind: func() -> kindandunwrap_i32/unwrap_i64/ ... methods;The last option is the natural approach in languages with linear types, like Rust: in Rust, I can define
and pass in
&Valueto 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?