use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use std::collections::HashMap; use std::collections::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 wit_parser::{PackageId, Resolve, UnresolvedPackageGroup, WorldId}; pub struct Config { opts: Opts, resolve: Resolve, world: WorldId, files: Vec, include_generated_code_from_file: bool, } pub fn expand(input: &Config) -> Result { if !cfg!(feature = "async") && input.opts.async_.maybe_async() { return Err(Error::new( Span::call_site(), "cannot enable async bindings unless `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())), }; if input.opts.stringify { return Ok(quote::quote!(#src)); } // 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 // be inspected manually. if input.include_generated_code_from_file || 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; let n = INVOCATION.fetch_add(1, Relaxed); let path = root.join(format!("{world_name}{n}.rs")); std::fs::write(&path, &src).unwrap(); // optimistically format the code but don't require success drop( std::process::Command::new("rustfmt") .arg(&path) .arg("--edition=2021") .output(), ); src = format!("include!({path:?});"); } let mut contents = src.parse::().unwrap(); // Include a dummy `include_str!` for any files we read so rustc knows that // we depend on the contents of those files. for file in input.files.iter() { contents.extend( format!("const _: &str = include_str!(r#\"{}\"#);\n", file.display()) .parse::() .unwrap(), ); } Ok(contents) } impl Parse for Config { fn parse(input: ParseStream<'_>) -> Result { let call_site = Span::call_site(); let mut opts = Opts::default(); let mut world = None; let mut inline = None; let mut paths = Vec::new(); let mut async_configured = false; let mut features = Vec::new(); let mut include_generated_code_from_file = false; if input.peek(token::Brace) { let content; syn::braced!(content in input); let fields = Punctuated::::parse_terminated(&content)?; for field in fields.into_pairs() { match field.into_value() { Opt::Path(p) => { paths.extend(p.into_iter().map(|p| p.value())); } Opt::World(s) => { if world.is_some() { return Err(Error::new(s.span(), "cannot specify second world")); } world = Some(s.value()); } Opt::Inline(s) => { if inline.is_some() { return Err(Error::new(s.span(), "cannot specify second source")); } inline = Some(s.value()); } Opt::Tracing(val) => opts.tracing = val, Opt::VerboseTracing(val) => opts.verbose_tracing = 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::TrappableErrorType(val) => opts.trappable_error_type = val, Opt::TrappableImports(val) => opts.trappable_imports = val, Opt::Ownership(val) => opts.ownership = val, Opt::Interfaces(s) => { if inline.is_some() { return Err(Error::new(s.span(), "cannot specify a second source")); } inline = Some(format!( " package wasmtime:component-macro-synthesized; world interfaces {{ {} }} ", s.value() )); if world.is_some() { return Err(Error::new( s.span(), "cannot specify a world with `interfaces`", )); } world = Some("interfaces".to_string()); opts.only_interfaces = true; } Opt::With(val) => opts.with.extend(val), Opt::AdditionalDerives(paths) => { opts.additional_derive_attributes = paths .into_iter() .map(|p| p.into_token_stream().to_string()) .collect() } Opt::Stringify(val) => opts.stringify = val, Opt::SkipMutForwardingImpls(val) => opts.skip_mut_forwarding_impls = val, Opt::Features(f) => { features.extend(f.into_iter().map(|f| f.value())); } Opt::RequireStoreDataSend(val) => opts.require_store_data_send = val, Opt::WasmtimeCrate(f) => { opts.wasmtime_crate = Some(f.into_token_stream().to_string()) } Opt::IncludeGeneratedCodeFromFile(i) => include_generated_code_from_file = i, } } } else { world = input.parse::>()?.map(|s| s.value()); if input.parse::>()?.is_some() { paths.push(input.parse::()?.value()); } } let (resolve, pkgs, files) = parse_source(&paths, &inline, &features) .map_err(|err| Error::new(call_site, format!("{err:?}")))?; let world = select_world(&resolve, &pkgs, world.as_deref()) .map_err(|e| Error::new(call_site, format!("{e:?}")))?; Ok(Config { opts, resolve, world, files, include_generated_code_from_file, }) } } fn parse_source( paths: &Vec, inline: &Option, features: &[String], ) -> anyhow::Result<(Resolve, Vec, Vec)> { let mut resolve = Resolve::default(); resolve.features.extend(features.iter().cloned()); let mut files = Vec::new(); let mut pkgs = Vec::new(); let root = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); let parse = |resolve: &mut Resolve, files: &mut Vec, pkgs: &mut Vec, paths: &[String]| -> anyhow::Result<_> { for path in paths { let p = root.join(path); // Try to normalize the path to make the error message more understandable when // the path is not correct. Fallback to the original path if normalization fails // (probably return an error somewhere else). let normalized_path = match std::fs::canonicalize(&p) { Ok(p) => p, Err(_) => p.to_path_buf(), }; let (pkg, sources) = resolve.push_path(normalized_path)?; pkgs.push(pkg); files.extend(sources); } Ok(()) }; if !paths.is_empty() { parse(&mut resolve, &mut files, &mut pkgs, &paths)?; } if let Some(inline) = inline { pkgs.push(resolve.push_group(UnresolvedPackageGroup::parse("macro-input", inline)?)?); } if pkgs.is_empty() { parse(&mut resolve, &mut files, &mut pkgs, &["wit".into()])?; } Ok((resolve, pkgs, files)) } fn select_world( resolve: &Resolve, pkgs: &[PackageId], world: Option<&str>, ) -> anyhow::Result { if pkgs.len() == 1 { resolve.select_world(pkgs[0], world) } else { assert!(!pkgs.is_empty()); match world { Some(name) => { if !name.contains(":") { anyhow::bail!( "with multiple packages a fully qualified \ world name must be specified" ) } // This will ignore the package argument due to the fully // qualified name being used. resolve.select_world(pkgs[0], world) } None => { let worlds = pkgs .iter() .filter_map(|p| resolve.select_world(*p, None).ok()) .collect::>(); match &worlds[..] { [] => anyhow::bail!("no packages have a world"), [world] => Ok(*world), _ => anyhow::bail!("multiple packages have a world, must specify which to use"), } } } } } mod kw { syn::custom_keyword!(inline); syn::custom_keyword!(path); syn::custom_keyword!(tracing); syn::custom_keyword!(verbose_tracing); syn::custom_keyword!(trappable_error_type); syn::custom_keyword!(world); syn::custom_keyword!(ownership); syn::custom_keyword!(interfaces); syn::custom_keyword!(with); syn::custom_keyword!(except_imports); syn::custom_keyword!(only_imports); syn::custom_keyword!(trappable_imports); syn::custom_keyword!(additional_derives); syn::custom_keyword!(stringify); syn::custom_keyword!(skip_mut_forwarding_impls); syn::custom_keyword!(features); syn::custom_keyword!(require_store_data_send); syn::custom_keyword!(wasmtime_crate); syn::custom_keyword!(include_generated_code_from_file); } enum Opt { World(syn::LitStr), Path(Vec), Inline(syn::LitStr), Tracing(bool), VerboseTracing(bool), Async(AsyncConfig, Span), TrappableErrorType(Vec), Ownership(Ownership), Interfaces(syn::LitStr), With(HashMap), TrappableImports(TrappableImports), AdditionalDerives(Vec), Stringify(bool), SkipMutForwardingImpls(bool), Features(Vec), RequireStoreDataSend(bool), WasmtimeCrate(syn::Path), IncludeGeneratedCodeFromFile(bool), } impl Parse for Opt { fn parse(input: ParseStream<'_>) -> Result { let l = input.lookahead1(); if l.peek(kw::path) { input.parse::()?; input.parse::()?; let mut paths: Vec = vec![]; let l = input.lookahead1(); if l.peek(syn::LitStr) { paths.push(input.parse()?); } else if l.peek(syn::token::Bracket) { let contents; syn::bracketed!(contents in input); let list = Punctuated::<_, Token![,]>::parse_terminated(&contents)?; paths.extend(list.into_iter()); } else { return Err(l.error()); }; Ok(Opt::Path(paths)) } else if l.peek(kw::inline) { input.parse::()?; input.parse::()?; Ok(Opt::Inline(input.parse()?)) } else if l.peek(kw::world) { input.parse::()?; input.parse::()?; Ok(Opt::World(input.parse()?)) } else if l.peek(kw::tracing) { input.parse::()?; input.parse::()?; Ok(Opt::Tracing(input.parse::()?.value)) } else if l.peek(kw::verbose_tracing) { input.parse::()?; input.parse::()?; Ok(Opt::VerboseTracing(input.parse::()?.value)) } else if l.peek(Token![async]) { let span = input.parse::()?.span; input.parse::()?; if input.peek(syn::LitBool) { match input.parse::()?.value { true => Ok(Opt::Async(AsyncConfig::All, span)), false => Ok(Opt::Async(AsyncConfig::None, span)), } } else { let contents; syn::braced!(contents in input); let l = contents.lookahead1(); let ctor: fn(HashSet) -> AsyncConfig = if l.peek(kw::except_imports) { contents.parse::()?; contents.parse::()?; AsyncConfig::AllExceptImports } else if l.peek(kw::only_imports) { contents.parse::()?; contents.parse::()?; AsyncConfig::OnlyImports } else { return Err(l.error()); }; let list; syn::bracketed!(list in contents); let fields: Punctuated = list.parse_terminated(Parse::parse, Token![,])?; if contents.peek(Token![,]) { contents.parse::()?; } Ok(Opt::Async( ctor(fields.iter().map(|s| s.value()).collect()), span, )) } } else if l.peek(kw::ownership) { input.parse::()?; input.parse::()?; let ownership = input.parse::()?; Ok(Opt::Ownership(match ownership.to_string().as_str() { "Owning" => Ownership::Owning, "Borrowing" => Ownership::Borrowing { duplicate_if_necessary: { let contents; braced!(contents in input); let field = contents.parse::()?; match field.to_string().as_str() { "duplicate_if_necessary" => { contents.parse::()?; contents.parse::()?.value } name => { return Err(Error::new( field.span(), format!( "unrecognized `Ownership::Borrowing` field: `{name}`; \ expected `duplicate_if_necessary`" ), )); } } }, }, name => { return Err(Error::new( ownership.span(), format!( "unrecognized ownership: `{name}`; \ expected `Owning` or `Borrowing`" ), )); } })) } else if l.peek(kw::trappable_error_type) { input.parse::()?; input.parse::()?; let contents; let _lbrace = braced!(contents in input); let fields: Punctuated<_, Token![,]> = contents.parse_terminated(trappable_error_field_parse, Token![,])?; Ok(Opt::TrappableErrorType(Vec::from_iter(fields))) } else if l.peek(kw::interfaces) { input.parse::()?; input.parse::()?; Ok(Opt::Interfaces(input.parse::()?)) } else if l.peek(kw::with) { input.parse::()?; input.parse::()?; let contents; let _lbrace = braced!(contents in input); let fields: Punctuated<(String, String), Token![,]> = contents.parse_terminated(with_field_parse, Token![,])?; Ok(Opt::With(HashMap::from_iter(fields))) } else if l.peek(kw::trappable_imports) { input.parse::()?; input.parse::()?; let config = if input.peek(syn::LitBool) { match input.parse::()?.value { true => TrappableImports::All, false => TrappableImports::None, } } else { let contents; syn::bracketed!(contents in input); let fields: Punctuated = contents.parse_terminated(Parse::parse, Token![,])?; TrappableImports::Only(fields.iter().map(|s| s.value()).collect()) }; Ok(Opt::TrappableImports(config)) } else if l.peek(kw::additional_derives) { input.parse::()?; input.parse::()?; let contents; syn::bracketed!(contents in input); let list = Punctuated::<_, Token![,]>::parse_terminated(&contents)?; Ok(Opt::AdditionalDerives(list.iter().cloned().collect())) } else if l.peek(kw::stringify) { input.parse::()?; input.parse::()?; Ok(Opt::Stringify(input.parse::()?.value)) } else if l.peek(kw::skip_mut_forwarding_impls) { input.parse::()?; input.parse::()?; Ok(Opt::SkipMutForwardingImpls( input.parse::()?.value, )) } else if l.peek(kw::features) { input.parse::()?; input.parse::()?; let contents; syn::bracketed!(contents in input); let list = Punctuated::<_, Token![,]>::parse_terminated(&contents)?; Ok(Opt::Features(list.into_iter().collect())) } else if l.peek(kw::require_store_data_send) { input.parse::()?; input.parse::()?; Ok(Opt::RequireStoreDataSend( input.parse::()?.value, )) } else if l.peek(kw::wasmtime_crate) { input.parse::()?; input.parse::()?; Ok(Opt::WasmtimeCrate(input.parse()?)) } else if l.peek(kw::include_generated_code_from_file) { input.parse::()?; input.parse::()?; Ok(Opt::IncludeGeneratedCodeFromFile( input.parse::()?.value, )) } else { Err(l.error()) } } } fn trappable_error_field_parse(input: ParseStream<'_>) -> Result { let wit_path = input.parse::()?.value(); input.parse::]>()?; let rust_type_name = input.parse::()?.to_token_stream().to_string(); Ok(TrappableError { wit_path, rust_type_name, }) } fn with_field_parse(input: ParseStream<'_>) -> Result<(String, String)> { let interface = input.parse::()?.value(); input.parse::()?; let start = input.span(); let path = input.parse::()?; // It's not possible for the segments of a path to be empty let span = start .join(path.segments.last().unwrap().ident.span()) .unwrap_or(start); let mut buf = String::new(); let append = |buf: &mut String, segment: syn::PathSegment| -> Result<()> { if segment.arguments != syn::PathArguments::None { return Err(Error::new( span, "Module path must not contain angles or parens", )); } buf.push_str(&segment.ident.to_string()); Ok(()) }; if path.leading_colon.is_some() { buf.push_str("::"); } let mut segments = path.segments.into_iter(); if let Some(segment) = segments.next() { append(&mut buf, segment)?; } for segment in segments { buf.push_str("::"); append(&mut buf, segment)?; } Ok((interface, buf)) }