From 9291655d486387814b7485fee91202a1499fc850 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sat, 3 Jun 2023 14:05:31 -0700 Subject: [PATCH 1/4] Make a "trampoline" crate to avoid unneeded dependencies in a proc-macro crate --- Cargo.toml | 1 + wasm-bindgen-derive-macro/Cargo.toml | 18 +++ wasm-bindgen-derive-macro/src/lib.rs | 177 +++++++++++++++++++++++++++ wasm-bindgen-derive/Cargo.toml | 7 +- wasm-bindgen-derive/src/lib.rs | 171 +------------------------- 5 files changed, 198 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..416e829 --- /dev/null +++ b/wasm-bindgen-derive-macro/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "wasm-bindgen-derive-macro" +version = "0.2.0" +edition = "2021" +authors = ["Bogdan Opanchuk "] +license = "MIT" +description = "Proc-macro backend for wasm-bindgen-derive" +repository = "https://github.com/fjarri/wasm-bindgen-derive" +categories = ["no-std", "wasm"] +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..a0553d2 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 = "0.2" 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; From 462c47c2d35671e874a0626b8cb49e8b9289ea3c Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sat, 3 Jun 2023 14:28:45 -0700 Subject: [PATCH 2/4] Use absolute paths to the wasm-bindgen macro, just in case --- wasm-bindgen-derive-macro/src/lib.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/wasm-bindgen-derive-macro/src/lib.rs b/wasm-bindgen-derive-macro/src/lib.rs index 213fb09..a56ae15 100644 --- a/wasm-bindgen-derive-macro/src/lib.rs +++ b/wasm-bindgen-derive-macro/src/lib.rs @@ -92,8 +92,11 @@ pub fn derive_try_from_jsvalue(input: TokenStream) -> TokenStream { }); let wasm_bindgen_macro_invocaton = match maybe_js_class { - Some(class) => format!("wasm_bindgen(js_class = \"{}\")", class), - None => "wasm_bindgen".to_string(), + Some(class) => format!( + "::wasm_bindgen::prelude::wasm_bindgen(js_class = \"{}\")", + class + ), + None => "::wasm_bindgen::prelude::wasm_bindgen".to_string(), } .parse::() .unwrap(); @@ -107,7 +110,7 @@ pub fn derive_try_from_jsvalue(input: TokenStream) -> TokenStream { #[#wasm_bindgen_macro_invocaton] impl #name { - #[wasm_bindgen(js_name = "__getClassname")] + #[::wasm_bindgen::prelude::wasm_bindgen(js_name = "__getClassname")] pub fn __js_get_classname(&self) -> String { use ::alloc::borrow::ToOwned; ::core::stringify!(#name).to_owned() From 5409a5bf276a66a3db42f417e94708e3b9c85adb Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sat, 3 Jun 2023 14:31:41 -0700 Subject: [PATCH 3/4] Check early that `alloc` is present (needed for the generated code) --- wasm-bindgen-derive/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wasm-bindgen-derive/src/lib.rs b/wasm-bindgen-derive/src/lib.rs index a189917..b3ed3b1 100644 --- a/wasm-bindgen-derive/src/lib.rs +++ b/wasm-bindgen-derive/src/lib.rs @@ -113,4 +113,7 @@ pub fn vec_example(val: &MyTypeArray) -> Result { #![doc(html_root_url = "https://docs.rs/wasm-bindgen-derive")] #![no_std] +// Ensure it is present. Needed for the generated code to work. +extern crate alloc; + pub use wasm_bindgen_derive_macro::TryFromJsValue; From 3186af97a11833d052ae45c7b8a12791b3aaf368 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sat, 3 Jun 2023 14:59:40 -0700 Subject: [PATCH 4/4] Add some smoke tests for wasm-bindgen-derive-macro, to get some coverage --- wasm-bindgen-derive-macro/Cargo.toml | 4 ++++ .../tests/integration.rs | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 wasm-bindgen-derive-macro/tests/integration.rs diff --git a/wasm-bindgen-derive-macro/Cargo.toml b/wasm-bindgen-derive-macro/Cargo.toml index 416e829..ff90b80 100644 --- a/wasm-bindgen-derive-macro/Cargo.toml +++ b/wasm-bindgen-derive-macro/Cargo.toml @@ -16,3 +16,7 @@ proc-macro = true syn = "1.0" quote = "1.0" proc-macro2 = "1.0" + +[dev-dependencies] +wasm-bindgen = "0.2.85" +js-sys = "0.3.55" diff --git a/wasm-bindgen-derive-macro/tests/integration.rs b/wasm-bindgen-derive-macro/tests/integration.rs new file mode 100644 index 0000000..fe75e82 --- /dev/null +++ b/wasm-bindgen-derive-macro/tests/integration.rs @@ -0,0 +1,23 @@ +// We cannot define unit tests within a proc-macro crate, so we have to use integration tests. +// Also it is non-trivial to check for compilation errors, so we only check the happy paths for now. +// TODO: see how compilation errors are tested in `auto_impl` crate. + +extern crate alloc; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen_derive_macro::TryFromJsValue; + +#[derive(TryFromJsValue)] +#[wasm_bindgen] +#[derive(Clone)] +struct MyType(usize); + +#[derive(TryFromJsValue)] +#[wasm_bindgen(js_name = "SomeJsName")] +#[derive(Clone)] +struct MyTypeCustomName(usize); + +#[test] +fn pass() { + let _x = MyType(1); + let _y = MyTypeCustomName(2); +}