wasm-bindgen
wasm-bindgen copied to clipboard
Downcast a JsValue to an exported struct
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
- Passing a vector or slice of objects to an exported function (currently
Vec<Foo>and&[Foo]are unsupported, butVec<JsValue>and&[JsValue]are supported, I think) - Having automatic casting rules for parameters of exported functions (automatically parsing strings if the parameter is a string, etc)
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!
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 (https://github.com/rustwasm/wasm-bindgen/blob/17950202ca9458d35bd78a48ebb126800edb0999/crates/backend/src/codegen.rs#L139) 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?
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 theToTokensimpl forast::Struct(https://github.com/rustwasm/wasm-bindgen/blob/17950202ca9458d35bd78a48ebb126800edb0999/crates/backend/src/codegen.rs#L139
) has a
js_namethat could be added. Would adding another trait towasm_bindgen::convertthat 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 towasm_bindgenitself (possibly to the samewasm_bindgen::converttrait, if we're adding a new one), or to a different component likejs_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?
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 https://github.com/rustwasm/wasm-bindgen/issues/1578#issuecomment-499962170.
It works, however, I get this error message:
Uncaught Error: recursive use of an object detected which would lead to unsafe aliasingThis might be relevant #1578 (comment).
Thanks, the situation was a bit different. After checking the source code, the error meant 3 possible things:
- Multiple
&mut Ts exist. &Tand&mut Texist at the same time.- An enormous amount of
&Texist (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.
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 theToTokensimpl forast::Struct( https://github.com/rustwasm/wasm-bindgen/blob/17950202ca9458d35bd78a48ebb126800edb0999/crates/backend/src/codegen.rs#L139) has a
js_namethat could be added. Would adding another trait towasm_bindgen::convertthat 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 towasm_bindgenitself (possibly to the samewasm_bindgen::converttrait, if we're adding a new one), or to a different component likejs_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 rustfor 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()
})
}
}
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 theToTokensimpl forast::Struct(https://github.com/rustwasm/wasm-bindgen/blob/17950202ca9458d35bd78a48ebb126800edb0999/crates/backend/src/codegen.rs#L139
) has a
js_namethat could be added. Would adding another trait towasm_bindgen::convertthat 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 towasm_bindgenitself (possibly to the samewasm_bindgen::converttrait, if we're adding a new one), or to a different component likejs_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()
@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.
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.
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.
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)
}
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
unchecked_into<T> requires T: JsCast, which is not implemented for #[wasm_bindgen] structs.
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.
@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?
No, I have not, feel free to use it as you wish, taking in consideration that the solution itself belongs to aweinstock314 =)
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.
We've unfortunately regressed on this as a part of #3709.