wasm_bindgen_downcast/
lib.rs

1//!  Downcast a JavaScript wrapper generated by `wasm-bindgen` back to its original
2//! struct.
3//!
4//! # Examples
5//!
6//! ```rust
7//! use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
8//! use wasm_bindgen_downcast::DowncastJS;
9//!
10//! #[wasm_bindgen]
11//! #[derive(DowncastJS)]
12//! pub struct Person {
13//!     name: String,
14//! }
15//!
16//! /// Try to greet something. If it is the provided value wraps a [`Person`]
17//! /// struct, we'll try to greet them by name.
18//! #[wasm_bindgen]
19//! pub fn greet(maybe_person: &JsValue) -> Option<String> {
20//!     let p = Person::downcast_js_ref(maybe_person)?;
21//!     Some(format!("Hello, {}!", p.name))
22//! }
23//! ```
24
25pub use wasm_bindgen_downcast_macros::DowncastJS;
26
27use js_sys::{Function, Reflect};
28use wasm_bindgen::{
29    convert::{FromWasmAbi, RefFromWasmAbi},
30    JsCast, JsValue, UnwrapThrowExt,
31};
32
33/// Cast a Rust wrapper class that was generated by `#[wasm_bindgen]` back to
34/// the underlying Rust type.
35///
36/// This is a workaround for
37/// [rustwasm/wasm-bindgen#2231](https://github.com/rustwasm/wasm-bindgen/issues/2231)
38/// that is robust with respect to minification (something other solutions
39/// [can't handle](https://github.com/rustwasm/wasm-bindgen/issues/2231#issuecomment-1167895291)).
40///
41/// # Safety
42///
43/// This assumes the wrapper class has a static `__wbgd_downcast_symbol`
44/// method which returns the same [`DowncastToken`] as
45/// [`DowncastJS::token()`]. Each trait implementation.
46///
47/// Users should use the custom derive rather than implement this manually.
48pub unsafe trait DowncastJS:
49    Sized + FromWasmAbi<Abi = u32> + RefFromWasmAbi<Abi = u32>
50{
51    fn token() -> &'static DowncastToken;
52
53    fn downcast_js(value: JsValue) -> Result<Self, JsValue> {
54        // Safety: By using an unsafe trait, we're the responsibility to
55        // satisfy downcast_to_ptr()'s invariants is moved to the implementor.
56        // Assuming they are satisfied, it should be fine to convert back
57        // from the ABI representation.
58        unsafe {
59            match downcast_to_ptr::<Self>(&value) {
60                Some(ptr) => Ok(Self::from_abi(ptr)),
61                None => Err(value),
62            }
63        }
64    }
65
66    fn downcast_js_ref(value: &JsValue) -> Option<Self::Anchor> {
67        // Safety: By using an unsafe trait, we're the responsibility to
68        // satisfy downcast_to_ptr()'s invariants is moved to the implementor.
69        // Assuming they are satisfied, it should be fine to convert back
70        // from the ABI representation.
71        unsafe { downcast_to_ptr::<Self>(value).map(|ptr| Self::ref_from_abi(ptr)) }
72    }
73}
74
75// This whole mechanism works because the JavaScript wrapper
76// class has a static `__wbgd_downcast_symbol()` method which returns the
77// same unique Symbol we get in the [`DowncastJS::symbol()`] method.
78//
79// If we can read that symbol and it matches the symbol we get from
80// [`DowncastJS::symbol()`], we know for sure that the pointer is valid and safe
81// to cast back to our type.
82unsafe fn downcast_to_ptr<T: DowncastJS>(value: &JsValue) -> Option<u32> {
83    if !value.is_object() {
84        return None;
85    }
86
87    let class = Reflect::get_prototype_of(value).ok()?.constructor();
88    let key = JsValue::from_str("__wbgd_downcast_token");
89
90    let downcast_symbol_func: Function = Reflect::get(&class, &key)
91        .and_then(|v: JsValue| v.dyn_into())
92        .ok()?;
93
94    let actual_symbol = downcast_symbol_func.call0(&class).ok()?;
95
96    if *T::token() != actual_symbol {
97        return None;
98    }
99
100    // Note: this assumes the wrapper class generated by #[wasm_bindgen] will
101    // always store the pointer in a field called "ptr".
102    let key = JsValue::from_str("ptr");
103    let ptr = Reflect::get(value, &key).ok().and_then(|v| v.as_f64())?;
104
105    Some(ptr as u32)
106}
107
108/// A token used when downcasting wrapper classes back to their Rust types.
109#[derive(Clone)]
110pub struct DowncastToken(pub js_sys::Symbol);
111
112impl DowncastToken {
113    /// Create a new, unique token.
114    pub fn unique() -> Self {
115        #[wasm_bindgen::prelude::wasm_bindgen]
116        extern "C" {
117            #[wasm_bindgen(js_name = "Symbol")]
118            static SYMBOL: js_sys::Function;
119        }
120
121        let js_symbol = SYMBOL
122            .call0(&JsValue::NULL)
123            .expect_throw("Unable to call the Symbol() function")
124            .dyn_into()
125            .expect_throw("Unable to convert the return value of Symbol() into a symbol");
126
127        DowncastToken(js_symbol)
128    }
129}
130
131impl PartialEq<wasm_bindgen::JsValue> for DowncastToken {
132    fn eq(&self, other: &wasm_bindgen::JsValue) -> bool {
133        *self.0 == *other
134    }
135}
136
137// Safety: This should be okay because
138// a) JavaScript is single-threaded, and
139// b) The Symbol type is only ever stored in an once_cell::sync::Lazy
140//    without being mutated.
141unsafe impl Sync for DowncastToken {}
142unsafe impl Send for DowncastToken {}
143
144#[doc(hidden)]
145pub mod internal {
146    pub use crate::DowncastToken;
147    pub use js_sys::Symbol;
148    pub use once_cell::sync::Lazy;
149    pub use wasm_bindgen::prelude::wasm_bindgen;
150}