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]>
  • Loading branch information
dicej committed Nov 20, 2024
1 parent e32292c commit a5c2bce
Show file tree
Hide file tree
Showing 161 changed files with 12,002 additions and 1,887 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 a5c2bce

Please sign in to comment.