From ecaad944f595801f8eda2cd451939b1ed2373559 Mon Sep 17 00:00:00 2001 From: Andrew Steurer <94206073+asteurer@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:05:13 -0500 Subject: [PATCH] feat(docs): add check to stop sync exports from calling async imports + updated docs Signed-off-by: Andrew Steurer <94206073+asteurer@users.noreply.github.com> --- crates/component-macro/src/bindgen.rs | 56 +++++++++++++++++++- crates/wasmtime/src/runtime/component/mod.rs | 46 +++++++++++++--- 2 files changed, 93 insertions(+), 9 deletions(-) diff --git a/crates/component-macro/src/bindgen.rs b/crates/component-macro/src/bindgen.rs index 852521c3acb2..2fac9ebc5cc8 100644 --- a/crates/component-macro/src/bindgen.rs +++ b/crates/component-macro/src/bindgen.rs @@ -10,7 +10,7 @@ use syn::{Token, braced, token}; use wasmtime_wit_bindgen::{ FunctionConfig, FunctionFilter, FunctionFlags, Opts, Ownership, TrappableError, }; -use wit_parser::{PackageId, Resolve, WorldId}; +use wit_parser::{Function, IndexMap, PackageId, Resolve, WorldId, WorldItem, WorldKey}; pub struct Config { opts: Opts, @@ -30,6 +30,34 @@ pub fn expand(input: &mut Config) -> Result { return Ok(quote::quote!(#src)); } + let world = &input.resolve.worlds[input.world]; + let sync_exports = fn_filter(&input.resolve, &world.exports, |f| !f.kind.is_async()); + let async_imports = fn_filter(&input.resolve, &world.imports, |f| f.kind.is_async()); + if !sync_exports.is_empty() && !async_imports.is_empty() { + let format_list = |names: &[String]| { + names + .iter() + .map(|n| format!(" - {n}\n")) + .collect::() + }; + + return Err(Error::new( + Span::call_site(), + format!( + "world `{world}` mixes synchronous exports with asynchronous imports, which is not allowed\n\ + \n\ + synchronous exports:\n{sync_exports}\n\ + asynchronous imports:\n{async_imports}\n\ + \n\ + help: synchronous exports that call asynchronous imports will trap at runtime. Either mark the \ + listed exports `async`, or make the listed imports synchronous", + world = world.name, + sync_exports = format_list(&sync_exports), + async_imports = format_list(&async_imports), + ), + )); + } + // If a magical `WASMTIME_DEBUG_BINDGEN` environment variable is set then // 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 @@ -74,6 +102,32 @@ pub fn expand(input: &mut Config) -> Result { Ok(contents) } +/// Get a list of world and interface-level function names that match some predicate +fn fn_filter( + resolve: &Resolve, + items: &IndexMap, + pred: impl Fn(&Function) -> bool, +) -> Vec { + let mut names = vec![]; + for (key, item) in items { + match item { + WorldItem::Function(f) if pred(f) => names.push(f.name.clone()), + WorldItem::Interface { id, .. } => { + let iface = resolve.name_world_key(key); + names.extend( + resolve.interfaces[*id] + .functions + .values() + .filter(|f| pred(f)) + .map(|f| format!("{iface}.{}", f.name)), + ); + } + _ => {} + } + } + names +} + impl Parse for Config { fn parse(input: ParseStream<'_>) -> Result { let call_site = Span::call_site(); diff --git a/crates/wasmtime/src/runtime/component/mod.rs b/crates/wasmtime/src/runtime/component/mod.rs index 2467367f8cca..9bdad288bcb3 100644 --- a/crates/wasmtime/src/runtime/component/mod.rs +++ b/crates/wasmtime/src/runtime/component/mod.rs @@ -271,11 +271,28 @@ pub(crate) use self::store::ComponentStoreData; /// // functionality per-function or by default for all imports. Note that /// // exports are also supported via the `exports` key below. /// // -/// // Functions in this list are specified as their interface first then -/// // the raw wasm name of the function. Interface versions can be -/// // optionally omitted and prefixes are also supported to configure -/// // entire interfaces at once for example. Only the first matching item -/// // in this list is used to configure a function. +/// // Each key names either one function or a group of functions: +/// // +/// // * `"my-func"` - a function imported directly by the world. +/// // * `"wasi:io/poll.poll"` - the `poll` function in the `wasi:io/poll` +/// // interface. A version is optional: `"wasi:io/poll@0.2.0.poll"`. +/// // * `"wasi:io/poll"` - every function in the `wasi:io/poll` interface. +/// // * `"wasi:io"` or `"wasi"` - every function in a package or namespace. +/// // +/// // Resource functions are named by their component-model name, written +/// // after the interface: +/// // +/// // * `"my:local/api.[constructor]my-resource"` +/// // * `"my:local/api.[method]my-resource.my-method"` +/// // * `"my:local/api.[static]my-resource.my-static-func"` +/// // * `"my:local/api.[drop]my-resource"` +/// // +/// // A resource name on its own (`"my:local/api.my-resource"`) isn't a valid +/// // key. List each resource function by itself, or use the interface key +/// // to cover all of them. +/// // +/// // When more than one key matches a function, the most specific one is +/// // used and the rest are ignored for that function. /// // /// // Configuration for a function is a set of flags which can be added /// // per-function. Each flag's meaning is documented below and the final @@ -353,6 +370,14 @@ pub(crate) use self::store::ComponentStoreData; /// // for example and instead return `Result`. /// "my:local/api.fallible": trappable, /// +/// // Resource methods, constructors, and static functions are +/// // configured with their `[method]`, `[constructor]`, `[drop]`, or `[static]` +/// // name as described above. +/// "my:local/api.[method]my-resource.my-method": async | trappable, +/// "my:local/api.[static]my-resource.my-static-func": trappable, +/// "my:local/api.[constructor]my-resource": trappable, +/// "my:local/api.[drop]my-resource": trappable, +/// /// // The `exact` flag ensures that the filter, here "f", only matches /// // functions exactly. For example "f" here would only refer to /// // `import f: func()` in a world. Without this flag then "f" @@ -369,9 +394,14 @@ pub(crate) use self::store::ComponentStoreData; /// /// // Mostly the same as `imports` above, but applies to exported functions. /// // -/// // The one difference here is that, whereas the `task_exit` flag has no -/// // effect for `imports`, it changes how bindings are generated for -/// // exported functions as described below. +/// // Two notable differences from `imports`: +/// // 1. Whereas the `task_exit` flag has no +/// // effect for `imports`, it changes how bindings are generated for +/// // exported functions as described below. +/// // 2. If any import is marked `async`, all exports must be `async` too, +/// // or `bindgen!` reports a compile error; otherwise, a synchronous +/// // function export that calls an `async` function import will trap at +/// // runtime. Synchronous and `async` imports may be freely mixed. /// exports: { /// /* ... */ ///