wit_bindgen_guest_rust_macro/
lib.rs

1use proc_macro2::{Span, TokenStream};
2use std::path::{Path, PathBuf};
3use syn::parse::{Error, Parse, ParseStream, Result};
4use syn::punctuated::Punctuated;
5use syn::{token, Token};
6use wit_bindgen_core::wit_parser::{PackageId, Resolve, UnresolvedPackage, WorldId};
7use wit_bindgen_gen_guest_rust::Opts;
8
9#[proc_macro]
10pub fn generate(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
11    syn::parse_macro_input!(input as Config)
12        .expand()
13        .unwrap_or_else(Error::into_compile_error)
14        .into()
15}
16
17struct Config {
18    opts: Opts,
19    resolve: Resolve,
20    world: WorldId,
21    files: Vec<PathBuf>,
22}
23
24enum Source {
25    Path(String),
26    Inline(String),
27}
28
29impl Parse for Config {
30    fn parse(input: ParseStream<'_>) -> Result<Self> {
31        let call_site = Span::call_site();
32        let mut opts = Opts::default();
33        let mut world = None;
34        let mut source = None;
35
36        let document = if input.peek(token::Brace) {
37            let content;
38            syn::braced!(content in input);
39            let fields = Punctuated::<Opt, Token![,]>::parse_terminated(&content)?;
40            let mut document = None;
41            for field in fields.into_pairs() {
42                match field.into_value() {
43                    Opt::Path(s) => {
44                        if source.is_some() {
45                            return Err(Error::new(s.span(), "cannot specify second source"));
46                        }
47                        source = Some(Source::Path(s.value()));
48                    }
49                    Opt::World(s) => {
50                        if document.is_some() {
51                            return Err(Error::new(s.span(), "cannot specify second document"));
52                        }
53                        document = Some(parse_doc(&s.value(), &mut world));
54                    }
55                    Opt::Inline(s) => {
56                        if source.is_some() {
57                            return Err(Error::new(s.span(), "cannot specify second source"));
58                        }
59                        source = Some(Source::Inline(s.value()));
60                    }
61                    Opt::Unchecked => opts.unchecked = true,
62                    Opt::NoStd => opts.no_std = true,
63                    Opt::RawStrings => opts.raw_strings = true,
64                    Opt::MacroExport => opts.macro_export = true,
65                    Opt::MacroCallPrefix(prefix) => opts.macro_call_prefix = Some(prefix.value()),
66                    Opt::ExportMacroName(name) => opts.export_macro_name = Some(name.value()),
67                    Opt::Skip(list) => opts.skip.extend(list.iter().map(|i| i.value())),
68                }
69            }
70            match (document, &source) {
71                (Some(doc), _) => doc,
72                (None, Some(Source::Inline(_))) => "macro-input".to_string(),
73                _ => {
74                    return Err(Error::new(
75                        call_site,
76                        "must specify a `world` to generate bindings for",
77                    ))
78                }
79            }
80        } else {
81            let document = input.parse::<syn::LitStr>()?;
82            if input.parse::<Option<syn::token::In>>()?.is_some() {
83                source = Some(Source::Path(input.parse::<syn::LitStr>()?.value()));
84            }
85            parse_doc(&document.value(), &mut world)
86        };
87        let (resolve, pkg, files) =
88            parse_source(&source).map_err(|err| Error::new(call_site, format!("{err:?}")))?;
89        let doc = resolve.packages[pkg]
90            .documents
91            .get(&document)
92            .copied()
93            .ok_or_else(|| {
94                Error::new(call_site, format!("no document named `{document}` found"))
95            })?;
96
97        let world = match &world {
98            Some(name) => resolve.documents[doc]
99                .worlds
100                .get(name)
101                .copied()
102                .ok_or_else(|| Error::new(call_site, format!("no world named `{name}` found")))?,
103            None => resolve.documents[doc].default_world.ok_or_else(|| {
104                Error::new(call_site, format!("no default world found in `{document}`"))
105            })?,
106        };
107        Ok(Config {
108            opts,
109            resolve,
110            world,
111            files,
112        })
113    }
114}
115
116fn parse_source(source: &Option<Source>) -> anyhow::Result<(Resolve, PackageId, Vec<PathBuf>)> {
117    let mut resolve = Resolve::default();
118    let mut files = Vec::new();
119    let root = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
120    let mut parse = |path: &Path| -> anyhow::Result<_> {
121        if path.is_dir() {
122            let (pkg, sources) = resolve.push_dir(&path)?;
123            files = sources;
124            Ok(pkg)
125        } else {
126            let pkg = UnresolvedPackage::parse_file(path)?;
127            files.extend(pkg.source_files().map(|s| s.to_owned()));
128            resolve.push(pkg, &Default::default())
129        }
130    };
131    let pkg = match source {
132        Some(Source::Inline(s)) => resolve.push(
133            UnresolvedPackage::parse("macro-input".as_ref(), &s)?,
134            &Default::default(),
135        )?,
136        Some(Source::Path(s)) => parse(&root.join(&s))?,
137        None => parse(&root.join("wit"))?,
138    };
139
140    Ok((resolve, pkg, files))
141}
142
143fn parse_doc(s: &str, world: &mut Option<String>) -> String {
144    match s.find('.') {
145        Some(pos) => {
146            *world = Some(s[pos + 1..].to_string());
147            s[..pos].to_string()
148        }
149        None => s.to_string(),
150    }
151}
152
153impl Config {
154    fn expand(self) -> Result<TokenStream> {
155        let mut files = Default::default();
156        self.opts
157            .build()
158            .generate(&self.resolve, self.world, &mut files);
159        let (_, src) = files.iter().next().unwrap();
160        let src = std::str::from_utf8(src).unwrap();
161        let mut contents = src.parse::<TokenStream>().unwrap();
162
163        // Include a dummy `include_str!` for any files we read so rustc knows that
164        // we depend on the contents of those files.
165        for file in self.files.iter() {
166            contents.extend(
167                format!("const _: &str = include_str!(r#\"{}\"#);\n", file.display())
168                    .parse::<TokenStream>()
169                    .unwrap(),
170            );
171        }
172
173        Ok(contents)
174    }
175}
176
177mod kw {
178    syn::custom_keyword!(unchecked);
179    syn::custom_keyword!(no_std);
180    syn::custom_keyword!(raw_strings);
181    syn::custom_keyword!(macro_export);
182    syn::custom_keyword!(macro_call_prefix);
183    syn::custom_keyword!(export_macro_name);
184    syn::custom_keyword!(skip);
185    syn::custom_keyword!(world);
186    syn::custom_keyword!(path);
187    syn::custom_keyword!(inline);
188}
189
190enum Opt {
191    World(syn::LitStr),
192    Path(syn::LitStr),
193    Inline(syn::LitStr),
194    Unchecked,
195    NoStd,
196    RawStrings,
197    MacroExport,
198    MacroCallPrefix(syn::LitStr),
199    ExportMacroName(syn::LitStr),
200    Skip(Vec<syn::LitStr>),
201}
202
203impl Parse for Opt {
204    fn parse(input: ParseStream<'_>) -> Result<Self> {
205        let l = input.lookahead1();
206        if l.peek(kw::path) {
207            input.parse::<kw::path>()?;
208            input.parse::<Token![:]>()?;
209            Ok(Opt::Path(input.parse()?))
210        } else if l.peek(kw::inline) {
211            input.parse::<kw::inline>()?;
212            input.parse::<Token![:]>()?;
213            Ok(Opt::Inline(input.parse()?))
214        } else if l.peek(kw::world) {
215            input.parse::<kw::world>()?;
216            input.parse::<Token![:]>()?;
217            Ok(Opt::World(input.parse()?))
218        } else if l.peek(kw::unchecked) {
219            input.parse::<kw::unchecked>()?;
220            Ok(Opt::Unchecked)
221        } else if l.peek(kw::no_std) {
222            input.parse::<kw::no_std>()?;
223            Ok(Opt::NoStd)
224        } else if l.peek(kw::raw_strings) {
225            input.parse::<kw::raw_strings>()?;
226            Ok(Opt::RawStrings)
227        } else if l.peek(kw::macro_export) {
228            input.parse::<kw::macro_export>()?;
229            Ok(Opt::MacroExport)
230        } else if l.peek(kw::macro_call_prefix) {
231            input.parse::<kw::macro_call_prefix>()?;
232            input.parse::<Token![:]>()?;
233            Ok(Opt::MacroCallPrefix(input.parse()?))
234        } else if l.peek(kw::export_macro_name) {
235            input.parse::<kw::export_macro_name>()?;
236            input.parse::<Token![:]>()?;
237            Ok(Opt::ExportMacroName(input.parse()?))
238        } else if l.peek(kw::skip) {
239            input.parse::<kw::skip>()?;
240            input.parse::<Token![:]>()?;
241            let contents;
242            syn::bracketed!(contents in input);
243            let list = Punctuated::<_, Token![,]>::parse_terminated(&contents)?;
244            Ok(Opt::Skip(list.iter().cloned().collect()))
245        } else {
246            Err(l.error())
247        }
248    }
249}