wasm_bindgen_derive_macro/
lib.rs

1//! A proc-macro to be re-exported by `wasm-bindgen-derive`.
2//! We need this trampoline to enforce the correct bounds on the `wasm-bindgen` and `js-sys`
3//! dependencies, but those are technically not the dependencies of this crate,
4//! but only of the code it generates.
5
6#![warn(missing_docs, rust_2018_idioms, unused_qualifications)]
7#![no_std]
8
9extern crate alloc;
10
11use alloc::format;
12use alloc::string::ToString;
13
14use proc_macro::TokenStream;
15use proc_macro2::{Span, TokenStream as TokenStream2};
16use quote::quote;
17use syn::{parse_macro_input, Data, DeriveInput, Error};
18
19macro_rules! derive_error {
20    ($string: tt) => {
21        Error::new(Span::call_site(), $string)
22            .to_compile_error()
23            .into()
24    };
25}
26
27/** Derives a `TryFrom<&JsValue>` for a type exported using `#[wasm_bindgen]`.
28
29Note that:
30* this derivation must be be positioned before `#[wasm_bindgen]`;
31* the type must implement [`Clone`].
32* `extern crate alloc` must be declared in scope.
33
34The macro is authored by [**@AlexKorn**](https://github.com/AlexKorn)
35based on the idea of [**@aweinstock314**](https://github.com/aweinstock314).
36See [this](https://github.com/rustwasm/wasm-bindgen/issues/2231#issuecomment-656293288)
37and [this](https://github.com/rustwasm/wasm-bindgen/issues/2231#issuecomment-1169658111)
38GitHub comments.
39*/
40#[proc_macro_derive(TryFromJsValue)]
41pub fn derive_try_from_jsvalue(input: TokenStream) -> TokenStream {
42    let input: DeriveInput = parse_macro_input!(input as DeriveInput);
43
44    let name = input.ident;
45    let data = input.data;
46
47    match data {
48        Data::Struct(_) => {}
49        _ => return derive_error!("TryFromJsValue may only be derived on structs"),
50    };
51
52    let wasm_bindgen_meta = input.attrs.iter().find_map(|attr| {
53        attr.parse_meta()
54            .ok()
55            .and_then(|meta| match meta.path().is_ident("wasm_bindgen") {
56                true => Some(meta),
57                false => None,
58            })
59    });
60    if wasm_bindgen_meta.is_none() {
61        return derive_error!(
62            "TryFromJsValue can be defined only on struct exported to wasm with #[wasm_bindgen]"
63        );
64    }
65
66    let maybe_js_class = wasm_bindgen_meta
67        .and_then(|meta| match meta {
68            syn::Meta::List(list) => Some(list),
69            _ => None,
70        })
71        .and_then(|meta_list| {
72            meta_list.nested.iter().find_map(|nested_meta| {
73                let maybe_meta = match nested_meta {
74                    syn::NestedMeta::Meta(meta) => Some(meta),
75                    _ => None,
76                };
77
78                maybe_meta
79                    .and_then(|meta| match meta {
80                        syn::Meta::NameValue(name_value) => Some(name_value),
81                        _ => None,
82                    })
83                    .and_then(|name_value| match name_value.path.is_ident("js_name") {
84                        true => Some(name_value.lit.clone()),
85                        false => None,
86                    })
87                    .and_then(|lit| match lit {
88                        syn::Lit::Str(str) => Some(str.value()),
89                        _ => None,
90                    })
91            })
92        });
93
94    let wasm_bindgen_macro_invocaton = match maybe_js_class {
95        Some(class) => format!(
96            "::wasm_bindgen::prelude::wasm_bindgen(js_class = \"{}\")",
97            class
98        ),
99        None => "::wasm_bindgen::prelude::wasm_bindgen".to_string(),
100    }
101    .parse::<TokenStream2>()
102    .unwrap();
103
104    let expanded = quote! {
105        impl #name {
106            pub fn __get_classname() -> &'static str {
107                ::core::stringify!(#name)
108            }
109        }
110
111        #[#wasm_bindgen_macro_invocaton]
112        impl #name {
113            #[::wasm_bindgen::prelude::wasm_bindgen(js_name = "__getClassname")]
114            pub fn __js_get_classname(&self) -> String {
115                use ::alloc::borrow::ToOwned;
116                ::core::stringify!(#name).to_owned()
117            }
118        }
119
120        impl ::core::convert::TryFrom<&::wasm_bindgen::JsValue> for #name {
121            type Error = String;
122
123            fn try_from(js: &::wasm_bindgen::JsValue) -> Result<Self, Self::Error> {
124                use ::alloc::borrow::ToOwned;
125                use ::alloc::string::ToString;
126                use ::wasm_bindgen::JsCast;
127                use ::wasm_bindgen::convert::RefFromWasmAbi;
128
129                let classname = Self::__get_classname();
130
131                if !js.is_object() {
132                    return Err(format!("Value supplied as {} is not an object", classname));
133                }
134
135                let no_get_classname_msg = concat!(
136                    "no __getClassname method specified for object; ",
137                    "did you forget to derive TryFromJsObject for this type?");
138
139                let get_classname = ::js_sys::Reflect::get(
140                    js,
141                    &::wasm_bindgen::JsValue::from("__getClassname"),
142                )
143                .or(Err(no_get_classname_msg.to_string()))?;
144
145                if get_classname.is_undefined() {
146                    return Err(no_get_classname_msg.to_string());
147                }
148
149                let get_classname = get_classname
150                    .dyn_into::<::js_sys::Function>()
151                    .map_err(|err| format!("__getClassname is not a function, {:?}", err))?;
152
153                let object_classname: String = ::js_sys::Reflect::apply(
154                        &get_classname,
155                        js,
156                        &::js_sys::Array::new(),
157                    )
158                    .ok()
159                    .and_then(|v| v.as_string())
160                    .ok_or_else(|| "Failed to get classname".to_owned())?;
161
162                if object_classname.as_str() == classname {
163                    // Note: using an undocumented implementation detail of `wasm-bindgen`:
164                    // the pointer property has the name `__wbg_ptr` (since wasm-bindgen 0.2.85)
165                    let ptr = ::js_sys::Reflect::get(js, &::wasm_bindgen::JsValue::from_str("__wbg_ptr"))
166                        .map_err(|err| format!("{:?}", err))?;
167                    let ptr_u32: u32 = ptr.as_f64().ok_or(::wasm_bindgen::JsValue::NULL)
168                        .map_err(|err| format!("{:?}", err))?
169                        as u32;
170                    let instance_ref = unsafe { #name::ref_from_abi(ptr_u32) };
171                    Ok(instance_ref.clone())
172                } else {
173                    Err(format!("Cannot convert {} to {}", object_classname, classname))
174                }
175            }
176        }
177    };
178
179    TokenStream::from(expanded)
180}