Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Downcast a JsValue to an exported struct #2231

Open
io12 opened this issue Jul 9, 2020 · 18 comments · Fixed by #3554
Open

Downcast a JsValue to an exported struct #2231

io12 opened this issue Jul 9, 2020 · 18 comments · Fixed by #3554
Labels

Comments

@io12
Copy link

io12 commented Jul 9, 2020

Summary

In the code below, how can I cast the JsValue to Foo?

#[wasm_bindgen]
pub struct Foo(u8); // Assume this can't be serialized or deserialized

#[wasm_bindgen]
pub fn foo_downcast(foo: JsValue) {
    let foo: Foo = todo!();
    // ...
}

Additional Details

From<Foo> for JsValue is implemented automatically, so I'd expect something like TryInto<Foo> for JsValue to be implemented too, but I couldn't find anything.

Reasons why you'd want to do this

  1. Passing a vector or slice of objects to an exported function (currently Vec<Foo> and &[Foo] are unsupported, but Vec<JsValue> and &[JsValue] are supported, I think)
  2. Having automatic casting rules for parameters of exported functions (automatically parsing strings if the parameter is a string, etc)
@io12 io12 added the question label Jul 9, 2020
@alexcrichton
Copy link
Contributor

Ah yeah currently this isn't implemented. It would require an intrinsic of one form or another since the JS class lives in the JS file to test inheritance. This would be a nice feature to have though!

@aweinstock314
Copy link

We've developed a workaround that discovers the runtime class name through .__proto__.constructor.name:

use wasm_bindgen::convert::FromWasmAbi;
pub fn generic_of_jsval<T: FromWasmAbi<Abi=u32>>(js: JsValue, classname: &str) -> Result<T, JsValue> {
    use js_sys::{Object, Reflect};
    let ctor_name = Object::get_prototype_of(&js).constructor().name();
    if ctor_name == classname {
        let ptr = Reflect::get(&js, &JsValue::from_str("ptr"))?;
        let ptr_u32: u32 = ptr.as_f64().ok_or(JsValue::NULL)? as u32;
        let foo = unsafe { T::from_abi(ptr_u32) };
        Ok(foo)
    } else {
        Err(JsValue::NULL)
    }
}
#[wasm_bindgen]
pub fn foo_of_jsval(js: JsValue) -> Option<Foo> {
    generic_of_jsval(js, "Foo").unwrap_or(None)
}

Currently, "Foo" needs to be hard-coded in the snippet since the #[wasm_bindgen] derive macro doesn't generate a way to refer to it programatically. It looks like the ToTokens impl for ast::Struct (

let name_str = self.js_name.to_string();
) has a js_name that could be added. Would adding another trait to wasm_bindgen::convert that exposes this metadata and generating it from the derive macro be the preferred way to do this?

Also, should the method that performs the downcast be added to wasm_bindgen itself (possibly to the same wasm_bindgen::convert trait, if we're adding a new one), or to a different component like js_sys?

@fxdave
Copy link

fxdave commented May 28, 2021

We've developed a workaround that discovers the runtime class name through .__proto__.constructor.name:

use wasm_bindgen::convert::FromWasmAbi;
pub fn generic_of_jsval<T: FromWasmAbi<Abi=u32>>(js: JsValue, classname: &str) -> Result<T, JsValue> {
    use js_sys::{Object, Reflect};
    let ctor_name = Object::get_prototype_of(&js).constructor().name();
    if ctor_name == classname {
        let ptr = Reflect::get(&js, &JsValue::from_str("ptr"))?;
        let ptr_u32: u32 = ptr.as_f64().ok_or(JsValue::NULL)? as u32;
        let foo = unsafe { T::from_abi(ptr_u32) };
        Ok(foo)
    } else {
        Err(JsValue::NULL)
    }
}
#[wasm_bindgen]
pub fn foo_of_jsval(js: JsValue) -> Option<Foo> {
    generic_of_jsval(js, "Foo").unwrap_or(None)
}

Currently, "Foo" needs to be hard-coded in the snippet since the #[wasm_bindgen] derive macro doesn't generate a way to refer to it programatically. It looks like the ToTokens impl for ast::Struct (

let name_str = self.js_name.to_string();

) has a js_name that could be added. Would adding another trait to wasm_bindgen::convert that exposes this metadata and generating it from the derive macro be the preferred way to do this?
Also, should the method that performs the downcast be added to wasm_bindgen itself (possibly to the same wasm_bindgen::convert trait, if we're adding a new one), or to a different component like js_sys?

Thanks for this workaround. It works, however, I get this error message: Uncaught Error: recursive use of an object detected which would lead to unsafe aliasing in rust for this line:
let foo = unsafe { T::from_abi(ptr_u32) };.

I don't know why. Any ideas?

@io12
Copy link
Author

io12 commented May 28, 2021

It works, however, I get this error message: Uncaught Error: recursive use of an object detected which would lead to unsafe aliasing

This might be relevant #1578 (comment).

@fxdave
Copy link

fxdave commented May 29, 2021

It works, however, I get this error message: Uncaught Error: recursive use of an object detected which would lead to unsafe aliasing

This might be relevant #1578 (comment).

Thanks, the situation was a bit different. After checking the source code, the error meant 3 possible things:

  1. Multiple &mut Ts exist.
  2. &T and &mut T exist at the same time.
  3. An enormous amount of &T exist (and its borrow count doesn't fit in the usize).
    (if this were in the error message, that would help)

However, none of the above was true in my case. In my case, I dispatched a custom event, and there were two listeners for that event. Thus the event.detail(), a JsValue holding T was owned twice. At least, I guess this was the problem because I removed the other listener, and it is fine now.

@AlexW00
Copy link

AlexW00 commented May 21, 2022

We've developed a workaround that discovers the runtime class name through .__proto__.constructor.name:

use wasm_bindgen::convert::FromWasmAbi;
pub fn generic_of_jsval<T: FromWasmAbi<Abi=u32>>(js: JsValue, classname: &str) -> Result<T, JsValue> {
    use js_sys::{Object, Reflect};
    let ctor_name = Object::get_prototype_of(&js).constructor().name();
    if ctor_name == classname {
        let ptr = Reflect::get(&js, &JsValue::from_str("ptr"))?;
        let ptr_u32: u32 = ptr.as_f64().ok_or(JsValue::NULL)? as u32;
        let foo = unsafe { T::from_abi(ptr_u32) };
        Ok(foo)
    } else {
        Err(JsValue::NULL)
    }
}
#[wasm_bindgen]
pub fn foo_of_jsval(js: JsValue) -> Option<Foo> {
    generic_of_jsval(js, "Foo").unwrap_or(None)
}

Currently, "Foo" needs to be hard-coded in the snippet since the #[wasm_bindgen] derive macro doesn't generate a way to refer to it programatically. It looks like the ToTokens impl for ast::Struct (

let name_str = self.js_name.to_string();

) has a js_name that could be added. Would adding another trait to wasm_bindgen::convert that exposes this metadata and generating it from the derive macro be the preferred way to do this?
Also, should the method that performs the downcast be added to wasm_bindgen itself (possibly to the same wasm_bindgen::convert trait, if we're adding a new one), or to a different component like js_sys?

Thanks for this workaround. It works, however, I get this error message: Uncaught Error: recursive use of an object detected which would lead to unsafe aliasing in rust for this line: let foo = unsafe { T::from_abi(ptr_u32) };.

I don't know why. Any ideas?

Got the same error. However, I neither have callbacks nor multiple ownership in my code. I already spent a few hours trying to fix this error, but I am simply stuck. Does anyone have an idea, where this error might come from?
In my code, the generic_of_jsval function works once. When I call it a second time, i get the error:

Error: recursive use of an object detected which would lead to unsafe aliasing in rust

Here is the relevant code:

Calling the generic_of_jsval function:

// ignore getter as defined in another struct:
#[wasm_bindgen(getter)]
pub fn ignore(&self) -> RangeArray {
    self.ignore.clone()
}

// calling the from function, which then calls generic_of_jsval  (see below):
pub fn ignore_vec (&self) -> Vec<Range> { Vec::from(self.ignore()) }

Definitions:

#[wasm_bindgen]
#[derive(Clone)]
pub struct Range {
    start: js_sys::Number,
    end: js_sys::Number,
}

#[wasm_bindgen]
impl Range {
    #[wasm_bindgen(constructor)]
    pub fn new(start: js_sys::Number, end: js_sys::Number) -> Range {
        Range {
            start,
            end,
        }
    }

    #[wasm_bindgen(getter)]
    pub fn start(&self) -> js_sys::Number {
        self.start.clone()
    }

    #[wasm_bindgen(getter)]
    pub fn end(&self) -> js_sys::Number {
        self.end.clone()
    }

}

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(typescript_type = "Array<Range>")]
    #[derive(Clone, Debug)]
    pub type RangeArray;
}


impl From<RangeArray> for Vec<Range> {
    fn from(range_array: RangeArray) -> Self {
        let arr: Result<Array, RangeArray> = range_array.dyn_into::<Array>();
        arr.map_or(vec![], |array: Array| {
            array.iter()
                .filter_map(|js_val_range: JsValue|
                    generic_of_jsval(js_val_range, "Range").ok())
                .collect()
        })
    }
}

@AlexKorn
Copy link

AlexKorn commented Jun 6, 2022

We've developed a workaround that discovers the runtime class name through .__proto__.constructor.name:

use wasm_bindgen::convert::FromWasmAbi;
pub fn generic_of_jsval<T: FromWasmAbi<Abi=u32>>(js: JsValue, classname: &str) -> Result<T, JsValue> {
    use js_sys::{Object, Reflect};
    let ctor_name = Object::get_prototype_of(&js).constructor().name();
    if ctor_name == classname {
        let ptr = Reflect::get(&js, &JsValue::from_str("ptr"))?;
        let ptr_u32: u32 = ptr.as_f64().ok_or(JsValue::NULL)? as u32;
        let foo = unsafe { T::from_abi(ptr_u32) };
        Ok(foo)
    } else {
        Err(JsValue::NULL)
    }
}
#[wasm_bindgen]
pub fn foo_of_jsval(js: JsValue) -> Option<Foo> {
    generic_of_jsval(js, "Foo").unwrap_or(None)
}

Currently, "Foo" needs to be hard-coded in the snippet since the #[wasm_bindgen] derive macro doesn't generate a way to refer to it programatically. It looks like the ToTokens impl for ast::Struct (

let name_str = self.js_name.to_string();

) has a js_name that could be added. Would adding another trait to wasm_bindgen::convert that exposes this metadata and generating it from the derive macro be the preferred way to do this?
Also, should the method that performs the downcast be added to wasm_bindgen itself (possibly to the same wasm_bindgen::convert trait, if we're adding a new one), or to a different component like js_sys?

Thank you for workaround! You saved me a lot of time )
I modified it a bit to check whether we really get object (otherwise original function panics) and to produce reference to class instance:

pub fn generic_of_jsval<T: RefFromWasmAbi<Abi = u32>>(
  js: &JsValue,
  classname: &str,
) -> Result<T::Anchor, JsValue> {
  if !js.is_object() {
    return Err(JsValue::from_str(
      format!("Value supplied as {} is not an object", classname).as_str(),
    ));
  }

  let ctor_name = Object::get_prototype_of(js).constructor().name();
  if ctor_name == classname {
    let ptr = Reflect::get(js, &JsValue::from_str("ptr"))?;
    let ptr_u32: u32 = ptr.as_f64().ok_or(JsValue::NULL)? as u32;
    let foo = unsafe { T::ref_from_abi(ptr_u32) };
    Ok(foo)
  } else {
    Err(JsValue::NULL)
  }
}

Note that return type is now not T itself, but T::Anchor, that is defined as Deref<Target = Self>.

@fxdave @AlexW00 as I tested, it solves issue with Error: recursive use of an object detected which would lead to unsafe aliasing in rust. For AlexW00 sample, if you do not mind cloning, invocation may be changed to

generic_of_jsval(js_val_range, "Range").ok().clone()

@anlumo
Copy link

anlumo commented Jun 27, 2022

@AlexKorn One problem with your solution I just ran into is that when I build my code for production, the minimizer changes the name of my struct to H, so the constructor name check fails (but only when building for production, fun to discover).

So, I'm currently trying to find a way to get the current JavaScript class name for my Rust struct.

@AlexKorn
Copy link

AlexKorn commented Jun 27, 2022

Got the same problem )
Our solution for now was to turn off minification for the library (by setting it as external dep in webpack and linking it manually in app).
The problem is that class name is specified in rust source, so it appears to be compiled in wasm and is not accessible to minifier.
One hacky solution comes to my mind: our library should make http requests, and to make it possible from both node & browser I added inititialization function in library's index.js that passes wrappers around axios to wasm module, something similar may be used that will pass class names for example as hashmap: as it will be invoked from javascript, it will be visible to minifier and minifier will change class names respectively. It's ugly, but it should work.

UPD: something less hacky: add wasm-exported method to each struct that just returns its classname, ex. get_classname, and generic_of_jsval may try to get this name from JsValue as first step. This may be easily made as derive macro.

@anlumo
Copy link

anlumo commented Jun 27, 2022

For now, I just went the easy way of marking the function unsafe and removing all checks. In my application, I'm certain that I'm getting the right type there anyways.

@AlexKorn
Copy link

AlexKorn commented Jun 29, 2022

I wrapped the solution in dirty proc macro:
src/lib.rs

extern crate proc_macro;

use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};

use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Error, Fields};

macro_rules! derive_error {
    ($string: tt) => {
        Error::new(Span::call_site(), $string)
            .to_compile_error()
            .into()
    };
}

#[proc_macro_derive(TryFromJsValue)]
pub fn derive_try_from_jsvalue(input: TokenStream) -> TokenStream {
    let input: DeriveInput = parse_macro_input!(input as DeriveInput);

    let ref name = input.ident;
    let ref 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 => format!("wasm_bindgen"),
    }
    .parse::<TokenStream2>()
    .unwrap();

    let expanded = quote! {
        #[cfg(target_arch = "wasm32")]
        impl #name {
            pub fn __get_classname() -> &'static str {
                ::std::stringify!(#name)
            }
        }

        #[cfg(target_arch = "wasm32")]
        #[#wasm_bindgen_macro_invocaton]
        impl #name {
          #[wasm_bindgen(js_name = "get_classname")]
          pub fn __js_get_classname(&self) -> String {
            ::std::stringify!(#name).to_owned()
          }
        }

        #[cfg(target_arch = "wasm32")]
        impl ::std::convert::TryFrom<&::wasm_bindgen::JsValue> for #name {
            type Error = String;

            fn try_from(js: &::wasm_bindgen::JsValue) -> Result<Self, Self::Error> {
                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 get_classname = ::js_sys::Reflect::get(js, &::wasm_bindgen::JsValue::from("get_classname"))
                    .map_err(|err| format!("no get_classname method specified for object, {:?}", err))?
                    .dyn_into::<::js_sys::Function>()
                    .map_err(|err| format!("get_classname 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 {
                    let ptr = ::js_sys::Reflect::get(js, &::wasm_bindgen::JsValue::from_str("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)
}

Cargo.toml:

[package]
name = "proc-macros"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
syn = "1.0"
quote = "1.0"
proc-macro2 = "1.0"
wasm-bindgen = { version = "0.2.79", features = ["serde-serialize"] }
js-sys = "0.3.51"

It returns copy of the value, not the reference, and relies on static method get_classname exported to js: this supports minification if method names are not mangled, but as far as I now method names are not usually mangled by default. At least it works with Angular production build.
It requires #[derive(...)] invocation to come above #[wasm_bindgen], because it checks whether wasm bindgings will be generated for struct & takes alias if js_name is specified. Also I guess it's slow due to active usage of reflection.

Usage example:

#[derive(Clone, TryFromJsValue)]
#[wasm_bindgen]
pub stuct SomeStruct { ... }

fn dyn_cast(js: &JsValue) -> Result<SomeStruct, String> {
  SomeStruct::try_from(&js)
}

@ranile
Copy link
Collaborator

ranile commented Jul 1, 2022

Does unchecked_into not solve your problem?

You can do:

let foo: Foo = jsvalue.unchecked_into()

dyn_into also exists which performs runtime check. There's also _ref variants that can be used without consuming the value

@anlumo
Copy link

anlumo commented Jul 1, 2022

unchecked_into<T> requires T: JsCast, which is not implemented for #[wasm_bindgen] structs.

@yishn
Copy link

yishn commented Jul 5, 2022

I worked around by using helper JS functions and bindings, which lets wasm-bindgen handle all that unsafe stuff:

// /js/cast.js

export function castInto(value) {
  return value;
}

export function castRef(value, callback) {
  callback(value);
}
#[wasm_bindgen(module = "/js/cast.js"))]
extern "C" {
  // This will let Rust regain ownership of `Foo`
  #[wasm_bindgen(js_name = castInto)]
  pub fn cast_into_foo(value: JsValue) -> Foo;

  // This will let you get a reference to `Foo` in the `callback` closure (JS retains ownership of `Foo`)
  #[wasm_bindgen(js_name = castRef)]
  pub fn cast_ref_foo(value: &JsValue, callback: &mut dyn FnMut(&Foo));
}

This can be improved by adding type checks in the JS functions and returning Option<Foo> and Option<&Foo> correspondingly.

@fjarri
Copy link

fjarri commented Sep 15, 2022

@AlexKorn thank you for that workaround with the proc macro! Would it be okay with you if I make it into a crate (referencing you as the author, of course)? Or perhaps you've already done so?

@AlexKorn
Copy link

No, I have not, feel free to use it as you wish, taking in consideration that the solution itself belongs to aweinstock314 =)

@fjarri
Copy link

fjarri commented Sep 17, 2022

Published as https://crates.io/crates/wasm-bindgen-derive with minor changes (decided to go for a more general name, in case there are other derive macros to be added). I added the attribution to @AlexKorn and @aweinstock314 in https://docs.rs/wasm-bindgen-derive/latest/wasm_bindgen_derive/derive.TryFromJsValue.html, please tell me if you would prefer it to be more prominent.

@Liamolucko
Copy link
Collaborator

We've unfortunately regressed on this as a part of #3709.

@Liamolucko Liamolucko reopened this Nov 28, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet