Skip to main content

bitwarden_ffi_macro/
wasm_import.rs

1use proc_macro2::TokenStream;
2use quote::{ToTokens, quote};
3use syn::{ForeignItem, ItemForeignMod, parse2};
4
5use crate::attrs;
6
7/// Declares a block of JavaScript imports, forwarding this macro's arguments to `#[wasm_bindgen]`,
8/// and implements the wire traits for each `type` it declares so the type crosses as itself.
9pub(crate) fn wasm_import(attr: TokenStream, item: TokenStream) -> TokenStream {
10    let forwarded = match attrs::parse_args(attr) {
11        Ok(args) => args,
12        Err(err) => return err.to_compile_error(),
13    };
14
15    let block = match parse2::<ItemForeignMod>(item.clone()) {
16        Ok(block) => block,
17        Err(_) => {
18            return syn::Error::new_spanned(
19                item,
20                "#[wasm_import] applies to an `extern \"C\"` block",
21            )
22            .to_compile_error();
23        }
24    };
25
26    if let Some(attr) = attrs::find_wasm_bindgen(&block.attrs) {
27        return syn::Error::new_spanned(attr, "#[wasm_import] replaces #[wasm_bindgen]; remove it")
28            .to_compile_error();
29    }
30
31    // The block's own `cfg`s gate the generated impls too, or they would name a type that does not
32    // exist.
33    let cfgs: Vec<_> = block
34        .attrs
35        .iter()
36        .filter(|attr| attr.path().is_ident("cfg"))
37        .collect();
38    let wire_impls = block.items.iter().filter_map(|item| match item {
39        ForeignItem::Type(ty) => {
40            let ident = &ty.ident;
41            Some(quote! {
42                #(#cfgs)*
43                ::bitwarden_ffi::impl_wire_object!(#ident);
44            })
45        }
46        _ => None,
47    });
48
49    let call = if forwarded.is_empty() {
50        quote!(wasm_bindgen)
51    } else {
52        quote!(wasm_bindgen(#forwarded))
53    };
54    let body = block.to_token_stream();
55    // Unconditional, unlike the other macros: a `type` in an extern block only compiles once
56    // wasm_bindgen rewrites it, and some blocks are gated on `target_arch` rather than `wasm`.
57    quote! {
58        #[::wasm_bindgen::prelude::#call]
59        #body
60
61        #(#wire_impls)*
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    fn expand(attr: TokenStream, item: TokenStream) -> String {
70        wasm_import(attr, item).to_string().replace(' ', "")
71    }
72
73    #[test]
74    fn applies_wasm_bindgen_and_implements_the_wire_traits_per_type() {
75        let out = expand(
76            TokenStream::new(),
77            quote! {
78                extern "C" {
79                    pub type Driver;
80                    pub type Signal;
81
82                    #[wasm_bindgen(method)]
83                    pub fn lock(this: &Driver);
84                }
85            },
86        );
87
88        assert!(!out.contains("compile_error!"), "{out}");
89        assert!(
90            out.contains("#[::wasm_bindgen::prelude::wasm_bindgen]"),
91            "{out}"
92        );
93        assert!(
94            out.contains("::bitwarden_ffi::impl_wire_object!(Driver);"),
95            "{out}"
96        );
97        assert!(
98            out.contains("::bitwarden_ffi::impl_wire_object!(Signal);"),
99            "{out}"
100        );
101        assert!(!out.contains("impl_wire_object!(lock)"), "{out}");
102    }
103
104    #[test]
105    fn forwards_arguments_to_wasm_bindgen() {
106        let out = expand(
107            quote!(module = "/js/driver.js"),
108            quote! {
109                extern "C" {
110                    pub type Driver;
111                }
112            },
113        );
114
115        assert!(
116            out.contains("#[::wasm_bindgen::prelude::wasm_bindgen(module=\"/js/driver.js\")]"),
117            "{out}"
118        );
119    }
120
121    #[test]
122    fn gates_the_wire_impls_on_the_blocks_cfg() {
123        let out = expand(
124            TokenStream::new(),
125            quote! {
126                #[cfg(target_arch = "wasm32")]
127                extern "C" {
128                    pub type Driver;
129                }
130            },
131        );
132
133        assert!(
134            out.contains(
135                "#[cfg(target_arch=\"wasm32\")]::bitwarden_ffi::impl_wire_object!(Driver);"
136            ),
137            "{out}"
138        );
139    }
140
141    #[test]
142    fn a_block_without_types_implements_nothing() {
143        let out = expand(
144            TokenStream::new(),
145            quote! {
146                extern "C" {
147                    pub fn log(message: &str);
148                }
149            },
150        );
151
152        assert!(!out.contains("impl_wire_object"), "{out}");
153    }
154
155    #[test]
156    fn rejects_an_item_that_is_not_an_extern_block() {
157        let out = expand(TokenStream::new(), quote! { pub struct Driver; });
158
159        assert!(out.contains("compile_error!"), "{out}");
160        assert!(out.contains("appliestoan`extern\\\"C\\\"`block"), "{out}");
161    }
162
163    #[test]
164    fn rejects_a_block_that_already_has_wasm_bindgen() {
165        let out = expand(
166            TokenStream::new(),
167            quote! {
168                #[wasm_bindgen]
169                extern "C" {
170                    pub type Driver;
171                }
172            },
173        );
174
175        assert!(out.contains("compile_error!"), "{out}");
176        assert!(out.contains("replaces#[wasm_bindgen]"), "{out}");
177    }
178}