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