Skip to content

Commit 1155d6d

Browse files
authored
Redesign function configuration in bindgen! (#11328)
* Redesign function configuration in `bindgen!` This commit is a redesign of how function-level configuration works in Wasmtime's `bindgen!` macro. The main goal of this redesign is to better support WASIp3 and component model async functions. Prior to this redesign there was a mish mash of mechanisms to configure behavior of imports/exports: * The `async` configuration could turn everything async, nothing async, only some imports async, or everything except some imports async. * The `concurrent_{imports,exports}` keys were required to explicitly opt-in to component model async signatures and applied to all imports/exports. * The `trappable_imports` configuration would indicate a list of imports allowed to trap and it had special configuration for everything, nothing, and only a certain list. * The `tracing` and `verbose_tracing` keys could be applied to either nothing or all functions. Overall the previous state of configuration in `bindgen!` was clearly a hodgepodge of systems that organically grew over time. In my personal opinion it was in dire need of a refresh to take into account how component-model-async ended up being implemented as well as consolidating the one-off systems amongst all of these configuration keys. A major motivation of this redesign, for example, was to inherit behavior from WIT files by default. An `async` function in WIT should not require `concurrent_*` keys to be configured, but rather it should generate correct bindings by default. In this commit, all of the above keys were removed. All keys have been replaced with `imports` and `exports` configuration keys. Each behaves the same way and looks like so: bindgen!({ // ... imports: { // enable tracing for just this function "my:local/interface/func": tracing, // enable verbose tracing for just this function "my:local/interface/other-func": tracing | verbose_tracing, // this is blocking in WIT, but generate async bindings for // it "my:local/interface/[method]io.block": async, // like above, but use "concurrent" bindings which have // access to the store. "my:local/interface/[method]io.block-again": async | store, // everything else is, by default, trappable default: trappable, }, }); Effectively all the function-level configuration items are now bitflags. These bitflags are by default inherited from the WIT files itself (e.g. `async` functions are `async | store` by default). Further configuration is then layered on top at the desires of the embedder. Supported keys are: * `async` - this means that a Rust-level `async` function should be generated. This is either `CallStyle::Async` or `CallStyle::Concurrent` as it was prior, depending on ... * `store` - this means that the generated function will have access to the store on the host. This is only implemented right now for `async | store` functions which map to `CallStyle::Concurrent`. In the future I'd like to support just-`store` functions which means that you could define a synchronous function with access to the store in addition to an asynchronous function. * `trappable` - this means that the function returns a `wasmtime::Result<TheWitBindingType>`. If `trappable_errors` is applicable then it means just a `Result<TheWitOkType, TrappableErrorType>` is returned (like before) * `tracing` - this enables `tracing!` integration for this function. * `verbose_tracing` - this logs all argument values for this function (including lists). * `ignore_wit` - this ignores the WIT-level defaults of the function (e.g. ignoring WIT `async`). The way this then works is all modeled is that for any WIT function being generated there are a set of flags associated with that function. To calculate the flags the algorithm looks like: 1. Find the first matching rule in the `imports` or `exports` map depending on if the function is imported or exported. If there is no matching rule then use the `default` rule if present. This is the initial set of flags for the function (or empty if nothing was found). 2. If `ignore_wit` is present, return the flags from step 1. Otherwise add in `async | store` if the function is `async` in WIT. The resulting set of flags are then used to control how everything is generated. For example the same split traits of today are still generated and it's controlled based on the flags. Note though that the previous `HostConcurrent` trait was renamed to `HostWithStore` to make space for synchronous functions in this trait in the future too. The end result of all these changes is that configuring imports/exports now uses the exact same selection system as the `with` replacement map, meaning there's only one system of selecting functions instead of 3. WIT-level `async` is now respected by default meaning that bindings work by default without further need to configure anything (unless more functionality is desired). One final minor change made here as well is that auto-generated `instantiate` methods are now always synchronous and an `instantiate_async` method is unconditionally generated for async mode. This means that bindings always generate both functions and it's up to the embedder to choose the appropriate one. Closes #11246 Closes #11247 * Update expanded test expectations prtest:full * Fix the min platform embedding example * Fix doc tests * Always generate `*WithStore` traits This helps when using the `with` mapping since that can always assume that `HostWithStore` is available in the generated bindings, avoiding the need to duplicate configuration options. * Update test expectations * Review comments
1 parent ae25a92 commit 1155d6d

File tree

183 files changed

+9313
-7184
lines changed

Some content is hidden

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

183 files changed

+9313
-7184
lines changed

Cargo.lock

Lines changed: 1 addition & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ gimli = { workspace = true, optional = true }
7979
pulley-interpreter = { workspace = true, optional = true }
8080

8181
async-trait = { workspace = true }
82-
trait-variant = { workspace = true }
8382
bytes = { workspace = true }
8483
cfg-if = { workspace = true }
8584
tokio = { workspace = true, optional = true, features = [ "signal", "macros" ] }
@@ -103,7 +102,6 @@ criterion = { workspace = true }
103102
num_cpus = "1.13.0"
104103
memchr = "2.4"
105104
async-trait = { workspace = true }
106-
trait-variant = { workspace = true }
107105
wat = { workspace = true }
108106
rayon = "1.5.0"
109107
wasmtime-wast = { workspace = true, features = ['component-model'] }
@@ -363,7 +361,6 @@ tracing = "0.1.26"
363361
bitflags = "2.0"
364362
thiserror = "2.0.12"
365363
async-trait = "0.1.71"
366-
trait-variant = "0.1.2"
367364
heck = "0.5"
368365
similar = "2.1.0"
369366
toml = "0.8.10"

crates/component-macro/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,5 @@ prettyplease = "0.2.31"
4040
similar = { workspace = true }
4141

4242
[features]
43-
async = []
43+
async = ['wasmtime-wit-bindgen/async']
4444
component-model-async = ['async', 'wasmtime-wit-bindgen/component-model-async']

crates/component-macro/src/bindgen.rs

Lines changed: 105 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
use proc_macro2::{Span, TokenStream};
22
use quote::ToTokens;
3-
use std::collections::{HashMap, HashSet};
3+
use std::collections::HashMap;
44
use std::env;
55
use std::path::{Path, PathBuf};
66
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
77
use syn::parse::{Error, Parse, ParseStream, Result};
88
use syn::punctuated::Punctuated;
99
use syn::{Token, braced, token};
1010
use wasmtime_wit_bindgen::{
11-
AsyncConfig, CallStyle, Opts, Ownership, TrappableError, TrappableImports,
11+
FunctionConfig, FunctionFilter, FunctionFlags, Opts, Ownership, TrappableError,
1212
};
1313
use wit_parser::{PackageId, Resolve, UnresolvedPackageGroup, WorldId};
1414

@@ -21,22 +21,6 @@ pub struct Config {
2121
}
2222

2323
pub fn expand(input: &Config) -> Result<TokenStream> {
24-
if let (CallStyle::Async | CallStyle::Concurrent, false) =
25-
(input.opts.call_style(), cfg!(feature = "async"))
26-
{
27-
return Err(Error::new(
28-
Span::call_site(),
29-
"cannot enable async bindings unless `async` crate feature is active",
30-
));
31-
}
32-
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-
4024
let mut src = match input.opts.generate(&input.resolve, input.world) {
4125
Ok(s) => s,
4226
Err(e) => return Err(Error::new(Span::call_site(), e.to_string())),
@@ -94,7 +78,8 @@ impl Parse for Config {
9478
let mut world = None;
9579
let mut inline = None;
9680
let mut paths = Vec::new();
97-
let mut async_configured = false;
81+
let mut imports_configured = false;
82+
let mut exports_configured = false;
9883
let mut include_generated_code_from_file = false;
9984

10085
if input.peek(token::Brace) {
@@ -118,20 +103,8 @@ impl Parse for Config {
118103
}
119104
inline = Some(s.value());
120105
}
121-
Opt::Tracing(val) => opts.tracing = val,
122-
Opt::VerboseTracing(val) => opts.verbose_tracing = val,
123106
Opt::Debug(val) => opts.debug = val,
124-
Opt::Async(val, span) => {
125-
if async_configured {
126-
return Err(Error::new(span, "cannot specify second async config"));
127-
}
128-
async_configured = true;
129-
opts.async_ = val;
130-
}
131-
Opt::ConcurrentImports(val) => opts.concurrent_imports = val,
132-
Opt::ConcurrentExports(val) => opts.concurrent_exports = val,
133107
Opt::TrappableErrorType(val) => opts.trappable_error_type = val,
134-
Opt::TrappableImports(val) => opts.trappable_imports = val,
135108
Opt::Ownership(val) => opts.ownership = val,
136109
Opt::Interfaces(s) => {
137110
if inline.is_some() {
@@ -172,6 +145,20 @@ impl Parse for Config {
172145
opts.wasmtime_crate = Some(f.into_token_stream().to_string())
173146
}
174147
Opt::IncludeGeneratedCodeFromFile(i) => include_generated_code_from_file = i,
148+
Opt::Imports(config, span) => {
149+
if imports_configured {
150+
return Err(Error::new(span, "cannot specify imports configuration"));
151+
}
152+
opts.imports = config;
153+
imports_configured = true;
154+
}
155+
Opt::Exports(config, span) => {
156+
if exports_configured {
157+
return Err(Error::new(span, "cannot specify exports configuration"));
158+
}
159+
opts.exports = config;
160+
exports_configured = true;
161+
}
175162
}
176163
}
177164
} else {
@@ -290,39 +277,38 @@ mod kw {
290277
syn::custom_keyword!(with);
291278
syn::custom_keyword!(except_imports);
292279
syn::custom_keyword!(only_imports);
293-
syn::custom_keyword!(trappable_imports);
294280
syn::custom_keyword!(additional_derives);
295281
syn::custom_keyword!(stringify);
296282
syn::custom_keyword!(skip_mut_forwarding_impls);
297283
syn::custom_keyword!(require_store_data_send);
298284
syn::custom_keyword!(wasmtime_crate);
299285
syn::custom_keyword!(include_generated_code_from_file);
300-
syn::custom_keyword!(concurrent_imports);
301-
syn::custom_keyword!(concurrent_exports);
302286
syn::custom_keyword!(debug);
287+
syn::custom_keyword!(imports);
288+
syn::custom_keyword!(exports);
289+
syn::custom_keyword!(store);
290+
syn::custom_keyword!(trappable);
291+
syn::custom_keyword!(ignore_wit);
292+
syn::custom_keyword!(exact);
303293
}
304294

305295
enum Opt {
306296
World(syn::LitStr),
307297
Path(Vec<syn::LitStr>),
308298
Inline(syn::LitStr),
309-
Tracing(bool),
310-
VerboseTracing(bool),
311-
Async(AsyncConfig, Span),
312299
TrappableErrorType(Vec<TrappableError>),
313300
Ownership(Ownership),
314301
Interfaces(syn::LitStr),
315302
With(HashMap<String, String>),
316-
TrappableImports(TrappableImports),
317303
AdditionalDerives(Vec<syn::Path>),
318304
Stringify(bool),
319305
SkipMutForwardingImpls(bool),
320306
RequireStoreDataSend(bool),
321307
WasmtimeCrate(syn::Path),
322308
IncludeGeneratedCodeFromFile(bool),
323-
ConcurrentImports(bool),
324-
ConcurrentExports(bool),
325309
Debug(bool),
310+
Imports(FunctionConfig, Span),
311+
Exports(FunctionConfig, Span),
326312
}
327313

328314
impl Parse for Opt {
@@ -360,60 +346,6 @@ impl Parse for Opt {
360346
input.parse::<kw::world>()?;
361347
input.parse::<Token![:]>()?;
362348
Ok(Opt::World(input.parse()?))
363-
} else if l.peek(kw::tracing) {
364-
input.parse::<kw::tracing>()?;
365-
input.parse::<Token![:]>()?;
366-
Ok(Opt::Tracing(input.parse::<syn::LitBool>()?.value))
367-
} else if l.peek(kw::verbose_tracing) {
368-
input.parse::<kw::verbose_tracing>()?;
369-
input.parse::<Token![:]>()?;
370-
Ok(Opt::VerboseTracing(input.parse::<syn::LitBool>()?.value))
371-
} else if l.peek(Token![async]) {
372-
let span = input.parse::<Token![async]>()?.span;
373-
input.parse::<Token![:]>()?;
374-
if input.peek(syn::LitBool) {
375-
match input.parse::<syn::LitBool>()?.value {
376-
true => Ok(Opt::Async(AsyncConfig::All, span)),
377-
false => Ok(Opt::Async(AsyncConfig::None, span)),
378-
}
379-
} else {
380-
let contents;
381-
syn::braced!(contents in input);
382-
383-
let l = contents.lookahead1();
384-
let ctor: fn(HashSet<String>) -> AsyncConfig = if l.peek(kw::except_imports) {
385-
contents.parse::<kw::except_imports>()?;
386-
contents.parse::<Token![:]>()?;
387-
AsyncConfig::AllExceptImports
388-
} else if l.peek(kw::only_imports) {
389-
contents.parse::<kw::only_imports>()?;
390-
contents.parse::<Token![:]>()?;
391-
AsyncConfig::OnlyImports
392-
} else {
393-
return Err(l.error());
394-
};
395-
396-
let list;
397-
syn::bracketed!(list in contents);
398-
let fields: Punctuated<syn::LitStr, Token![,]> =
399-
list.parse_terminated(Parse::parse, Token![,])?;
400-
401-
if contents.peek(Token![,]) {
402-
contents.parse::<Token![,]>()?;
403-
}
404-
Ok(Opt::Async(
405-
ctor(fields.iter().map(|s| s.value()).collect()),
406-
span,
407-
))
408-
}
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))
417349
} else if l.peek(kw::ownership) {
418350
input.parse::<kw::ownership>()?;
419351
input.parse::<Token![:]>()?;
@@ -472,22 +404,6 @@ impl Parse for Opt {
472404
let fields: Punctuated<(String, String), Token![,]> =
473405
contents.parse_terminated(with_field_parse, Token![,])?;
474406
Ok(Opt::With(HashMap::from_iter(fields)))
475-
} else if l.peek(kw::trappable_imports) {
476-
input.parse::<kw::trappable_imports>()?;
477-
input.parse::<Token![:]>()?;
478-
let config = if input.peek(syn::LitBool) {
479-
match input.parse::<syn::LitBool>()?.value {
480-
true => TrappableImports::All,
481-
false => TrappableImports::None,
482-
}
483-
} else {
484-
let contents;
485-
syn::bracketed!(contents in input);
486-
let fields: Punctuated<syn::LitStr, Token![,]> =
487-
contents.parse_terminated(Parse::parse, Token![,])?;
488-
TrappableImports::Only(fields.iter().map(|s| s.value()).collect())
489-
};
490-
Ok(Opt::TrappableImports(config))
491407
} else if l.peek(kw::additional_derives) {
492408
input.parse::<kw::additional_derives>()?;
493409
input.parse::<Token![:]>()?;
@@ -521,6 +437,14 @@ impl Parse for Opt {
521437
Ok(Opt::IncludeGeneratedCodeFromFile(
522438
input.parse::<syn::LitBool>()?.value,
523439
))
440+
} else if l.peek(kw::imports) {
441+
let span = input.parse::<kw::imports>()?.span;
442+
input.parse::<Token![:]>()?;
443+
Ok(Opt::Imports(parse_function_config(input)?, span))
444+
} else if l.peek(kw::exports) {
445+
let span = input.parse::<kw::exports>()?.span;
446+
input.parse::<Token![:]>()?;
447+
Ok(Opt::Exports(parse_function_config(input)?, span))
524448
} else {
525449
Err(l.error())
526450
}
@@ -579,3 +503,74 @@ fn with_field_parse(input: ParseStream<'_>) -> Result<(String, String)> {
579503

580504
Ok((interface, buf))
581505
}
506+
507+
fn parse_function_config(input: ParseStream<'_>) -> Result<FunctionConfig> {
508+
let content;
509+
syn::braced!(content in input);
510+
let mut ret = FunctionConfig::new();
511+
512+
let list = Punctuated::<FunctionConfigSyntax, Token![,]>::parse_terminated(&content)?;
513+
for item in list.into_iter() {
514+
ret.push(item.filter, item.flags);
515+
}
516+
517+
return Ok(ret);
518+
519+
struct FunctionConfigSyntax {
520+
filter: FunctionFilter,
521+
flags: FunctionFlags,
522+
}
523+
524+
impl Parse for FunctionConfigSyntax {
525+
fn parse(input: ParseStream<'_>) -> Result<Self> {
526+
let l = input.lookahead1();
527+
let filter = if l.peek(syn::LitStr) {
528+
FunctionFilter::Name(input.parse::<syn::LitStr>()?.value())
529+
} else if l.peek(Token![default]) {
530+
input.parse::<Token![default]>()?;
531+
FunctionFilter::Default
532+
} else {
533+
return Err(l.error());
534+
};
535+
536+
input.parse::<Token![:]>()?;
537+
538+
let mut flags = FunctionFlags::empty();
539+
while !input.is_empty() {
540+
let l = input.lookahead1();
541+
if l.peek(Token![async]) {
542+
input.parse::<Token![async]>()?;
543+
flags |= FunctionFlags::ASYNC;
544+
} else if l.peek(kw::tracing) {
545+
input.parse::<kw::tracing>()?;
546+
flags |= FunctionFlags::TRACING;
547+
} else if l.peek(kw::verbose_tracing) {
548+
input.parse::<kw::verbose_tracing>()?;
549+
flags |= FunctionFlags::VERBOSE_TRACING;
550+
} else if l.peek(kw::store) {
551+
input.parse::<kw::store>()?;
552+
flags |= FunctionFlags::STORE;
553+
} else if l.peek(kw::trappable) {
554+
input.parse::<kw::trappable>()?;
555+
flags |= FunctionFlags::TRAPPABLE;
556+
} else if l.peek(kw::ignore_wit) {
557+
input.parse::<kw::ignore_wit>()?;
558+
flags |= FunctionFlags::IGNORE_WIT;
559+
} else if l.peek(kw::exact) {
560+
input.parse::<kw::exact>()?;
561+
flags |= FunctionFlags::EXACT;
562+
} else {
563+
return Err(l.error());
564+
}
565+
566+
if input.peek(Token![|]) {
567+
input.parse::<Token![|]>()?;
568+
} else {
569+
break;
570+
}
571+
}
572+
573+
Ok(FunctionConfigSyntax { filter, flags })
574+
}
575+
}
576+
}

0 commit comments

Comments
 (0)