Add support for named imports to WASI implementations - #14275
Add support for named imports to WASI implementations#14275alexcrichton wants to merge 2 commits into
Conversation
This commit is at least an initial stab at making the `wasmtime-wasi` and `wasmtime-wasi-http` crates compatible with "named imports" or the `implements` field in the component model. This field enables importing an interface under a kebab-name while annotating that it's additionally to be considered an import of another interface's name. One example use case for this feature is [dependency isolation][diso] when composing two components together -- if they both import the filesystem the final component will import the filesystem twice under two different kebab names which means both components can have a different view of the filesystem. Wasmtime previously gained support for named imports and `implements` in `bindgen!` as part of bytecodealliance#13513 where the `named_imports` option can be specified at `bindgen!`-time which generates traits that take an extra id-style parameter. This runtime parameter indicates which kebab-name is being invoked through which the runtime can then dispatch on. The goal here is to actually wire all this up in a way that's usable for embedders. Specifically `named_imports` bindings generation is now available for all WASIp2 and WASIp3 interfaces. Additionally all implementations of these `id`-carrying traits are routed through the previous implementations after locating the correct context to operate over. All implementations of WASI functionality are already modeled more-or-less as methods on `Wasi*CtxView`-style types which internally have a borrow to the actual state and the resource table to operate on. This fits quite cleanly with named imports where conceptually what we want is the ability to configure the context-per-kebab-name. This in theory will keep the maintenance burden managable as there's still largely one source of truth for the implementation. This neatly works for all `Host`-style traits which are literally methods on `Wasi*CtxView` types, meaning the `id`-carrying versions actually do just acquire a `Wasi*CtxView` and then delegate the method. This requires more finesse for `*WithStore` traits which work with `Access` and `Accessor`, however. The `id` parameter cannot be threaded into the `fn(..)` within the `Accessor`, so refactoring is performed where appropriate to make the implementation of each interface a one-liner to reduce duplication. The end result of all of this is that this is a very large commit but it's written in such a way that the Rust compiler in theory should catch all mistakes. In other words we're heavily relying on the type system and type checking here and don't ever rely on duplication of methods that hopefully-won't-change. There's a lot of traits and a lot of interfaces, hence the size of the commit, but conceptually everything is intended to be pretty simple. Some design decisions as part of this commit, in no particular order: * IDs are represented by `wasmtime_wasi::NamedId` which is a newtype-wrapper around `usize`. The goal here is to enable an efficient implementation of dealing with ids. This notably forces the embedder to derive some sort of string-to-id (and perhaps back) map when adding items to a linker. * All of this is opt-in and nothing is changed by default. For example the `wasmtime` CLI does not support any of this yet -- in theory that would require the ability to configure `-S` flags per-named-import as opposed to all-at-once. * Mapping a `NamedId` to a context is abstracted behind a trait rather than dictating that a `Vec` or `HashMap` or similar is required. This increases the cognitive load when reading code (more generics), but avoids making this design decision within these crates and leaves exact representations up to embedders. * The `HasData` implementation can't reuse the preexisting `WasiCli`, and this uses a new `WasiCliNamed<T>` instead. This enables threading this trait-to-find-a-context to the right location for `*WithStore` trait impls. * Some miscellaneous `bindgen!` issues have been fixed during this commit to ensure that this compiles and works correctly. * An attempt has been made at documenting all the new primitives/structs/etc here. These are sort of difficult to align correctly unless you know what you're doing, so the documentation and examples are intended to serve as a way of spreading this knowledge. * One possible alternative I ended up deciding not to do was to put some sort of map-to-context storage within each preexisting context type. For example commit would be simpler for the `*WithStore` and infrastructure if it reused the exact same `Self` type as all other impls do. My thinking though is that this requires dictating the use of a `HashMap` or something else which I was hoping to avoid. Additionally the preexisting context structures are already minimal enough that they're basically what you already want as the source for each implementation, so I wanted to lean on them as much as possible. * The main wrinkle in the new implementation is that `Accessor` carries `fn(..)` to project out it's `D::Data<'_>` which means that it can't close over any information. This feature needs to in theory close over an `id: NamedId`, however, and there's no easy way to put this square peg into a round hole. To work around this internal implementations within `wasmtime-wasi{,-http}` now have a generic `F` parameter which is a closure which projects data, but this closure is typically only ever on the stack and doesn't make its way to the heap. This was one of the more awkward things to work around in this commit. * The design here is intentionally done to help ensure that this commit is correct with minimal testing. It's not really feasible to duplicate the entire test suite just for named imports but these are duplicate trait impls which otherwise shouldn't be wrong. By ensuring that there's either strict delegation or each-function-is-at-least-one-line that the light amount of testing here is sufficient for keeping this working over time. [diso]: spinframework/spin#3708
rvolosatovs
left a comment
There was a problem hiding this comment.
Sorry for the delay, missed this one in my inbox. Currently still working on a full review, but I noticed that the PR introduces a regression against latest main (b7764a7), so sharing it early.
For a case like this:
pub struct ErrorA;
pub struct ErrorB;
wasmtime::component::bindgen!({
inline: "
package test:collision;
interface a { enum error { failed } }
interface b { enum error { failed } }
interface combined {
use a.{error as error-a};
use b.{error as error-b};
first: func() -> result<_, error-a>;
second: func() -> result<_, error-b>;
}
world test { import combined; }
",
imports: { default: trappable },
named_imports: { "test:collision/combined": usize },
trappable_error_type: {
"test:collision/a.error" => ErrorA,
"test:collision/b.error" => ErrorB,
},
});I'm getting:
error[E0308]: mismatched types
--> src/lib.rs:3:1
|
3 | / wasmtime::component::bindgen!({
4 | | inline: "
5 | | package test:collision;
6 | | interface a { enum error { failed } }
... |
21 | | },
22 | | });
| | ^
| | |
| |__expected `ErrorA`, found `ErrorB`
| arguments to this function are incorrect
|
note: method defined here
--> src/lib.rs:3:1
|
3 | / wasmtime::component::bindgen!({
4 | | inline: "
5 | | package test:collision;
6 | | interface a { enum error { failed } }
... |
21 | | },
22 | | });
| |__^
= note: this error originates in the macro `wasmtime::component::bindgen` (in Nightly builds, run with -Z macro-backtrace for more info)
While it works on current main
|
|
||
| fn from_list( | ||
| &mut self, | ||
| id: NamedId, |
There was a problem hiding this comment.
Is the plan to indefinitely duplicate implementations of all WASI interfaces using this scheme or could we consider always generating bindings with a id: Option<NamedId> or id: Option<T>? (perhaps once the feature is stable)
On a similar note, since additional functionality is likely to be added to WIT and be relevant (e.g. annotations, external-id etc.), would it make sense to introduce something like a unified context type? Something like a
struct InterfaceContext<Name, Id, Annotations> {
name: Option<Name>,
external_id: Option<Id>,
annotations: Option<Annotations>,
}
struct FuncContext<Name, Id, Annotations> {
interface: InterfaceContext<Name, Id>,
external_id: Option<Id>,
annotations: Option<Annotations>, // not sure what this is yet, but we probably don't want to use wasmtime::component::Val, so likely this is a WIT type passed to bindgen given latest discussions?
}
impl FuncContext {
fn is_empty(&self) -> bool { .. }
}
impl InterfaceContext {
fn is_empty(&self) -> bool { .. }
}We could then generate something like:
trait HostFields {
fn from_list(&mut self, cx: wasmtime::component::FuncContext<NamedId, ExternalId, Annotations>, entries: Vec<(String, Vec<u8>)>) -> HeaderResult<Resource<FieldMap>>
I guess what I'm trying to point out is that it seems like this goes beyond just the interface names, since the external-id and annotations might also be of importance for the embedder.
More concretely:
- Should we always generate bindings, which take an additional parameter instead of the split into named and unnamed?
- Should we generalize this to account for the upcoming additions and instead allow embedders to derive a custom per-function context at link time? (this would replace a generic
{Func,Interface}ContextI suggested above by either a wasmtime-wasi provided abstraction or, (IMO) ideally, a genericTthat embedder could supply and derive at link time)
WDYT?
| let res = cx | ||
| .table | ||
| .push(res) | ||
| .context("failed to push response to table")?; |
There was a problem hiding this comment.
nit; since we're dropping these, should probably drop this one as well?
| .context("failed to push response to table")?; | |
| ?; |
This commit is at least an initial stab at making the
wasmtime-wasiandwasmtime-wasi-httpcrates compatible with "named imports" or theimplementsfield in the component model. This field enables importing an interface under a kebab-name while annotating that it's additionally to be considered an import of another interface's name. One example use case for this feature is dependency isolation when composing two components together -- if they both import the filesystem the final component will import the filesystem twice under two different kebab names which means both components can have a different view of the filesystem.Wasmtime previously gained support for named imports and
implementsinbindgen!as part of #13513 where thenamed_importsoption can be specified atbindgen!-time which generates traits that take an extra id-style parameter. This runtime parameter indicates which kebab-name is being invoked through which the runtime can then dispatch on.The goal here is to actually wire all this up in a way that's usable for embedders. Specifically
named_importsbindings generation is now available for all WASIp2 and WASIp3 interfaces. Additionally all implementations of theseid-carrying traits are routed through the previous implementations after locating the correct context to operate over.All implementations of WASI functionality are already modeled more-or-less as methods on
Wasi*CtxView-style types which internally have a borrow to the actual state and the resource table to operate on. This fits quite cleanly with named imports where conceptually what we want is the ability to configure the context-per-kebab-name. This in theory will keep the maintenance burden managable as there's still largely one source of truth for the implementation. This neatly works for allHost-style traits which are literally methods onWasi*CtxViewtypes, meaning theid-carrying versions actually do just acquire aWasi*CtxViewand then delegate the method. This requires more finesse for*WithStoretraits which work withAccessandAccessor, however. Theidparameter cannot be threaded into thefn(..)within theAccessor, so refactoring is performed where appropriate to make the implementation of each interface a one-liner to reduce duplication.The end result of all of this is that this is a very large commit but it's written in such a way that the Rust compiler in theory should catch all mistakes. In other words we're heavily relying on the type system and type checking here and don't ever rely on duplication of methods that hopefully-won't-change. There's a lot of traits and a lot of interfaces, hence the size of the commit, but conceptually everything is intended to be pretty simple.
Some design decisions as part of this commit, in no particular order:
IDs are represented by
wasmtime_wasi::NamedIdwhich is a newtype-wrapper aroundusize. The goal here is to enable an efficient implementation of dealing with ids. This notably forces the embedder to derive some sort of string-to-id (and perhaps back) map when adding items to a linker.All of this is opt-in and nothing is changed by default. For example the
wasmtimeCLI does not support any of this yet -- in theory that would require the ability to configure-Sflags per-named-import as opposed to all-at-once.Mapping a
NamedIdto a context is abstracted behind a trait rather than dictating that aVecorHashMapor similar is required. This increases the cognitive load when reading code (more generics), but avoids making this design decision within these crates and leaves exact representations up to embedders.The
HasDataimplementation can't reuse the preexistingWasiCli, and this uses a newWasiCliNamed<T>instead. This enables threading this trait-to-find-a-context to the right location for*WithStoretrait impls.Some miscellaneous
bindgen!issues have been fixed during this commit to ensure that this compiles and works correctly.An attempt has been made at documenting all the new primitives/structs/etc here. These are sort of difficult to align correctly unless you know what you're doing, so the documentation and examples are intended to serve as a way of spreading this knowledge.
One possible alternative I ended up deciding not to do was to put some sort of map-to-context storage within each preexisting context type. For example commit would be simpler for the
*WithStoreand infrastructure if it reused the exact sameSelftype as all other impls do. My thinking though is that this requires dictating the use of aHashMapor something else which I was hoping to avoid. Additionally the preexisting context structures are already minimal enough that they're basically what you already want as the source for each implementation, so I wanted to lean on them as much as possible.The main wrinkle in the new implementation is that
Accessorcarriesfn(..)to project out it'sD::Data<'_>which means that it can't close over any information. This feature needs to in theory close over anid: NamedId, however, and there's no easy way to put this square peg into a round hole. To work around this internal implementations withinwasmtime-wasi{,-http}now have a genericFparameter which is a closure which projects data, but this closure is typically only ever on the stack and doesn't make its way to the heap. This was one of the more awkward things to work around in this commit.The design here is intentionally done to help ensure that this commit is correct with minimal testing. It's not really feasible to duplicate the entire test suite just for named imports but these are duplicate trait impls which otherwise shouldn't be wrong. By ensuring that there's either strict delegation or each-function-is-at-least-one-line that the light amount of testing here is sufficient for keeping this working over time.
Closes #14185