Skip to main content

wry_bindgen/
closure.rs

1//! Closure compatibility types and traits.
2
3use alloc::boxed::Box;
4
5use crate::JsValue;
6use crate::encode::{BinaryEncode, EncodeTypeDef};
7use crate::ipc::{DecodeError, DecodedData, EncodedData};
8use crate::object_store::insert_object;
9use wry_bindgen_core::{CallbackKey, ObjectHandle, RustCallback};
10
11/// Borrowed or owned closure handle for passing Rust closures to JavaScript.
12pub struct ScopedClosure<'a, T: ?Sized> {
13    // careful: must be Box<T> not just T because unsized PhantomData
14    // seems to have weird interaction with Pin<>
15    pub(crate) _phantom: core::marker::PhantomData<(&'a (), crate::alloc::boxed::Box<T>)>,
16    pub(crate) callback: CallbackOwnership,
17    pub(crate) value: crate::JsValue,
18}
19
20/// Who is responsible for disposing the JS-side `RustFunction` wrapper that
21/// backs a `ScopedClosure`. Encoding an owned `Closure` by value detaches it
22/// because JavaScript takes ownership; borrowed encodes keep Rust ownership.
23/// Destructors only dispose in the `Owned` state.
24#[derive(Clone, Copy)]
25pub(crate) enum CallbackOwnership {
26    /// No Rust callback backing this closure (e.g., wrapping a raw JS function).
27    None,
28    /// Rust owns the callback; `ScopedClosure::drop` disposes it.
29    Owned,
30    /// Ownership has been handed off. No dispose on drop, but encoders still
31    /// flush so JS receives the callable before the call that needs it.
32    Detached,
33}
34
35impl CallbackOwnership {
36    pub(crate) fn owned(_handle: ObjectHandle) -> Self {
37        Self::Owned
38    }
39
40    /// Encoding this closure to JS requires an immediate flush so the JS
41    /// side has the callable ready.
42    pub(crate) fn needs_flush(&self) -> bool {
43        !matches!(self, Self::None)
44    }
45
46    /// Transition `Owned` → `Detached`. No-op for `None` / `Detached`.
47    pub(crate) fn detach(&mut self) {
48        if matches!(self, Self::Owned) {
49            *self = Self::Detached;
50        }
51    }
52}
53
54/// Owned closure handle. This follows upstream wasm-bindgen's alias direction.
55pub type Closure<T> = ScopedClosure<'static, T>;
56
57impl<T: ?Sized> ScopedClosure<'static, T> {
58    /// Wrap a raw closure. Only for use by generated code.
59    pub(crate) fn wrap_encode_decode<FnPtr>(
60        encode_decode: impl Fn(&mut DecodedData, &mut EncodedData) -> Result<(), DecodeError> + 'static,
61    ) -> Self
62    where
63        CallbackKey<FnPtr>: BinaryEncode + EncodeTypeDef,
64    {
65        let key = insert_object(RustCallback::new_fn(encode_decode));
66        let value =
67            crate::__rt::wbg_cast::<CallbackKey<FnPtr>, crate::JsValue>(CallbackKey::new(key));
68        Self {
69            _phantom: core::marker::PhantomData,
70            callback: crate::closure::CallbackOwnership::owned(key),
71            value,
72        }
73    }
74
75    /// Wrap a raw closure. Only for use by generated code.
76    pub(crate) fn wrap_encode_decode_mut<FnPtr>(
77        encode_decode: impl FnMut(&mut DecodedData, &mut EncodedData) -> Result<(), DecodeError>
78        + 'static,
79    ) -> Self
80    where
81        CallbackKey<FnPtr>: BinaryEncode + EncodeTypeDef,
82    {
83        let key = insert_object(RustCallback::new_fn_mut(encode_decode));
84        let value =
85            crate::__rt::wbg_cast::<CallbackKey<FnPtr>, crate::JsValue>(CallbackKey::new(key));
86        Self {
87            _phantom: core::marker::PhantomData,
88            callback: crate::closure::CallbackOwnership::owned(key),
89            value,
90        }
91    }
92
93    /// Wrap a raw one-shot closure. Only for use by generated code.
94    pub(crate) fn wrap_once_encode_decode_mut<FnPtr>(
95        mut encode_decode: impl FnMut(&mut DecodedData, &mut EncodedData) -> Result<(), DecodeError>
96        + 'static,
97    ) -> Self
98    where
99        CallbackKey<FnPtr>: BinaryEncode + EncodeTypeDef,
100    {
101        let handle_cell = alloc::rc::Rc::new(core::cell::Cell::new(None::<ObjectHandle>));
102        let handle_for_callback = handle_cell.clone();
103        let key = insert_object(RustCallback::new_fn_mut(move |decoder, encoder| {
104            let result = encode_decode(decoder, encoder);
105            // Dispose the one-shot callback whether or not decoding succeeded.
106            if let Some(handle) = handle_for_callback.take() {
107                handle.drop_rust_object();
108            }
109            result
110        }));
111        handle_cell.set(Some(key));
112        let value = crate::__rt::wbg_cast::<CallbackKey<FnPtr>, crate::JsValue>(
113            CallbackKey::js_owned_once(key),
114        );
115        // Once-cells dispose themselves after the first call, but they still
116        // need Rust-side ownership while a `Closure` handle exists. Promise
117        // adapters store resolve/reject once-closures and rely on dropping the
118        // pair after the first completion to dispose the unused callback.
119        Self {
120            _phantom: core::marker::PhantomData,
121            callback: crate::closure::CallbackOwnership::owned(key),
122            value,
123        }
124    }
125}
126
127impl<'a, T> ScopedClosure<'a, T>
128where
129    T: ?Sized + WasmClosure,
130{
131    /// Returns the JavaScript function value for this closure.
132    pub fn as_js_value(&self) -> &JsValue {
133        &self.value
134    }
135}
136
137impl<T: ?Sized> Drop for ScopedClosure<'_, T> {
138    fn drop(&mut self) {
139        if let crate::closure::CallbackOwnership::Owned = self.callback {
140            self.value.js_ref().dispose_js_rust_function();
141        }
142    }
143}
144
145impl<T: ?Sized> Unpin for ScopedClosure<'_, T> {}
146
147/// Marker for closures that are safe to invoke through panic-catching glue.
148#[doc(hidden)]
149pub trait MaybeUnwindSafe {}
150
151impl<T: ?Sized> MaybeUnwindSafe for T {}
152
153/// A trait for converting a Rust closure into the JS closure type `T`.
154#[doc(hidden)]
155pub trait IntoWasmClosure<T: ?Sized> {
156    fn into_closure(self) -> Closure<T>
157    where
158        Self: Sized,
159    {
160        unreachable!("unsized closure objects must be converted from Box<Self>")
161    }
162
163    fn into_closure_box(self: Box<Self>) -> Closure<T>;
164}
165
166/// A trait for converting a shared borrowed Rust closure into the JS closure type `T`.
167#[doc(hidden)]
168pub trait IntoWasmClosureRef<T: ?Sized> {
169    fn into_scoped_closure_ref<'a>(t: &'a Self) -> ScopedClosure<'a, T::Static>
170    where
171        T: WasmClosure;
172}
173
174/// A trait for converting a mutably borrowed Rust closure into the JS closure type `T`.
175#[doc(hidden)]
176pub trait IntoWasmClosureRefMut<T: ?Sized> {
177    fn into_scoped_closure_ref_mut<'a>(t: &'a mut Self) -> ScopedClosure<'a, T::Static>
178    where
179        T: WasmClosure;
180}
181
182/// A trait for converting an `FnOnce(A...) -> R` into a `Closure<dyn FnMut(A...) -> R>`.
183#[doc(hidden)]
184pub trait WasmClosureFnOnce<T: ?Sized, A, R>: Sized + 'static {
185    fn into_closure(self) -> Closure<T>;
186}
187
188/// A trait for converting an aborting `FnOnce(A...) -> R` into a closure.
189#[doc(hidden)]
190pub trait WasmClosureFnOnceAbort<T: ?Sized, A, R>: Sized + 'static {
191    fn into_closure(self) -> Closure<T>;
192}
193
194impl<T: ?Sized> AsRef<JsValue> for ScopedClosure<'_, T> {
195    fn as_ref(&self) -> &JsValue {
196        &self.value
197    }
198}
199
200impl<T> core::fmt::Debug for ScopedClosure<'_, T>
201where
202    T: ?Sized,
203{
204    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
205        // Mirror wasm-bindgen's opaque `Closure { ... }`.
206        f.write_str("Closure { ... }")
207    }
208}
209
210/// Upstream-compatible marker trait for closure signatures.
211#[doc(hidden)]
212pub trait WasmClosure {
213    /// The `'static` version of this closure type.
214    type Static: ?Sized;
215    /// The mutable version of this closure type.
216    type AsMut: ?Sized;
217}
218
219/// Internal trait for closure types that can be wrapped and passed to JavaScript.
220#[doc(hidden)]
221pub trait WryWasmClosure<M> {
222    /// Create a Closure from a boxed closure.
223    fn into_js_closure(boxed: Box<Self>) -> Closure<Self>;
224}
225
226impl<T> ScopedClosure<'static, T>
227where
228    T: ?Sized + WasmClosure,
229{
230    pub fn new<F>(t: F) -> Self
231    where
232        F: IntoWasmClosure<T> + MaybeUnwindSafe + 'static,
233    {
234        Self::own(t)
235    }
236
237    pub fn own<F>(t: F) -> Self
238    where
239        F: IntoWasmClosure<T> + MaybeUnwindSafe + 'static,
240    {
241        <F as IntoWasmClosure<T>>::into_closure(t)
242    }
243
244    pub fn own_aborting<F>(t: F) -> Self
245    where
246        F: IntoWasmClosure<T> + 'static,
247    {
248        <F as IntoWasmClosure<T>>::into_closure(t)
249    }
250
251    pub fn own_assert_unwind_safe<F>(t: F) -> Self
252    where
253        F: IntoWasmClosure<T> + 'static,
254    {
255        <F as IntoWasmClosure<T>>::into_closure(t)
256    }
257
258    pub fn wrap<F>(data: Box<F>) -> Self
259    where
260        F: IntoWasmClosure<T> + ?Sized + MaybeUnwindSafe,
261    {
262        <F as IntoWasmClosure<T>>::into_closure_box(data)
263    }
264
265    pub fn wrap_aborting<F>(data: Box<F>) -> Self
266    where
267        F: IntoWasmClosure<T> + ?Sized,
268    {
269        <F as IntoWasmClosure<T>>::into_closure_box(data)
270    }
271
272    pub fn wrap_assert_unwind_safe<F>(data: Box<F>) -> Self
273    where
274        F: IntoWasmClosure<T> + ?Sized,
275    {
276        <F as IntoWasmClosure<T>>::into_closure_box(data)
277    }
278
279    pub fn borrow<'a, F>(t: &'a F) -> ScopedClosure<'a, T::Static>
280    where
281        F: IntoWasmClosureRef<T> + MaybeUnwindSafe + ?Sized,
282    {
283        F::into_scoped_closure_ref(t)
284    }
285
286    pub fn borrow_aborting<'a, F>(t: &'a F) -> ScopedClosure<'a, T::Static>
287    where
288        F: IntoWasmClosureRef<T> + ?Sized,
289    {
290        F::into_scoped_closure_ref(t)
291    }
292
293    pub fn borrow_assert_unwind_safe<'a, F>(t: &'a F) -> ScopedClosure<'a, T::Static>
294    where
295        F: IntoWasmClosureRef<T> + ?Sized,
296    {
297        F::into_scoped_closure_ref(t)
298    }
299
300    pub fn borrow_mut<'a, F>(t: &'a mut F) -> ScopedClosure<'a, T::Static>
301    where
302        F: IntoWasmClosureRefMut<T> + MaybeUnwindSafe + ?Sized,
303    {
304        F::into_scoped_closure_ref_mut(t)
305    }
306
307    pub fn borrow_mut_aborting<'a, F>(t: &'a mut F) -> ScopedClosure<'a, T::Static>
308    where
309        F: IntoWasmClosureRefMut<T> + ?Sized,
310    {
311        F::into_scoped_closure_ref_mut(t)
312    }
313
314    pub fn borrow_mut_assert_unwind_safe<'a, F>(t: &'a mut F) -> ScopedClosure<'a, T::Static>
315    where
316        F: IntoWasmClosureRefMut<T> + ?Sized,
317    {
318        F::into_scoped_closure_ref_mut(t)
319    }
320
321    /// Create a `Closure` from a function that can only be called once.
322    ///
323    /// Since we have no way of enforcing that JS cannot attempt to call this
324    /// `FnOnce` more than once, this produces a `Closure<dyn FnMut(A...) -> R>`
325    /// that will panic if called more than once.
326    pub fn once<F, A, R>(fn_once: F) -> Self
327    where
328        F: WasmClosureFnOnce<T, A, R> + MaybeUnwindSafe,
329    {
330        <F as WasmClosureFnOnce<T, A, R>>::into_closure(fn_once)
331    }
332
333    pub fn once_aborting<F, A, R>(fn_once: F) -> Self
334    where
335        F: WasmClosureFnOnceAbort<T, A, R>,
336    {
337        <F as WasmClosureFnOnceAbort<T, A, R>>::into_closure(fn_once)
338    }
339
340    pub fn once_assert_unwind_safe<F, A, R>(fn_once: F) -> Self
341    where
342        F: WasmClosureFnOnceAbort<T, A, R>,
343    {
344        Self::once_aborting(fn_once)
345    }
346
347    /// Forgets the closure, leaking it.
348    pub fn forget(self) {
349        core::mem::forget(self);
350    }
351
352    /// Converts the `Closure` into a `JsValue`.
353    pub fn into_js_value(self) -> JsValue {
354        let value = core::mem::ManuallyDrop::new(self);
355        // Clone the value to get ownership without triggering drop
356        value.value.clone()
357    }
358
359    /// Create a `Closure` from a function that can only be called once,
360    /// and return the underlying `JsValue` directly.
361    ///
362    /// This is a convenience method that combines `once` and `into_js_value`.
363    pub fn once_into_js<F, A, R>(fn_once: F) -> JsValue
364    where
365        F: WasmClosureFnOnce<T, A, R> + MaybeUnwindSafe,
366    {
367        Self::once(fn_once).into_js_value()
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use core::panic::{RefUnwindSafe, UnwindSafe};
374
375    use super::ScopedClosure;
376
377    fn assert_auto_traits<T: Send + Sync + UnwindSafe + RefUnwindSafe>() {}
378
379    #[test]
380    fn scoped_closure_preserves_auto_traits() {
381        assert_auto_traits::<
382            ScopedClosure<'static, dyn Fn() + Send + Sync + UnwindSafe + RefUnwindSafe>,
383        >();
384        assert_auto_traits::<
385            ScopedClosure<'static, dyn FnMut() + Send + Sync + UnwindSafe + RefUnwindSafe>,
386        >();
387    }
388}