Skip to content

Commit c166a9f

Browse files
committed
Add support for async/streams/futures
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. 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]> switch wasm-tools to v1.220.0-based branch Signed-off-by: Joel Dice <[email protected]> check `task.return` type at runtime We can't statically verify a given call to `task.return` corresponds to the expected core signature appropriate for the currently running task, so we must do so at runtime. In order to make that check efficient, we intern the types. My initial plan was to use `ModuleInternedTypeIndex` and/or `VMSharedTypeIndex` for interning, but that got hairy with WasmGC considerations, so instead I added new fields to `ComponentTypes` and `ComponentTypesBuilder`. Signed-off-by: Joel Dice <[email protected]> add `TypedFunc::call_concurrent` and refine stream/future APIs This implements what I proposed in https://github.com/dicej/rfcs/blob/component-async/accepted/component-model-async.md#wasmtime. Specifically, it adds: - A new `Promise` type, useful for working with concurrent operations that require access to a `Store` to make progress. - A new `PromisesUnordered` type for `await`ing multiple promises concurrently -`TypedFunc::call_concurrent` (which returns a `Promise`), allowing multiple host->guest calls to run concurrently on the same instance. - Updated `{Stream|Future}{Writer|Reader}` APIs which use `Promise` The upshot is that the embedder can now ergonomically manage arbitrary numbers of concurrent operations. Previously, this was a lot more difficult to do without accidentally starving some of the operations due to another one monopolizing the `Store`. Finally, this includes various refactorings and fixes for bugs exposed by the newer, more versatile APIs. Signed-off-by: Joel Dice <[email protected]> clean up verbosity in component/func.rs Signed-off-by: Joel Dice <[email protected]> snapshot Signed-off-by: Joel Dice <[email protected]> implement stream/future read/write cancellation This required a somewhat viral addition of `Send` and `Sync` bounds for async host function closure types, unfortunately. Signed-off-by: Joel Dice <[email protected]> add `Func::call_concurrent` and `LinkerInstance::func_new_concurrent` Signed-off-by: Joel Dice <[email protected]> dynamic API support for streams/futures/error-contexts Signed-off-by: Joel Dice <[email protected]> support callback-less (AKA stackful) async lifts Signed-off-by: Joel Dice <[email protected]> fix `call_host` regression Signed-off-by: Joel Dice <[email protected]> add component model async end-to-end tests I've ported these over from https://github.com/dicej/component-async-demo Signed-off-by: Joel Dice <[email protected]> fix test regressions and clippy warnings Signed-off-by: Joel Dice <[email protected]> satisfy clippy Signed-off-by: Joel Dice <[email protected]>
1 parent 7c4f0c7 commit c166a9f

File tree

190 files changed

+17139
-2166
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

190 files changed

+17139
-2166
lines changed

Cargo.lock

+218-151
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+16-12
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ members = [
146146
"crates/bench-api",
147147
"crates/c-api/artifact",
148148
"crates/environ/fuzz",
149+
"crates/misc/component-async-tests",
149150
"crates/test-programs",
150151
"crates/wasi-preview1-component-adapter",
151152
"crates/wasi-preview1-component-adapter/verify",
@@ -229,6 +230,7 @@ wasmtime-versioned-export-macros = { path = "crates/versioned-export-macros", ve
229230
wasmtime-slab = { path = "crates/slab", version = "=29.0.0" }
230231
component-test-util = { path = "crates/misc/component-test-util" }
231232
component-fuzz-util = { path = "crates/misc/component-fuzz-util" }
233+
component-async-tests = { path = "crates/misc/component-async-tests" }
232234
wiggle = { path = "crates/wiggle", version = "=29.0.0", default-features = false }
233235
wiggle-macro = { path = "crates/wiggle/macro", version = "=29.0.0" }
234236
wiggle-generate = { path = "crates/wiggle/generate", version = "=29.0.0" }
@@ -281,20 +283,22 @@ io-lifetimes = { version = "2.0.3", default-features = false }
281283
io-extras = "0.18.1"
282284
rustix = "0.38.31"
283285
# wit-bindgen:
284-
wit-bindgen = { version = "0.35.0", default-features = false }
285-
wit-bindgen-rust-macro = { version = "0.35.0", default-features = false }
286+
wit-bindgen = { git = "https://github.com/dicej/wit-bindgen", branch = "async-v1.221.2", default-features = false }
287+
wit-bindgen-rt = { git = "https://github.com/dicej/wit-bindgen", branch = "async-v1.221.2", default-features = false }
288+
wit-bindgen-rust-macro = { git = "https://github.com/dicej/wit-bindgen", branch = "async-v1.221.2", default-features = false }
286289

287290
# wasm-tools family:
288-
wasmparser = { version = "0.221.2", default-features = false, features = ['simd'] }
289-
wat = "1.221.2"
290-
wast = "221.0.2"
291-
wasmprinter = "0.221.2"
292-
wasm-encoder = "0.221.2"
293-
wasm-smith = "0.221.2"
294-
wasm-mutate = "0.221.2"
295-
wit-parser = "0.221.2"
296-
wit-component = "0.221.2"
297-
wasm-wave = "0.221.2"
291+
wasmparser = { git = "https://github.com/dicej/wasm-tools", branch = "async-v1.221.2", default-features = false, features = ['simd'] }
292+
wat = { git = "https://github.com/dicej/wasm-tools", branch = "async-v1.221.2" }
293+
wast = { git = "https://github.com/dicej/wasm-tools", branch = "async-v1.221.2" }
294+
wasmprinter = { git = "https://github.com/dicej/wasm-tools", branch = "async-v1.221.2" }
295+
wasm-encoder = { git = "https://github.com/dicej/wasm-tools", branch = "async-v1.221.2" }
296+
wasm-smith = { git = "https://github.com/dicej/wasm-tools", branch = "async-v1.221.2" }
297+
wasm-mutate = { git = "https://github.com/dicej/wasm-tools", branch = "async-v1.221.2" }
298+
wit-parser = { git = "https://github.com/dicej/wasm-tools", branch = "async-v1.221.2" }
299+
wit-component = { git = "https://github.com/dicej/wasm-tools", branch = "async-v1.221.2" }
300+
wasm-wave = { git = "https://github.com/dicej/wasm-tools", branch = "async-v1.221.2" }
301+
wasm-compose = { git = "https://github.com/dicej/wasm-tools", branch = "async-v1.221.2" }
298302

299303
# Non-Bytecode Alliance maintained dependencies:
300304
# --------------------------

benches/call.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,8 @@ mod component {
628628
+ PartialEq
629629
+ Debug
630630
+ Send
631-
+ Sync,
631+
+ Sync
632+
+ 'static,
632633
{
633634
// Benchmark the "typed" version.
634635
c.bench_function(&format!("component - host-to-wasm - typed - {name}"), |b| {

crates/component-macro/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@ similar = { workspace = true }
4141
[features]
4242
async = []
4343
std = ['wasmtime-wit-bindgen/std']
44+
component-model-async = ['std', 'async', 'wasmtime-wit-bindgen/component-model-async']

crates/component-macro/src/bindgen.rs

+41-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
use proc_macro2::{Span, TokenStream};
22
use quote::ToTokens;
3-
use std::collections::HashMap;
4-
use std::collections::HashSet;
3+
use std::collections::{HashMap, HashSet};
54
use std::env;
65
use std::path::{Path, PathBuf};
76
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
87
use syn::parse::{Error, Parse, ParseStream, Result};
98
use syn::punctuated::Punctuated;
109
use syn::{braced, token, Token};
11-
use wasmtime_wit_bindgen::{AsyncConfig, Opts, Ownership, TrappableError, TrappableImports};
10+
use wasmtime_wit_bindgen::{
11+
AsyncConfig, CallStyle, Opts, Ownership, TrappableError, TrappableImports,
12+
};
1213
use wit_parser::{PackageId, Resolve, UnresolvedPackageGroup, WorldId};
1314

1415
pub struct Config {
@@ -20,13 +21,22 @@ pub struct Config {
2021
}
2122

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

33+
if input.opts.concurrent_imports && !cfg!(feature = "component-model-async") {
34+
return Err(Error::new(
35+
Span::call_site(),
36+
"cannot enable `concurrent_imports` option unless `component-model-async` crate feature is active",
37+
));
38+
}
39+
3040
let mut src = match input.opts.generate(&input.resolve, input.world) {
3141
Ok(s) => s,
3242
Err(e) => return Err(Error::new(Span::call_site(), e.to_string())),
@@ -40,7 +50,10 @@ pub fn expand(input: &Config) -> Result<TokenStream> {
4050
// place a formatted version of the expanded code into a file. This file
4151
// will then show up in rustc error messages for any codegen issues and can
4252
// be inspected manually.
43-
if input.include_generated_code_from_file || std::env::var("WASMTIME_DEBUG_BINDGEN").is_ok() {
53+
if input.include_generated_code_from_file
54+
|| input.opts.debug
55+
|| std::env::var("WASMTIME_DEBUG_BINDGEN").is_ok()
56+
{
4457
static INVOCATION: AtomicUsize = AtomicUsize::new(0);
4558
let root = Path::new(env!("DEBUG_OUTPUT_DIR"));
4659
let world_name = &input.resolve.worlds[input.world].name;
@@ -107,13 +120,16 @@ impl Parse for Config {
107120
}
108121
Opt::Tracing(val) => opts.tracing = val,
109122
Opt::VerboseTracing(val) => opts.verbose_tracing = val,
123+
Opt::Debug(val) => opts.debug = val,
110124
Opt::Async(val, span) => {
111125
if async_configured {
112126
return Err(Error::new(span, "cannot specify second async config"));
113127
}
114128
async_configured = true;
115129
opts.async_ = val;
116130
}
131+
Opt::ConcurrentImports(val) => opts.concurrent_imports = val,
132+
Opt::ConcurrentExports(val) => opts.concurrent_exports = val,
117133
Opt::TrappableErrorType(val) => opts.trappable_error_type = val,
118134
Opt::TrappableImports(val) => opts.trappable_imports = val,
119135
Opt::Ownership(val) => opts.ownership = val,
@@ -138,7 +154,7 @@ impl Parse for Config {
138154
"cannot specify a world with `interfaces`",
139155
));
140156
}
141-
world = Some("interfaces".to_string());
157+
world = Some("wasmtime:component-macro-synthesized/interfaces".to_string());
142158

143159
opts.only_interfaces = true;
144160
}
@@ -281,6 +297,9 @@ mod kw {
281297
syn::custom_keyword!(require_store_data_send);
282298
syn::custom_keyword!(wasmtime_crate);
283299
syn::custom_keyword!(include_generated_code_from_file);
300+
syn::custom_keyword!(concurrent_imports);
301+
syn::custom_keyword!(concurrent_exports);
302+
syn::custom_keyword!(debug);
284303
}
285304

286305
enum Opt {
@@ -301,12 +320,19 @@ enum Opt {
301320
RequireStoreDataSend(bool),
302321
WasmtimeCrate(syn::Path),
303322
IncludeGeneratedCodeFromFile(bool),
323+
ConcurrentImports(bool),
324+
ConcurrentExports(bool),
325+
Debug(bool),
304326
}
305327

306328
impl Parse for Opt {
307329
fn parse(input: ParseStream<'_>) -> Result<Self> {
308330
let l = input.lookahead1();
309-
if l.peek(kw::path) {
331+
if l.peek(kw::debug) {
332+
input.parse::<kw::debug>()?;
333+
input.parse::<Token![:]>()?;
334+
Ok(Opt::Debug(input.parse::<syn::LitBool>()?.value))
335+
} else if l.peek(kw::path) {
310336
input.parse::<kw::path>()?;
311337
input.parse::<Token![:]>()?;
312338

@@ -380,6 +406,14 @@ impl Parse for Opt {
380406
span,
381407
))
382408
}
409+
} else if l.peek(kw::concurrent_imports) {
410+
input.parse::<kw::concurrent_imports>()?;
411+
input.parse::<Token![:]>()?;
412+
Ok(Opt::ConcurrentImports(input.parse::<syn::LitBool>()?.value))
413+
} else if l.peek(kw::concurrent_exports) {
414+
input.parse::<kw::concurrent_exports>()?;
415+
input.parse::<Token![:]>()?;
416+
Ok(Opt::ConcurrentExports(input.parse::<syn::LitBool>()?.value))
383417
} else if l.peek(kw::ownership) {
384418
input.parse::<kw::ownership>()?;
385419
input.parse::<Token![:]>()?;

crates/component-macro/tests/expanded/char.rs

+21-8
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ impl<T> Clone for TheWorldPre<T> {
1818
}
1919
}
2020
}
21-
impl<_T> TheWorldPre<_T> {
21+
impl<_T: 'static> TheWorldPre<_T> {
2222
/// Creates a new copy of `TheWorldPre` bindings which can then
2323
/// be used to instantiate into a particular store.
2424
///
@@ -152,7 +152,10 @@ const _: () = {
152152
mut store: impl wasmtime::AsContextMut<Data = _T>,
153153
component: &wasmtime::component::Component,
154154
linker: &wasmtime::component::Linker<_T>,
155-
) -> wasmtime::Result<TheWorld> {
155+
) -> wasmtime::Result<TheWorld>
156+
where
157+
_T: 'static,
158+
{
156159
let pre = linker.instantiate_pre(component)?;
157160
TheWorldPre::new(pre)?.instantiate(store)
158161
}
@@ -194,19 +197,23 @@ pub mod foo {
194197
}
195198
pub trait GetHost<
196199
T,
197-
>: Fn(T) -> <Self as GetHost<T>>::Host + Send + Sync + Copy + 'static {
200+
D,
201+
>: Fn(T) -> <Self as GetHost<T, D>>::Host + Send + Sync + Copy + 'static {
198202
type Host: Host;
199203
}
200-
impl<F, T, O> GetHost<T> for F
204+
impl<F, T, D, O> GetHost<T, D> for F
201205
where
202206
F: Fn(T) -> O + Send + Sync + Copy + 'static,
203207
O: Host,
204208
{
205209
type Host = O;
206210
}
207-
pub fn add_to_linker_get_host<T>(
211+
pub fn add_to_linker_get_host<
212+
T,
213+
G: for<'a> GetHost<&'a mut T, T, Host: Host>,
214+
>(
208215
linker: &mut wasmtime::component::Linker<T>,
209-
host_getter: impl for<'a> GetHost<&'a mut T>,
216+
host_getter: G,
210217
) -> wasmtime::Result<()> {
211218
let mut inst = linker.instance("foo:foo/chars")?;
212219
inst.func_wrap(
@@ -354,7 +361,10 @@ pub mod exports {
354361
&self,
355362
mut store: S,
356363
arg0: char,
357-
) -> wasmtime::Result<()> {
364+
) -> wasmtime::Result<()>
365+
where
366+
<S as wasmtime::AsContext>::Data: Send + 'static,
367+
{
358368
let callee = unsafe {
359369
wasmtime::component::TypedFunc::<
360370
(char,),
@@ -369,7 +379,10 @@ pub mod exports {
369379
pub fn call_return_char<S: wasmtime::AsContextMut>(
370380
&self,
371381
mut store: S,
372-
) -> wasmtime::Result<char> {
382+
) -> wasmtime::Result<char>
383+
where
384+
<S as wasmtime::AsContext>::Data: Send + 'static,
385+
{
373386
let callee = unsafe {
374387
wasmtime::component::TypedFunc::<
375388
(),

crates/component-macro/tests/expanded/char_async.rs

+13-12
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ impl<T> Clone for TheWorldPre<T> {
1818
}
1919
}
2020
}
21-
impl<_T> TheWorldPre<_T> {
21+
impl<_T: Send + 'static> TheWorldPre<_T> {
2222
/// Creates a new copy of `TheWorldPre` bindings which can then
2323
/// be used to instantiate into a particular store.
2424
///
@@ -46,10 +46,7 @@ impl<_T> TheWorldPre<_T> {
4646
pub async fn instantiate_async(
4747
&self,
4848
mut store: impl wasmtime::AsContextMut<Data = _T>,
49-
) -> wasmtime::Result<TheWorld>
50-
where
51-
_T: Send,
52-
{
49+
) -> wasmtime::Result<TheWorld> {
5350
let mut store = store.as_context_mut();
5451
let instance = self.instance_pre.instantiate_async(&mut store).await?;
5552
self.indices.load(&mut store, &instance)
@@ -157,7 +154,7 @@ const _: () = {
157154
linker: &wasmtime::component::Linker<_T>,
158155
) -> wasmtime::Result<TheWorld>
159156
where
160-
_T: Send,
157+
_T: Send + 'static,
161158
{
162159
let pre = linker.instantiate_pre(component)?;
163160
TheWorldPre::new(pre)?.instantiate_async(store).await
@@ -202,19 +199,23 @@ pub mod foo {
202199
}
203200
pub trait GetHost<
204201
T,
205-
>: Fn(T) -> <Self as GetHost<T>>::Host + Send + Sync + Copy + 'static {
202+
D,
203+
>: Fn(T) -> <Self as GetHost<T, D>>::Host + Send + Sync + Copy + 'static {
206204
type Host: Host + Send;
207205
}
208-
impl<F, T, O> GetHost<T> for F
206+
impl<F, T, D, O> GetHost<T, D> for F
209207
where
210208
F: Fn(T) -> O + Send + Sync + Copy + 'static,
211209
O: Host + Send,
212210
{
213211
type Host = O;
214212
}
215-
pub fn add_to_linker_get_host<T>(
213+
pub fn add_to_linker_get_host<
214+
T,
215+
G: for<'a> GetHost<&'a mut T, T, Host: Host + Send>,
216+
>(
216217
linker: &mut wasmtime::component::Linker<T>,
217-
host_getter: impl for<'a> GetHost<&'a mut T>,
218+
host_getter: G,
218219
) -> wasmtime::Result<()>
219220
where
220221
T: Send,
@@ -372,7 +373,7 @@ pub mod exports {
372373
arg0: char,
373374
) -> wasmtime::Result<()>
374375
where
375-
<S as wasmtime::AsContext>::Data: Send,
376+
<S as wasmtime::AsContext>::Data: Send + 'static,
376377
{
377378
let callee = unsafe {
378379
wasmtime::component::TypedFunc::<
@@ -392,7 +393,7 @@ pub mod exports {
392393
mut store: S,
393394
) -> wasmtime::Result<char>
394395
where
395-
<S as wasmtime::AsContext>::Data: Send,
396+
<S as wasmtime::AsContext>::Data: Send + 'static,
396397
{
397398
let callee = unsafe {
398399
wasmtime::component::TypedFunc::<

0 commit comments

Comments
 (0)