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}