1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
//!  Downcast a JavaScript wrapper generated by `wasm-bindgen` back to its original
//! struct.
//!
//! # Examples
//!
//! ```rust
//! use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
//! use wasm_bindgen_downcast::DowncastJS;
//!
//! #[wasm_bindgen]
//! #[derive(DowncastJS)]
//! pub struct Person {
//!     name: String,
//! }
//!
//! /// Try to greet something. If it is the provided value wraps a [`Person`]
//! /// struct, we'll try to greet them by name.
//! #[wasm_bindgen]
//! pub fn greet(maybe_person: &JsValue) -> Option<String> {
//!     let p = Person::downcast_js_ref(maybe_person)?;
//!     Some(format!("Hello, {}!", p.name))
//! }
//! ```

pub use wasm_bindgen_downcast_macros::DowncastJS;

use js_sys::{Function, Reflect};
use wasm_bindgen::{
    convert::{FromWasmAbi, RefFromWasmAbi},
    JsCast, JsValue, UnwrapThrowExt,
};

/// Cast a Rust wrapper class that was generated by `#[wasm_bindgen]` back to
/// the underlying Rust type.
///
/// This is a workaround for
/// [rustwasm/wasm-bindgen#2231](https://github.com/rustwasm/wasm-bindgen/issues/2231)
/// that is robust with respect to minification (something other solutions
/// [can't handle](https://github.com/rustwasm/wasm-bindgen/issues/2231#issuecomment-1167895291)).
///
/// # Safety
///
/// This assumes the wrapper class has a static `__wbgd_downcast_symbol`
/// method which returns the same [`DowncastToken`] as
/// [`DowncastJS::token()`]. Each trait implementation.
///
/// Users should use the custom derive rather than implement this manually.
pub unsafe trait DowncastJS:
    Sized + FromWasmAbi<Abi = u32> + RefFromWasmAbi<Abi = u32>
{
    fn token() -> &'static DowncastToken;

    fn downcast_js(value: JsValue) -> Result<Self, JsValue> {
        // Safety: By using an unsafe trait, we're the responsibility to
        // satisfy downcast_to_ptr()'s invariants is moved to the implementor.
        // Assuming they are satisfied, it should be fine to convert back
        // from the ABI representation.
        unsafe {
            match downcast_to_ptr::<Self>(&value) {
                Some(ptr) => Ok(Self::from_abi(ptr)),
                None => Err(value),
            }
        }
    }

    fn downcast_js_ref(value: &JsValue) -> Option<Self::Anchor> {
        // Safety: By using an unsafe trait, we're the responsibility to
        // satisfy downcast_to_ptr()'s invariants is moved to the implementor.
        // Assuming they are satisfied, it should be fine to convert back
        // from the ABI representation.
        unsafe { downcast_to_ptr::<Self>(value).map(|ptr| Self::ref_from_abi(ptr)) }
    }
}

// This whole mechanism works because the JavaScript wrapper
// class has a static `__wbgd_downcast_symbol()` method which returns the
// same unique Symbol we get in the [`DowncastJS::symbol()`] method.
//
// If we can read that symbol and it matches the symbol we get from
// [`DowncastJS::symbol()`], we know for sure that the pointer is valid and safe
// to cast back to our type.
unsafe fn downcast_to_ptr<T: DowncastJS>(value: &JsValue) -> Option<u32> {
    if !value.is_object() {
        return None;
    }

    let class = Reflect::get_prototype_of(value).ok()?.constructor();
    let key = JsValue::from_str("__wbgd_downcast_token");

    let downcast_symbol_func: Function = Reflect::get(&class, &key)
        .and_then(|v: JsValue| v.dyn_into())
        .ok()?;

    let actual_symbol = downcast_symbol_func.call0(&class).ok()?;

    if *T::token() != actual_symbol {
        return None;
    }

    // Note: this assumes the wrapper class generated by #[wasm_bindgen] will
    // always store the pointer in a field called "ptr".
    let key = JsValue::from_str("ptr");
    let ptr = Reflect::get(value, &key).ok().and_then(|v| v.as_f64())?;

    Some(ptr as u32)
}

/// A token used when downcasting wrapper classes back to their Rust types.
#[derive(Clone)]
pub struct DowncastToken(pub js_sys::Symbol);

impl DowncastToken {
    /// Create a new, unique token.
    pub fn unique() -> Self {
        #[wasm_bindgen::prelude::wasm_bindgen]
        extern "C" {
            #[wasm_bindgen(js_name = "Symbol")]
            static SYMBOL: js_sys::Function;
        }

        let js_symbol = SYMBOL
            .call0(&JsValue::NULL)
            .expect_throw("Unable to call the Symbol() function")
            .dyn_into()
            .expect_throw("Unable to convert the return value of Symbol() into a symbol");

        DowncastToken(js_symbol)
    }
}

impl PartialEq<wasm_bindgen::JsValue> for DowncastToken {
    fn eq(&self, other: &wasm_bindgen::JsValue) -> bool {
        *self.0 == *other
    }
}

// Safety: This should be okay because
// a) JavaScript is single-threaded, and
// b) The Symbol type is only ever stored in an once_cell::sync::Lazy
//    without being mutated.
unsafe impl Sync for DowncastToken {}
unsafe impl Send for DowncastToken {}

#[doc(hidden)]
pub mod internal {
    pub use crate::DowncastToken;
    pub use js_sys::Symbol;
    pub use once_cell::sync::Lazy;
    pub use wasm_bindgen::prelude::wasm_bindgen;
}