From 074c82dd3398d24574bba6f08b12317224e2f49f Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sat, 3 Jun 2023 14:05:31 -0700 Subject: [PATCH] Make a "trampoline" crate to avoid unneeded dependencies in a proc-macro crate --- Cargo.toml | 1 + wasm-bindgen-derive-macro/Cargo.toml | 16 +++ wasm-bindgen-derive-macro/src/lib.rs | 177 +++++++++++++++++++++++++++ wasm-bindgen-derive/Cargo.toml | 7 +- wasm-bindgen-derive/src/lib.rs | 171 +------------------------- 5 files changed, 196 insertions(+), 176 deletions(-) create mode 100644 wasm-bindgen-derive-macro/Cargo.toml create mode 100644 wasm-bindgen-derive-macro/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 70e74ec..9e72800 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members=[ + 'wasm-bindgen-derive-macro', 'wasm-bindgen-derive', 'tests' ] diff --git a/wasm-bindgen-derive-macro/Cargo.toml b/wasm-bindgen-derive-macro/Cargo.toml new file mode 100644 index 0000000..aba9547 --- /dev/null +++ b/wasm-bindgen-derive-macro/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "wasm-bindgen-derive-macro" +version = "0.0.0" +edition = "2021" +authors = ["Bogdan Opanchuk "] +license = "MIT" +description = "Proc-macro backend for wasm-bindgen-derive" +rust-version = "1.65.0" + +[lib] +proc-macro = true + +[dependencies] +syn = "1.0" +quote = "1.0" +proc-macro2 = "1.0" diff --git a/wasm-bindgen-derive-macro/src/lib.rs b/wasm-bindgen-derive-macro/src/lib.rs new file mode 100644 index 0000000..213fb09 --- /dev/null +++ b/wasm-bindgen-derive-macro/src/lib.rs @@ -0,0 +1,177 @@ +//! A proc-macro to be re-exported by `wasm-bindgen-derive`. +//! We need this trampoline to enforce the correct bounds on the `wasm-bindgen` and `js-sys` +//! dependencies, but those are technically not the dependencies of this crate, +//! but only of the code it generates. + +#![warn(missing_docs, rust_2018_idioms, unused_qualifications)] +#![no_std] + +extern crate alloc; + +use alloc::format; +use alloc::string::ToString; + +use proc_macro::TokenStream; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::quote; +use syn::{parse_macro_input, Data, DeriveInput, Error}; + +macro_rules! derive_error { + ($string: tt) => { + Error::new(Span::call_site(), $string) + .to_compile_error() + .into() + }; +} + +/** Derives a `TryFrom<&JsValue>` for a type exported using `#[wasm_bindgen]`. + +Note that: +* this derivation must be be positioned before `#[wasm_bindgen]`; +* the type must implement [`Clone`]. +* `extern crate alloc` must be declared in scope. + +The macro is authored by [**@AlexKorn**](https://github.com/AlexKorn) +based on the idea of [**@aweinstock314**](https://github.com/aweinstock314). +See [this](https://github.com/rustwasm/wasm-bindgen/issues/2231#issuecomment-656293288) +and [this](https://github.com/rustwasm/wasm-bindgen/issues/2231#issuecomment-1169658111) +GitHub comments. +*/ +#[proc_macro_derive(TryFromJsValue)] +pub fn derive_try_from_jsvalue(input: TokenStream) -> TokenStream { + let input: DeriveInput = parse_macro_input!(input as DeriveInput); + + let name = input.ident; + let data = input.data; + + match data { + Data::Struct(_) => {} + _ => return derive_error!("TryFromJsValue may only be derived on structs"), + }; + + let wasm_bindgen_meta = input.attrs.iter().find_map(|attr| { + attr.parse_meta() + .ok() + .and_then(|meta| match meta.path().is_ident("wasm_bindgen") { + true => Some(meta), + false => None, + }) + }); + if wasm_bindgen_meta.is_none() { + return derive_error!( + "TryFromJsValue can be defined only on struct exported to wasm with #[wasm_bindgen]" + ); + } + + let maybe_js_class = wasm_bindgen_meta + .and_then(|meta| match meta { + syn::Meta::List(list) => Some(list), + _ => None, + }) + .and_then(|meta_list| { + meta_list.nested.iter().find_map(|nested_meta| { + let maybe_meta = match nested_meta { + syn::NestedMeta::Meta(meta) => Some(meta), + _ => None, + }; + + maybe_meta + .and_then(|meta| match meta { + syn::Meta::NameValue(name_value) => Some(name_value), + _ => None, + }) + .and_then(|name_value| match name_value.path.is_ident("js_name") { + true => Some(name_value.lit.clone()), + false => None, + }) + .and_then(|lit| match lit { + syn::Lit::Str(str) => Some(str.value()), + _ => None, + }) + }) + }); + + let wasm_bindgen_macro_invocaton = match maybe_js_class { + Some(class) => format!("wasm_bindgen(js_class = \"{}\")", class), + None => "wasm_bindgen".to_string(), + } + .parse::() + .unwrap(); + + let expanded = quote! { + impl #name { + pub fn __get_classname() -> &'static str { + ::core::stringify!(#name) + } + } + + #[#wasm_bindgen_macro_invocaton] + impl #name { + #[wasm_bindgen(js_name = "__getClassname")] + pub fn __js_get_classname(&self) -> String { + use ::alloc::borrow::ToOwned; + ::core::stringify!(#name).to_owned() + } + } + + impl ::core::convert::TryFrom<&::wasm_bindgen::JsValue> for #name { + type Error = String; + + fn try_from(js: &::wasm_bindgen::JsValue) -> Result { + use ::alloc::borrow::ToOwned; + use ::alloc::string::ToString; + use ::wasm_bindgen::JsCast; + use ::wasm_bindgen::convert::RefFromWasmAbi; + + let classname = Self::__get_classname(); + + if !js.is_object() { + return Err(format!("Value supplied as {} is not an object", classname)); + } + + let no_get_classname_msg = concat!( + "no __getClassname method specified for object; ", + "did you forget to derive TryFromJsObject for this type?"); + + let get_classname = ::js_sys::Reflect::get( + js, + &::wasm_bindgen::JsValue::from("__getClassname"), + ) + .or(Err(no_get_classname_msg.to_string()))?; + + if get_classname.is_undefined() { + return Err(no_get_classname_msg.to_string()); + } + + let get_classname = get_classname + .dyn_into::<::js_sys::Function>() + .map_err(|err| format!("__getClassname is not a function, {:?}", err))?; + + let object_classname: String = ::js_sys::Reflect::apply( + &get_classname, + js, + &::js_sys::Array::new(), + ) + .ok() + .and_then(|v| v.as_string()) + .ok_or_else(|| "Failed to get classname".to_owned())?; + + if object_classname.as_str() == classname { + // Note: using an undocumented implementation detail of `wasm-bindgen`: + // the pointer property has the name `__wbg_ptr` (since wasm-bindgen 0.2.85) + let ptr = ::js_sys::Reflect::get(js, &::wasm_bindgen::JsValue::from_str("__wbg_ptr")) + .map_err(|err| format!("{:?}", err))?; + let ptr_u32: u32 = ptr.as_f64().ok_or(::wasm_bindgen::JsValue::NULL) + .map_err(|err| format!("{:?}", err))? + as u32; + let instance_ref = unsafe { #name::ref_from_abi(ptr_u32) }; + Ok(instance_ref.clone()) + } else { + Err(format!("Cannot convert {} to {}", object_classname, classname)) + } + } + } + }; + + TokenStream::from(expanded) +} diff --git a/wasm-bindgen-derive/Cargo.toml b/wasm-bindgen-derive/Cargo.toml index c7830ff..d671222 100644 --- a/wasm-bindgen-derive/Cargo.toml +++ b/wasm-bindgen-derive/Cargo.toml @@ -10,12 +10,7 @@ readme = "../README.md" categories = ["no-std", "wasm"] rust-version = "1.65.0" -[lib] -proc-macro = true - [dependencies] -syn = "1.0" -quote = "1.0" -proc-macro2 = "1.0" +wasm-bindgen-derive-macro = { path = "../wasm-bindgen-derive-macro" } wasm-bindgen = "0.2.85" js-sys = "0.3.55" diff --git a/wasm-bindgen-derive/src/lib.rs b/wasm-bindgen-derive/src/lib.rs index 6e6b4fd..a189917 100644 --- a/wasm-bindgen-derive/src/lib.rs +++ b/wasm-bindgen-derive/src/lib.rs @@ -111,175 +111,6 @@ pub fn vec_example(val: &MyTypeArray) -> Result { ``` */ #![doc(html_root_url = "https://docs.rs/wasm-bindgen-derive")] -#![warn(missing_docs, rust_2018_idioms, unused_qualifications)] #![no_std] -extern crate alloc; - -use alloc::format; -use alloc::string::ToString; - -use proc_macro::TokenStream; -use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::quote; -use syn::{parse_macro_input, Data, DeriveInput, Error}; - -macro_rules! derive_error { - ($string: tt) => { - Error::new(Span::call_site(), $string) - .to_compile_error() - .into() - }; -} - -/** Derives a `TryFrom<&JsValue>` for a type exported using `#[wasm_bindgen]`. - -Note that: -* this derivation must be be positioned before `#[wasm_bindgen]`; -* the type must implement [`Clone`]. -* `extern crate alloc` must be declared in scope. - -The macro is authored by [**@AlexKorn**](https://github.com/AlexKorn) -based on the idea of [**@aweinstock314**](https://github.com/aweinstock314). -See [this](https://github.com/rustwasm/wasm-bindgen/issues/2231#issuecomment-656293288) -and [this](https://github.com/rustwasm/wasm-bindgen/issues/2231#issuecomment-1169658111) -GitHub comments. -*/ -#[proc_macro_derive(TryFromJsValue)] -pub fn derive_try_from_jsvalue(input: TokenStream) -> TokenStream { - let input: DeriveInput = parse_macro_input!(input as DeriveInput); - - let name = input.ident; - let data = input.data; - - match data { - Data::Struct(_) => {} - _ => return derive_error!("TryFromJsValue may only be derived on structs"), - }; - - let wasm_bindgen_meta = input.attrs.iter().find_map(|attr| { - attr.parse_meta() - .ok() - .and_then(|meta| match meta.path().is_ident("wasm_bindgen") { - true => Some(meta), - false => None, - }) - }); - if wasm_bindgen_meta.is_none() { - return derive_error!( - "TryFromJsValue can be defined only on struct exported to wasm with #[wasm_bindgen]" - ); - } - - let maybe_js_class = wasm_bindgen_meta - .and_then(|meta| match meta { - syn::Meta::List(list) => Some(list), - _ => None, - }) - .and_then(|meta_list| { - meta_list.nested.iter().find_map(|nested_meta| { - let maybe_meta = match nested_meta { - syn::NestedMeta::Meta(meta) => Some(meta), - _ => None, - }; - - maybe_meta - .and_then(|meta| match meta { - syn::Meta::NameValue(name_value) => Some(name_value), - _ => None, - }) - .and_then(|name_value| match name_value.path.is_ident("js_name") { - true => Some(name_value.lit.clone()), - false => None, - }) - .and_then(|lit| match lit { - syn::Lit::Str(str) => Some(str.value()), - _ => None, - }) - }) - }); - - let wasm_bindgen_macro_invocaton = match maybe_js_class { - Some(class) => format!("wasm_bindgen(js_class = \"{}\")", class), - None => "wasm_bindgen".to_string(), - } - .parse::() - .unwrap(); - - let expanded = quote! { - impl #name { - pub fn __get_classname() -> &'static str { - ::core::stringify!(#name) - } - } - - #[#wasm_bindgen_macro_invocaton] - impl #name { - #[wasm_bindgen(js_name = "__getClassname")] - pub fn __js_get_classname(&self) -> String { - use ::alloc::borrow::ToOwned; - ::core::stringify!(#name).to_owned() - } - } - - impl ::core::convert::TryFrom<&::wasm_bindgen::JsValue> for #name { - type Error = String; - - fn try_from(js: &::wasm_bindgen::JsValue) -> Result { - use ::alloc::borrow::ToOwned; - use ::alloc::string::ToString; - use ::wasm_bindgen::JsCast; - use ::wasm_bindgen::convert::RefFromWasmAbi; - - let classname = Self::__get_classname(); - - if !js.is_object() { - return Err(format!("Value supplied as {} is not an object", classname)); - } - - let no_get_classname_msg = concat!( - "no __getClassname method specified for object; ", - "did you forget to derive TryFromJsObject for this type?"); - - let get_classname = ::js_sys::Reflect::get( - js, - &::wasm_bindgen::JsValue::from("__getClassname"), - ) - .or(Err(no_get_classname_msg.to_string()))?; - - if get_classname.is_undefined() { - return Err(no_get_classname_msg.to_string()); - } - - let get_classname = get_classname - .dyn_into::<::js_sys::Function>() - .map_err(|err| format!("__getClassname is not a function, {:?}", err))?; - - let object_classname: String = ::js_sys::Reflect::apply( - &get_classname, - js, - &::js_sys::Array::new(), - ) - .ok() - .and_then(|v| v.as_string()) - .ok_or_else(|| "Failed to get classname".to_owned())?; - - if object_classname.as_str() == classname { - // Note: using an undocumented implementation detail of `wasm-bindgen`: - // the pointer property has the name `__wbg_ptr` (since wasm-bindgen 0.2.85) - let ptr = ::js_sys::Reflect::get(js, &::wasm_bindgen::JsValue::from_str("__wbg_ptr")) - .map_err(|err| format!("{:?}", err))?; - let ptr_u32: u32 = ptr.as_f64().ok_or(::wasm_bindgen::JsValue::NULL) - .map_err(|err| format!("{:?}", err))? - as u32; - let instance_ref = unsafe { #name::ref_from_abi(ptr_u32) }; - Ok(instance_ref.clone()) - } else { - Err(format!("Cannot convert {} to {}", object_classname, classname)) - } - } - } - }; - - TokenStream::from(expanded) -} +pub use wasm_bindgen_derive_macro::TryFromJsValue;