Skip to main content

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::{Data, DeriveInput, Error, parse_macro_input};
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
33The macro is authored by [**@AlexKorn**](https://github.com/AlexKorn)
34based on the idea of [**@aweinstock314**](https://github.com/aweinstock314).
35See [this](https://github.com/rustwasm/wasm-bindgen/issues/2231#issuecomment-656293288)
36and [this](https://github.com/rustwasm/wasm-bindgen/issues/2231#issuecomment-1169658111)
37GitHub comments.
38*/
39#[proc_macro_derive(TryFromJsValue)]
40pub fn derive_try_from_jsvalue(input: TokenStream) -> TokenStream {
41    let input: DeriveInput = parse_macro_input!(input as DeriveInput);
42
43    let name = input.ident;
44    let data = input.data;
45
46    match data {
47        Data::Struct(_) => {}
48        _ => return derive_error!("TryFromJsValue may only be derived on structs"),
49    };
50
51    // Find the first occurrence of `#[wasm_bindgen]` or `#[wasm_bindgen(.. = ..)]
52    let wasm_bindgen_attr = input.attrs.iter().find(|attr| match &attr.meta {
53        syn::Meta::Path(path) => path.is_ident("wasm_bindgen"),
54        syn::Meta::List(list) => list.path.is_ident("wasm_bindgen"),
55        syn::Meta::NameValue(_) => false,
56    });
57
58    let Some(wasm_bindgen_attr) = wasm_bindgen_attr else {
59        return derive_error!(
60            "TryFromJsValue can be defined only on struct exported to wasm with #[wasm_bindgen]"
61        );
62    };
63
64    let maybe_js_class = if let syn::Meta::List(list) = &wasm_bindgen_attr.meta {
65        let mut js_name = None;
66        if let Err(err) = list.parse_nested_meta(|meta| {
67            if meta.path.is_ident("js_name") {
68                let value = meta.value()?;
69                let s: syn::LitStr = value.parse()?;
70                js_name = Some(s.value());
71            }
72            Ok(())
73        }) {
74            return err.into_compile_error().into();
75        }
76        js_name
77    } else {
78        None
79    };
80
81    let wasm_bindgen_macro_invocaton = match maybe_js_class {
82        Some(class) => format!(
83            "::wasm_bindgen::prelude::wasm_bindgen(js_class = \"{}\")",
84            class
85        ),
86        None => "::wasm_bindgen::prelude::wasm_bindgen".to_string(),
87    }
88    .parse::<TokenStream2>()
89    .unwrap();
90
91    // Note that we use `::wasm_bindgen_derive` here,
92    // because this crate will only ever be imported via it.
93    let expanded = quote! {
94        impl #name {
95            pub fn __get_classname() -> &'static str {
96                ::core::stringify!(#name)
97            }
98        }
99
100        #[#wasm_bindgen_macro_invocaton]
101        impl #name {
102            #[::wasm_bindgen::prelude::wasm_bindgen(js_name = "__getClassname")]
103            pub fn __js_get_classname(&self) -> ::wasm_bindgen_derive::alloc::string::String {
104                use ::wasm_bindgen_derive::alloc::borrow::ToOwned;
105                ::core::stringify!(#name).to_owned()
106            }
107        }
108
109        impl ::core::convert::TryFrom<&::wasm_bindgen::JsValue> for #name {
110            type Error = ::wasm_bindgen_derive::alloc::string::String;
111
112            fn try_from(js: &::wasm_bindgen::JsValue) -> Result<Self, Self::Error> {
113                use ::wasm_bindgen_derive::alloc::{borrow::ToOwned, string::{String, ToString}, format};
114                use ::wasm_bindgen::JsCast;
115                use ::wasm_bindgen::convert::RefFromWasmAbi;
116
117                let classname = Self::__get_classname();
118
119                if !js.is_object() {
120                    return Err(format!("Value supplied as {} is not an object", classname));
121                }
122
123                let no_get_classname_msg = concat!(
124                    "no __getClassname method specified for object; ",
125                    "did you forget to derive TryFromJsObject for this type?");
126
127                let get_classname = ::js_sys::Reflect::get(
128                    js,
129                    &::wasm_bindgen::JsValue::from("__getClassname"),
130                )
131                .or(Err(no_get_classname_msg.to_string()))?;
132
133                if get_classname.is_undefined() {
134                    return Err(no_get_classname_msg.to_string());
135                }
136
137                let get_classname = get_classname
138                    .dyn_into::<::js_sys::Function>()
139                    .map_err(|err| format!("__getClassname is not a function, {:?}", err))?;
140
141                let object_classname: String = ::js_sys::Reflect::apply(
142                        &get_classname,
143                        js,
144                        &::js_sys::Array::new(),
145                    )
146                    .ok()
147                    .and_then(|v| v.as_string())
148                    .ok_or_else(|| "Failed to get classname".to_owned())?;
149
150                if object_classname.as_str() == classname {
151                    // Note: using an undocumented implementation detail of `wasm-bindgen`:
152                    // the pointer property has the name `__wbg_ptr` (since wasm-bindgen 0.2.85)
153                    let ptr = ::js_sys::Reflect::get(js, &::wasm_bindgen::JsValue::from_str("__wbg_ptr"))
154                        .map_err(|err| format!("{:?}", err))?;
155                    // All numbers in JS are float64.
156                    let ptr_u32: u32 = ptr.as_f64().ok_or(::wasm_bindgen::JsValue::NULL)
157                        .map_err(|err| format!("{:?}", err))?
158                        as u32;
159                    let ptr_abi: ::wasm_bindgen::__rt::WasmPtr<::wasm_bindgen::__rt::WasmRefCell<#name>> =
160                        ::wasm_bindgen::__rt::WasmPtr::from_usize(ptr_u32 as usize);
161                    let instance_ref = unsafe { #name::ref_from_abi(ptr_abi) };
162                    Ok(instance_ref.clone())
163                } else {
164                    Err(format!("Cannot convert {} to {}", object_classname, classname))
165                }
166            }
167        }
168    };
169
170    TokenStream::from(expanded)
171}