Skip to content

Commit

Permalink
Add support for async/streams/futures
Browse files Browse the repository at this point in the history
This adds support for loading, compiling, linking, and running components which
use the [Async
ABI](https://github.com/WebAssembly/component-model/blob/main/design/mvp/Async.md)
along with the [`stream`, `future`, and
`error-context`](WebAssembly/component-model#405) types.
It also adds support for generating host bindings such that multiple host
functions can be run concurrently with guest tasks -- without monopolizing the
`Store`.

See the [implementation RFC](bytecodealliance/rfcs#38)
for details, as well as [this
repo](https://github.com/dicej/component-async-demo) containing end-to-end smoke
tests.

This is very much a work-in progress, with a number of tasks remaining:

- [ ] Avoid exposing global task IDs to guests and use per-instance IDs instead
- [ ] Track `task.return` type during compilation and assert the actual and expected types match at runtime
- [ ] Ensure all guest pointers are bounds-checked when lifting, lowering, or copying values
- [ ] Reduce code duplication in `wasmtime_cranelift::compiler::component`
- [ ] Reduce code duplication between `StoreContextMut::on_fiber` and `concurrent::on_fiber`
- [ ] Minimize and/or document the use of unsafe code
- [ ] Add support for `(Typed)Func::call_concurrent` per the RFC
- [ ] Add support for multiplexing stream/future reads/writes and concurrent calls to guest exports per the RFC
- [ ] Refactor, clean up, and unify handling of backpressure, yields, and even polling
- [ ] Guard against reentrance where required (e.g. in certain fused adapter calls)
- [ ] Add integration test cases covering new functionality to tests/all/component_model (starting by porting over the tests in https://github.com/dicej/component-async-demo)
- [ ] Add binding generation test cases to crates/component-macro/tests
- [ ] Add WAST tests to tests/misc_testsuite/component-model
- [ ] Add support and test coverage for callback-less async functions (e.g. goroutines)
- [ ] Switch to back to upstream `wasm-tools` once bytecodealliance/wasm-tools#1895 has been merged and released

Signed-off-by: Joel Dice <[email protected]>

fix clippy warnings and bench/fuzzing errors

Signed-off-by: Joel Dice <[email protected]>

revert atomic.wit whitespace change

Signed-off-by: Joel Dice <[email protected]>

fix build when component-model disabled

Signed-off-by: Joel Dice <[email protected]>

bless component-macro expected output

Signed-off-by: Joel Dice <[email protected]>

fix no-std build error

Signed-off-by: Joel Dice <[email protected]>

fix build with --no-default-features --features runtime,component-model

Signed-off-by: Joel Dice <[email protected]>

partly fix no-std build

It's still broken due to the use of `std::collections::HashMap` in
crates/wasmtime/src/runtime/vm/component.rs.  I'll address that as part of the
work to avoid exposing global task/future/stream/error-context handles to
guests.

Signed-off-by: Joel Dice <[email protected]>

maintain per-instance tables for futures, streams, and error-contexts

Signed-off-by: Joel Dice <[email protected]>

refactor task/stream/future handle lifting/lowering

This addresses a couple of issues:

- Previously, we were passing task/stream/future/error-context reps directly to
  instances while keeping track of which instance had access to which rep.  That
  worked fine in that there was no way to forge access to inaccessible reps, but
  it leaked information about what other instances were doing.  Now we maintain
  per-instance waitable and error-context tables which map the reps to and from
  the handles which the instance sees.

- The `no_std` build was broken due to use of `HashMap` in
  `runtime::vm::component`, which is now fixed.

Note that we use one single table per instance for all tasks, streams, and
futures.  This is partly necessary because, when async events are delivered to
the guest, it wouldn't have enough context to know which stream or future we're
talking about if each unique stream and future type had its own table.  So at
minimum, we need to use the same table for all streams (regardless of payload
type), and likewise for futures.  Also, per
WebAssembly/component-model#395 (comment),
the plan is to move towards a shared table for all resource types as well, so
this moves us in that direction.

Signed-off-by: Joel Dice <[email protected]>

fix wave breakage due to new stream/future/error-context types

Signed-off-by: Joel Dice <[email protected]>
  • Loading branch information
dicej committed Nov 21, 2024
1 parent 8fd6208 commit 71897f2
Show file tree
Hide file tree
Showing 162 changed files with 12,235 additions and 1,892 deletions.
176 changes: 128 additions & 48 deletions Cargo.lock

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -276,16 +276,16 @@ wit-bindgen = { version = "0.35.0", default-features = false }
wit-bindgen-rust-macro = { version = "0.35.0", default-features = false }

# wasm-tools family:
wasmparser = { version = "0.220.0", default-features = false }
wat = "1.220.0"
wast = "220.0.0"
wasmprinter = "0.220.0"
wasm-encoder = "0.220.0"
wasm-smith = "0.220.0"
wasm-mutate = "0.220.0"
wit-parser = "0.220.0"
wit-component = "0.220.0"
wasm-wave = "0.220.0"
wasmparser = { git = "https://github.com/dicej/wasm-tools", branch = "async", default-features = false }
wat = { git = "https://github.com/dicej/wasm-tools", branch = "async" }
wast = { git = "https://github.com/dicej/wasm-tools", branch = "async" }
wasmprinter = { git = "https://github.com/dicej/wasm-tools", branch = "async" }
wasm-encoder = { git = "https://github.com/dicej/wasm-tools", branch = "async" }
wasm-smith = { git = "https://github.com/dicej/wasm-tools", branch = "async" }
wasm-mutate = { git = "https://github.com/dicej/wasm-tools", branch = "async" }
wit-parser = { git = "https://github.com/dicej/wasm-tools", branch = "async" }
wit-component = { git = "https://github.com/dicej/wasm-tools", branch = "async" }
wasm-wave = { git = "https://github.com/dicej/wasm-tools", branch = "async" }

# Non-Bytecode Alliance maintained dependencies:
# --------------------------
Expand Down
3 changes: 2 additions & 1 deletion benches/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,8 @@ mod component {
+ PartialEq
+ Debug
+ Send
+ Sync,
+ Sync
+ 'static,
{
// Benchmark the "typed" version.
c.bench_function(&format!("component - host-to-wasm - typed - {name}"), |b| {
Expand Down
1 change: 1 addition & 0 deletions crates/component-macro/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ similar = { workspace = true }
[features]
async = []
std = ['wasmtime-wit-bindgen/std']
component-model-async = ['std', 'async', 'wasmtime-wit-bindgen/component-model-async']
41 changes: 34 additions & 7 deletions crates/component-macro/src/bindgen.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
use proc_macro2::{Span, TokenStream};
use quote::ToTokens;
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::env;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
use syn::parse::{Error, Parse, ParseStream, Result};
use syn::punctuated::Punctuated;
use syn::{braced, token, Token};
use wasmtime_wit_bindgen::{AsyncConfig, Opts, Ownership, TrappableError, TrappableImports};
use wasmtime_wit_bindgen::{
AsyncConfig, CallStyle, Opts, Ownership, TrappableError, TrappableImports,
};
use wit_parser::{PackageId, Resolve, UnresolvedPackageGroup, WorldId};

pub struct Config {
Expand All @@ -20,13 +21,22 @@ pub struct Config {
}

pub fn expand(input: &Config) -> Result<TokenStream> {
if !cfg!(feature = "async") && input.opts.async_.maybe_async() {
if let (CallStyle::Async | CallStyle::Concurrent, false) =
(input.opts.call_style(), cfg!(feature = "async"))
{
return Err(Error::new(
Span::call_site(),
"cannot enable async bindings unless `async` crate feature is active",
));
}

if input.opts.concurrent_imports && !cfg!(feature = "component-model-async") {
return Err(Error::new(
Span::call_site(),
"cannot enable `concurrent_imports` option unless `component-model-async` crate feature is active",
));
}

let mut src = match input.opts.generate(&input.resolve, input.world) {
Ok(s) => s,
Err(e) => return Err(Error::new(Span::call_site(), e.to_string())),
Expand All @@ -40,7 +50,10 @@ pub fn expand(input: &Config) -> Result<TokenStream> {
// place a formatted version of the expanded code into a file. This file
// will then show up in rustc error messages for any codegen issues and can
// be inspected manually.
if input.include_generated_code_from_file || std::env::var("WASMTIME_DEBUG_BINDGEN").is_ok() {
if input.include_generated_code_from_file
|| input.opts.debug
|| std::env::var("WASMTIME_DEBUG_BINDGEN").is_ok()
{
static INVOCATION: AtomicUsize = AtomicUsize::new(0);
let root = Path::new(env!("DEBUG_OUTPUT_DIR"));
let world_name = &input.resolve.worlds[input.world].name;
Expand Down Expand Up @@ -107,13 +120,15 @@ impl Parse for Config {
}
Opt::Tracing(val) => opts.tracing = val,
Opt::VerboseTracing(val) => opts.verbose_tracing = val,
Opt::Debug(val) => opts.debug = val,
Opt::Async(val, span) => {
if async_configured {
return Err(Error::new(span, "cannot specify second async config"));
}
async_configured = true;
opts.async_ = val;
}
Opt::ConcurrentImports(val) => opts.concurrent_imports = val,
Opt::TrappableErrorType(val) => opts.trappable_error_type = val,
Opt::TrappableImports(val) => opts.trappable_imports = val,
Opt::Ownership(val) => opts.ownership = val,
Expand All @@ -138,7 +153,7 @@ impl Parse for Config {
"cannot specify a world with `interfaces`",
));
}
world = Some("interfaces".to_string());
world = Some("wasmtime:component-macro-synthesized/interfaces".to_string());

opts.only_interfaces = true;
}
Expand Down Expand Up @@ -281,6 +296,8 @@ mod kw {
syn::custom_keyword!(require_store_data_send);
syn::custom_keyword!(wasmtime_crate);
syn::custom_keyword!(include_generated_code_from_file);
syn::custom_keyword!(concurrent_imports);
syn::custom_keyword!(debug);
}

enum Opt {
Expand All @@ -301,12 +318,18 @@ enum Opt {
RequireStoreDataSend(bool),
WasmtimeCrate(syn::Path),
IncludeGeneratedCodeFromFile(bool),
ConcurrentImports(bool),
Debug(bool),
}

impl Parse for Opt {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let l = input.lookahead1();
if l.peek(kw::path) {
if l.peek(kw::debug) {
input.parse::<kw::debug>()?;
input.parse::<Token![:]>()?;
Ok(Opt::Debug(input.parse::<syn::LitBool>()?.value))
} else if l.peek(kw::path) {
input.parse::<kw::path>()?;
input.parse::<Token![:]>()?;

Expand Down Expand Up @@ -380,6 +403,10 @@ impl Parse for Opt {
span,
))
}
} else if l.peek(kw::concurrent_imports) {
input.parse::<kw::concurrent_imports>()?;
input.parse::<Token![:]>()?;
Ok(Opt::ConcurrentImports(input.parse::<syn::LitBool>()?.value))
} else if l.peek(kw::ownership) {
input.parse::<kw::ownership>()?;
input.parse::<Token![:]>()?;
Expand Down
29 changes: 21 additions & 8 deletions crates/component-macro/tests/expanded/char.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ impl<T> Clone for TheWorldPre<T> {
}
}
}
impl<_T> TheWorldPre<_T> {
impl<_T: 'static> TheWorldPre<_T> {
/// Creates a new copy of `TheWorldPre` bindings which can then
/// be used to instantiate into a particular store.
///
Expand Down Expand Up @@ -152,7 +152,10 @@ const _: () = {
mut store: impl wasmtime::AsContextMut<Data = _T>,
component: &wasmtime::component::Component,
linker: &wasmtime::component::Linker<_T>,
) -> wasmtime::Result<TheWorld> {
) -> wasmtime::Result<TheWorld>
where
_T: 'static,
{
let pre = linker.instantiate_pre(component)?;
TheWorldPre::new(pre)?.instantiate(store)
}
Expand Down Expand Up @@ -194,19 +197,23 @@ pub mod foo {
}
pub trait GetHost<
T,
>: Fn(T) -> <Self as GetHost<T>>::Host + Send + Sync + Copy + 'static {
D,
>: Fn(T) -> <Self as GetHost<T, D>>::Host + Send + Sync + Copy + 'static {
type Host: Host;
}
impl<F, T, O> GetHost<T> for F
impl<F, T, D, O> GetHost<T, D> for F
where
F: Fn(T) -> O + Send + Sync + Copy + 'static,
O: Host,
{
type Host = O;
}
pub fn add_to_linker_get_host<T>(
pub fn add_to_linker_get_host<
T,
G: for<'a> GetHost<&'a mut T, T, Host: Host>,
>(
linker: &mut wasmtime::component::Linker<T>,
host_getter: impl for<'a> GetHost<&'a mut T>,
host_getter: G,
) -> wasmtime::Result<()> {
let mut inst = linker.instance("foo:foo/chars")?;
inst.func_wrap(
Expand Down Expand Up @@ -354,7 +361,10 @@ pub mod exports {
&self,
mut store: S,
arg0: char,
) -> wasmtime::Result<()> {
) -> wasmtime::Result<()>
where
<S as wasmtime::AsContext>::Data: Send + 'static,
{
let callee = unsafe {
wasmtime::component::TypedFunc::<
(char,),
Expand All @@ -369,7 +379,10 @@ pub mod exports {
pub fn call_return_char<S: wasmtime::AsContextMut>(
&self,
mut store: S,
) -> wasmtime::Result<char> {
) -> wasmtime::Result<char>
where
<S as wasmtime::AsContext>::Data: Send + 'static,
{
let callee = unsafe {
wasmtime::component::TypedFunc::<
(),
Expand Down
25 changes: 13 additions & 12 deletions crates/component-macro/tests/expanded/char_async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ impl<T> Clone for TheWorldPre<T> {
}
}
}
impl<_T> TheWorldPre<_T> {
impl<_T: Send + 'static> TheWorldPre<_T> {
/// Creates a new copy of `TheWorldPre` bindings which can then
/// be used to instantiate into a particular store.
///
Expand Down Expand Up @@ -46,10 +46,7 @@ impl<_T> TheWorldPre<_T> {
pub async fn instantiate_async(
&self,
mut store: impl wasmtime::AsContextMut<Data = _T>,
) -> wasmtime::Result<TheWorld>
where
_T: Send,
{
) -> wasmtime::Result<TheWorld> {
let mut store = store.as_context_mut();
let instance = self.instance_pre.instantiate_async(&mut store).await?;
self.indices.load(&mut store, &instance)
Expand Down Expand Up @@ -157,7 +154,7 @@ const _: () = {
linker: &wasmtime::component::Linker<_T>,
) -> wasmtime::Result<TheWorld>
where
_T: Send,
_T: Send + 'static,
{
let pre = linker.instantiate_pre(component)?;
TheWorldPre::new(pre)?.instantiate_async(store).await
Expand Down Expand Up @@ -202,19 +199,23 @@ pub mod foo {
}
pub trait GetHost<
T,
>: Fn(T) -> <Self as GetHost<T>>::Host + Send + Sync + Copy + 'static {
D,
>: Fn(T) -> <Self as GetHost<T, D>>::Host + Send + Sync + Copy + 'static {
type Host: Host + Send;
}
impl<F, T, O> GetHost<T> for F
impl<F, T, D, O> GetHost<T, D> for F
where
F: Fn(T) -> O + Send + Sync + Copy + 'static,
O: Host + Send,
{
type Host = O;
}
pub fn add_to_linker_get_host<T>(
pub fn add_to_linker_get_host<
T,
G: for<'a> GetHost<&'a mut T, T, Host: Host + Send>,
>(
linker: &mut wasmtime::component::Linker<T>,
host_getter: impl for<'a> GetHost<&'a mut T>,
host_getter: G,
) -> wasmtime::Result<()>
where
T: Send,
Expand Down Expand Up @@ -373,7 +374,7 @@ pub mod exports {
arg0: char,
) -> wasmtime::Result<()>
where
<S as wasmtime::AsContext>::Data: Send,
<S as wasmtime::AsContext>::Data: Send + 'static,
{
let callee = unsafe {
wasmtime::component::TypedFunc::<
Expand All @@ -393,7 +394,7 @@ pub mod exports {
mut store: S,
) -> wasmtime::Result<char>
where
<S as wasmtime::AsContext>::Data: Send,
<S as wasmtime::AsContext>::Data: Send + 'static,
{
let callee = unsafe {
wasmtime::component::TypedFunc::<
Expand Down
25 changes: 13 additions & 12 deletions crates/component-macro/tests/expanded/char_tracing_async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ impl<T> Clone for TheWorldPre<T> {
}
}
}
impl<_T> TheWorldPre<_T> {
impl<_T: Send + 'static> TheWorldPre<_T> {
/// Creates a new copy of `TheWorldPre` bindings which can then
/// be used to instantiate into a particular store.
///
Expand Down Expand Up @@ -46,10 +46,7 @@ impl<_T> TheWorldPre<_T> {
pub async fn instantiate_async(
&self,
mut store: impl wasmtime::AsContextMut<Data = _T>,
) -> wasmtime::Result<TheWorld>
where
_T: Send,
{
) -> wasmtime::Result<TheWorld> {
let mut store = store.as_context_mut();
let instance = self.instance_pre.instantiate_async(&mut store).await?;
self.indices.load(&mut store, &instance)
Expand Down Expand Up @@ -157,7 +154,7 @@ const _: () = {
linker: &wasmtime::component::Linker<_T>,
) -> wasmtime::Result<TheWorld>
where
_T: Send,
_T: Send + 'static,
{
let pre = linker.instantiate_pre(component)?;
TheWorldPre::new(pre)?.instantiate_async(store).await
Expand Down Expand Up @@ -202,19 +199,23 @@ pub mod foo {
}
pub trait GetHost<
T,
>: Fn(T) -> <Self as GetHost<T>>::Host + Send + Sync + Copy + 'static {
D,
>: Fn(T) -> <Self as GetHost<T, D>>::Host + Send + Sync + Copy + 'static {
type Host: Host + Send;
}
impl<F, T, O> GetHost<T> for F
impl<F, T, D, O> GetHost<T, D> for F
where
F: Fn(T) -> O + Send + Sync + Copy + 'static,
O: Host + Send,
{
type Host = O;
}
pub fn add_to_linker_get_host<T>(
pub fn add_to_linker_get_host<
T,
G: for<'a> GetHost<&'a mut T, T, Host: Host + Send>,
>(
linker: &mut wasmtime::component::Linker<T>,
host_getter: impl for<'a> GetHost<&'a mut T>,
host_getter: G,
) -> wasmtime::Result<()>
where
T: Send,
Expand Down Expand Up @@ -402,7 +403,7 @@ pub mod exports {
arg0: char,
) -> wasmtime::Result<()>
where
<S as wasmtime::AsContext>::Data: Send,
<S as wasmtime::AsContext>::Data: Send + 'static,
{
use tracing::Instrument;
let span = tracing::span!(
Expand Down Expand Up @@ -431,7 +432,7 @@ pub mod exports {
mut store: S,
) -> wasmtime::Result<char>
where
<S as wasmtime::AsContext>::Data: Send,
<S as wasmtime::AsContext>::Data: Send + 'static,
{
use tracing::Instrument;
let span = tracing::span!(
Expand Down
Loading

0 comments on commit 71897f2

Please sign in to comment.