Repository navigation
Awkward lack of borrowing on variants that contain resources #615
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?
Activity
cfallin commentedon Feb 25, 2026
Additional food for thought: composition of borrowing with lists, options, maps.
Basically the definition of borrowing as a parameter mode ("second-class value") rather than type constructor ("first-class value") is the fundamental PL thing going on here. FWIW, Rust went through a similar evolution where borrowing used to be a parameter mode ("inout"-like thing) and then
&was lifted into the type-system proper.cfallin commentedon Feb 25, 2026
A little more detail on why cloning with variants is awkward but cloning with resources-as-fake-variants is less so: if I have
then I have to have a function
clone-value(v: value) -> tuple<value, value>(take one, return two) because I can't put a method on a variant or borrow it (subject of this issue). On the other hand if I build a "fake variant" with a resourcethe
clonemethod can live on the resource and work exactly like one expects in typical guest-language bindgen'd code.lukewagner commentedon Feb 25, 2026
Thanks for filing! I might be misunderstanding your question, but it is legal (and, just testing, implemented) to put
borrow<some-resource>handles inside variant cases a la:Here we're not borrowing (at least at the WIT-level) the variant or list, but we are borrowing all handles contained inside them.
cfallin commentedon Feb 25, 2026
Yep, that's a good point. The issue is that one also wants the non-borrowing variant for other cases -- for example, to return a Wasm value -- and so one ends up with both
variant valueandvariant borrowed-value, and functions to convert between them.One also can't actually write a function to return that variant (e.g., a method
value.to-borrow) because one cannot return borrows. So one would need to build a helper directly in guest code that destructures, explicitly borrows, puts the variant back together, and then passes that in.All-in-all it seems it would be worthwhile to think about whether borrows could be hoisted to a first-level concept somehow. One seed of an idea is that lazy value lowering gives value numbers to values in a value table; those look suspiciously like resource handles to resources in a resource table; borrows of any value; discuss?
cfallin commentedon Feb 25, 2026
One more example that came to mind, in the slightly expanded scope that includes lists too: in a design where I have
Tandborrowed-T, doing the manual thing to build alist<borrowed-T>from alist<T>is O(n) eager work that createsnnew resource handles; it's much more efficient to have a type-system understanding ofborrow<list<T>>such that when the underlying list ofu32s is copied over, the host directly puts them in the callee's resource table in borrowed mode.I have only a fairly hazy understanding of the canonical ABI's lowering but (ignoring my further-out lazy lowering handles as borrows thought) I wonder if one could interpret lowering of
borrow<T>generically as altering any handle borrowing modes transitively withinTbut otherwise lowering as before? So then we could have borrows of variants, records, lists, options, ... with the expected semantics. It would not avoid eager copying of the whole list so e.g.borrow<list<u32>>is not a direct analogue to&[u32], but the ownership-passing semantics are right at least...lukewagner commentedon Feb 25, 2026
You're right that, at the ABI level, lazy-lowering will have a strong feel of "borrowing" to it b/c we're talking about a (dynamic ref-count-enforced) scope during which you can copy or drop a value you were given. One neat thing about lazy lowering is that, by necessity, it introduces "borrowing" in the return direction: when the callee returns a value, the callee's memory is "lent" to the caller until the caller calls
subtask.drop, after which the callee'spost-returnfunction will be run at which point the callee knows that it has regained ownership of its memory. But that means we could also relax the rules to allowborrow<R>in result types (trapping if all returned borrowed handles weren't dropped beforesubtask.drop), addressing the asymmetry you mentioned.Other than that, though, it seems like owned and borrowed handles are just different types. I suppose we could consider a generic
handle<resource-type>type that is "parameterized" by whether its owned or borrowed, so that I could write:variant v { one(handle<r>) } foo: func(one: list<v>); bar: func(two: borrow<list<v>>);and have this be roughly equivalent to:
variant v { one(r) } variant borrowed-v { one(borrow<r>) } foo: func(one: list<v>); bar: func(two: list<borrowed-v>);which could be convenient in avoiding duplication. I guess I'd be interested to see if more use cases for this pop up to help prioritize it accordingly since I expect we could always add it later without changing of the meaning of things as they currently stand.
cfallin commentedon Feb 25, 2026
Sure, "just different types" is a valid outcome of the discussion. But I think there's a little bit of danger in falling back to that too easily: it's true in the same sense that monomorphized template instantiations in C++ are "just different types", or
Tand&Tin Rust, or anything else. Building a little more structure into things helps us avoid repetitive and error-prone boilerplate, which is what makes this worth considering. As-is, the use-case linked above (which is real and will land in Wasmtime!) cannot use WIT variants because of this limitation.I think this is probably the most promising direction, except (as proposed above) if we have just
one(r), we can define the lowering ofborrow<list<v>>to lower the innerras a borrow but the lowering oflist<v>to lower the innerras an owned handle. In other words, lowering of any given type is "in borrowed context" or "in owned context",borrow<T>lowersT"in borrowed context", and primitives are lowered the same in borrowed/owned, records and variants pass through context, and resource handles lower with the given borrowing mode. (In other words, we don't need thehandlemodifier, I think.) Thoughts?lukewagner commentedon Feb 26, 2026
Cool; I think we're agreeing on the abstract shape of this potential solution, and maybe there's just a syntactic difference in how we're thinking about it.
FWIW, I'm imagining that, at the WAT level, there's only one
(handle <typeidx> <ownership>)type constructor for all handles whereownershipis eitherownedorborrowedand then(own $R) ≡ (handle $R owned)and(borrow $R) ≡ (handle $R borrowed)as abbreviations. And then in WIT if you sayr, that's(own $r), just like it is today, but if you writehandle<R>, that's(handle $r $ownership)for some$ownershipparameter. And iiuc, you're saying that we could also reconsiderrto mean(handle $r $ownership), which is a fair point and worth discussing.Separately, there's a tricky question of what
vlooks like as a standalone type definition (that can be used in both owned and borrowed contexts). It seems like we'd somehow need to introduce a way to bind$ownershipas a parameter that can be used inside the definition ofvand then applied toowned|borrowedby uses ofv.For reference, we do something analogous for
interfaces whereuseis effectively a parameter that can be bound differently in different contexts. E.g., in:we take the
instancetype that representsiand wrap it with acomponenttype thatimportsj.{r}so thatrbecomes a parameter that can be used in the definition of theinstancetype. (For a worked example, see here.) It seems like we'd need to do likewise for ownership, except that we can't (ab)use component types in the same way b/cownershipisn't really atype.cfallin commentedon Feb 26, 2026
Ah, interesting -- I guess I was imagining a more direct syntactic correspondence between WIT and the CM type-system. So
ris(own r)(as today?); andborrow<T>is(borrow $lower_type(T))where$lower_typeis a meta-function defining this mapping. Maybe we call(own r)(handle r)for reasons that will become clear below.That means that
borrow<r>is(borrow (handle $R)),borrow<list<r>>is(borrow (list (handle $R))), andborrow<variant>is(borrow $Variant).Then the canonical ABI defines how we lower with a mapping from type to lowered format that also takes "context" as an argument. So
$canonical_abi_lowering(r, mode=owned)lowersrto a u32 pointing to an owned handle in the resource table, but$canonical_abi_lowering(r, mode=borrowed)lowersrto a borrowed handle in the table; and$canonical_abi_lowering(list<T>, mode)lowers elements with$canonical_abi_lowering(T, mode), in other words the "borrowed" context aroundlist<r>passes down to how we lowerr.Does that seem workable? The basic principle I'm going for is to not create bifurcated notions of borrowing at WIT and component level, but have them exactly correspond; and to make this as general and as composable as possible.
alexcrichton commentedon Feb 26, 2026
I've been trying to follow/skim this, but I haven't ingested it all fully necessarily, so forgive me if this is a repeat of something or already been discussed:
This, and other comments here, I'm not sure are quite accurate. The issues you're describing of whether to use a
borrow<T>orown<T>within avariantis the exact same problem that Rust has of using aTor&'a Tin an enum. Rust is slightly better off with generics where there's just oneOption<T>for example, but for a concrete type likewasm-valuethe design constraints WIT has are, at least in my opinion, the exact same as those in Rust.Basically I don't think that this is a novel problem or a indicator of something wrong in WIT, I'm under the impression that it's the exact same problem in Rust or at least the exact same problem can arise.
I would personally not be in favor of
borrow<list<T>>which would auto-borrow all resources withinT. From a bindings generation perspective that would be quite difficult to bind within Rust, C, or any other static language. It would mean, for example, that arecordwith a resource in it is unknown whether that resource should be anown<T>or aborrow<T>. Generators would have to generate two types for all WIT types that contain resources and then choose the right one.I don't mean to say that the original problem is solved via some other means of course. WIT today would force some usage of
wasm-valueandborrow-wasm-valueas two different type definitions. In Rust, however, this is also the case. I would imagine that the duplication is undesirable here and theclone-style method would be the bridge to using avariant.cfallin commentedon Feb 26, 2026
I'm referring to this post about early Rust history by Graydon Hoare and others I've read describing "The second-class & types in early Rust" and "I wanted & to be a "second-class" parameter-passing mode" -- that's the analogy to WIT
borrowtypes today. I didn't use the language pre-lifetimes so I don't know for sure but in any case "references as a special mode" is a fairly common PL thing going back to e.g. Pascal's out-parameters, or upvalues in Tcl, etc.I don't think WIT has the same expressive power: Rust's
Option<T>can takeT = &U, or in other words, the universe of types includes&as a type-constructor that can modify any other type (i.e., first-class). That's not the case for WIT types:borrowcan modify handles in particular, not any other type.So concretely, I can have the
enum Valuein my original comment with an owned value (e.g. anArc) inside, in Rust, and I can pass a&Valueas a parameter with no issues. In WIT, I can have a variant that includes a resource in one arm, but I cannot pass a borrow of that variant (no such concept exists). Instead I have to define a completely new variant, and manually convert, and linear types make that manual conversion extremely tedious (static funcs that take one thing and return two, etc).I don't think it is! In Rust, I can do the equivalent of passing
&wasm-value; I don't have to define the enum twice and convert between them.I may be missing some way to avoid that, but right now it seems like a clear expressivity hole in the language. It's fine if the resolution is "we don't want to pay the complexity tax", but we should acknowledge that it's a limitation.
cfallin commentedon Feb 26, 2026
(And to head off confusion: above in your comment and mine we're both switching between containers/variants-of-borrows and borrows-of-containers/variants. The former is supported and fine; it's the latter (
&MyEnum) that WIT doesn't have. The "auto-borrowification" of the inner bit is the moral equivalent of matching on a&MyEnumand getting&-borrows of fields that are owned types in the enum's actual definition.)fitzgen commentedon Feb 26, 2026
Unless$O(n)$ operation for e.g.
Tand&Talways have the same representation for all typesT, then the "auto-borrowification" is anlist<T>-to-list<&T>, so I don't think auto-borrowification is reasonable.fitzgen commentedon Feb 26, 2026
But being able to pass
borrow<list<T>>to functions and not have that drop any resources or deallocate any memory associated with the innerTdoes seem reasonable to me, and also seems like it would resolve the OP papercut.cfallin commentedon Feb 26, 2026
The lowering is already O(n), right?
What this is saying is that
(borrow (list (handle $R)))is lowered just like(list (handle $R)), i.e., N pushes into the callee's resource table, except we set the handle-mode to borrowed rather than owned.cfallin commentedon Feb 26, 2026
(The Rust analogy helps to a certain point but then may confuse beyond that --
list<t>doesn't look the same as e.g.&[T]for some arbitrarily largeTdoes in Rust, so we don't have to do the moral equivalent of&[T]->[&T]. Likewise for theTvs&Tinside an enum. All handles are already references here.)fitzgen commentedon Feb 26, 2026
But if it forces callers to reallocate and convert their data that is already in$O(n)$ either way.
list<T>'s canonical ABI representation intolist<borrow<T>>'s canonical ABI representation because the two representations are different that is not great... even if stillcfallin commentedon Feb 26, 2026
They're the same canonical representation, is my point: when
Tis a resource, its representation is a u32 either way, only the mode-bit in the resource table changes. WhenTis something else that contains a resource, then also (recursively) its canonical representation is the same either way. So this is purely a one-bit change when we set up the resource table entries.fitzgen commentedon Feb 26, 2026
But it isn't just resources, we would need to commit to
borrow<T>andThaving the same representation for allT, forever.That is maybe not too bad for the types we have now, but the constraints on anything we add in the future makes me hesitant.
But it also seems like it isn't necessary to solve OP or round out the type system by making resources not special as the only type that can ever be borrowed? If we allow functions to take
borrow<list<T>>as an argument, for example, then there is no need for auto-borrowification?cfallin commentedon Feb 26, 2026
Yes, and that seems fine I think? Borrowing here has a different meaning than in Rust, because of the value-transfer/copying nature of cross-sandbox calls; so borrows really have to do with resources specifically, and so what we're really saying is that borrowed and owned resource handles have the same representation (only), nothing about any other possible future types.
And I push on this because it is required to make
borrow<variant-with-t>orborrow<list<T>>to work: in order to (i) keep one type definition (e.g., the variant, i.e. the OP issue) rather than requiring the user to define two, we need to somehow handle the inner handle (which is owned in the definition) differently in a borrowed context.cfallin commentedon Feb 26, 2026
Or said differently: if we don't lower
borrow<list<T>>as a list of u32s, each of which is a borrowed handle, then what do we lower it as? And if we do, that's exactly (and only) what auto-borrowification is.alexcrichton commentedon Feb 26, 2026
Ah ok thanks for clarifying, that makes sense to me. I see where I was focusing too much on resources specifically as opposed to types in general.
W.r.t. what @fitzgen is saying I agree with @cfallin here in that
borrowonly affects resources and the ABI of everything remains the same irrespectiveborrow-or-not. In that sense I can't think of an ABI hazard for something likeborrow<list<T>>.I'll note though that
borrow<rsrc>andown<rsrc>being represented the same way at the ABI level does not hold true for language bindings. In Rustborrow<rsrc>is&Rsrcwhileown<rsrc>isRsrc. I don't actually know what we'd do if we allowedborrow<T>for all typesT. Let's say for example WIT had:variant my-variant { nothing, things(list<some-resource>), } foo: func(x: borrow<my-variant>);If we were to map
borrow<my-variant>to Rust it could take any of the following forms, in theory:All of these semantically mean the same thing where
SomeResourceisn't being taken by ownership, and it's not clear to me what would actually want to get generated. Even defaulting tofoo1's style can unnecessarily force allocations on callers where it's not present withlist<borrow<some-resource>>for example.I mostly bring this up where for any changes to WIT or the component type system I think we need to be sure to consider how this maps into languages. Restricting
borrowandownto just resource types has a relatively straightforward mapping and enables languages to possibly have distinct types for each mode of resources. Not all languages use this (e.g. dynamic ones like Python or JS probably wouldn't), but languages that want to (e.g. Rust/C++) are able to expose statically the ownership model.The best I can think of to thread things around would be to translate
list<T>toVec<T>at all times and then generate types with a<const BORROW: bool>parameter on all types. Ergonomically I'm not sure how feasible that would be, however.cfallin commentedon Feb 27, 2026
That's a good point. My instinct is to keep the type definition orthogonal to the borrow that wraps it; so
borrow<my-variant>from your example puts a&in front of the Rust-lowered type ofmy-variant, nothing more -- doesn't try to propagate slice-borrows in, etc. That's the "borrowed-variantproblem" in new clothes I think. So the codegen I would expect formight be something like
which is more or less a direct translation of the WIT syntax. That keeps things simple and predictable, and addresses the main use-case, which is allowing parameter-passing without ad-hoc cloning built in userspace.
For the use-cases I hit in my example WIT above, checking that this makes sense: I need
borrow<wasm-value>(functions that take one Wasm value), which becomes&WasmValue, which is sensible;borrow<list<wasm-value>>orlist<borrow<wasm-value>>; I'd probably choose the latter because the list is specific to the API call we're making (we've assembled arguments for one function call) and it lets me build aVec<&WasmValue>on the Rust side rather than moving the values into theVec, passing a&[], then moving the values back out.alexcrichton commentedon Feb 27, 2026
Two further thoughts from me:
One is that to support this, and I think this is implied by other comments here, we will want to update the type grammar in the component model itself, not just WIT. I think it would be too complicated, for example, for a WIT-level
borrow<list<T>>to turn into a(list (borrow T))at the component-model level (primarily whenTis an aggregate/structure/etc).If we go down that road, however, then the current rules/validation about resources may no longer make sense. Right now resources have an index in the type index space but you can't actually refer to them directly (it's a validation error). Instead resources need to be referenced through either
(own $r)or(borrow $r). If, however, you can you(borrow $t)anywhere for any type then that leaves a question of why do we even haveown.Basically what I'm getting at is that if
(borrow T)is allowed we should (long term) delete(own $r)I think. Referring to a bare$rresource would be allowed and would mean "this is a transfer of ownership" by default. That transfer would semantically mean something different when transitively included in a(borrow T), though.The second thing is that supporting
borrow<T>for arbitraryTis likely going to be quite difficult from the perspective of expoted functions in Rust. Already I think there's bugs in wit-bindgen where if you have an exported function taking alist<borrow<some-resource>>it doesn't codegen correctly, and that's got all the same issues as supporting aborrow<variant-with-resource>. Effectively the way the Rust bindings works is that the export function stack-roots any borrows a function needs so the function itself that users write will recieve&Resource(which has to live somewhere on the stack in Rust). This would need to be translated for anyTwhere any value would need to be stack-rooted for the underlying guest to receive&T.Another consequence of this, with the codegen pattern you're describing @cfallin, is that C for example would have no real means of conveying this information. For example in C you'd receive a
wasm-valueenum which would look exactly like the owned version, and it's just semantically you'd have to understand you don't actually own the contents. Not that this makes the C perspective worse per-se, but it means that languages other than Rust/C++ will probably end up having no real protections/static distinction about whether a resource in an aggregate is a borrow or anown, it'll all be mostly dynamic checks and such.cfallin commentedon Feb 27, 2026
Yes, agreed -- semantically once we have "borrow of X" as a modifier (type constructor), then it makes sense for "handle" to just mean "owned handle", and "borrow of owned handle" is how we say "borrowed handle".
Yes, agreed, C doesn't have any way of representing contracts of pointer ownership; in a sense this is no different from any other C programming, where I give you a
struct BigThing*and if I tell you that I'm passing ownership of the big thing, I'm implicitly passing ownership of all of its owning pointers inside; whereas if I'm giving you a (borrowed) view, I am not.To summarize my own thoughts/feelings on this: I agree that the generalization adds some complexity in some places, but I feel it also makes things simpler by being more general and composable, at least for someone who expects type systems to be composable. I was banging my head against this for a while trying to figure out how to pass variants with resources without giving up ownership (then giving up and building my own variant with a resource) and frankly it's an odd expressivity hole that will have a lot of people reaching for workarounds in more complex APIs if WIT gets used much beyond OS-like interfaces. So I do think it's important to keep that motivation in mind; this isn't just a "nice to have" IMHO.