WebAssembly / WebAssembly/component-model

Awkward lack of borrowing on variants that contain resources

Open
#615 25 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
WebAssembly
Stars
1.4k
Forks
130
Avg merge
2d 1h
Merged PRs (30d)
15

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() -> 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?

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading the WIT debugger-interface draft linked in the issue, including the value, gc-ref, funcref, and inject-call examples, and review the described Wasmtime WIP branch context. The work is done when the project has a decided and documented path for borrowing variants that contain resources, including the relevant component-model changes.

Written by the indexing model from the issue text.

Assessment

Tech stack
wasm
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.