wry_bindgen/
lib.rs

1//! wry-bindgen - Runtime support for wasm-bindgen-style bindings over Wry's WebView
2//!
3//! This crate provides the runtime types and traits needed for the `#[wasm_bindgen]`
4//! attribute macro to generate code that works with Wry's IPC protocol.
5//!
6//! # Architecture
7//!
8//! The crate is organized into several modules:
9//!
10//! - [`encode`] - Core encoding/decoding traits for Rust types
11//! - [`function`] - JSFunction type for calling JavaScript functions
12//! - [`mod@batch`] - Batching system for grouping multiple JS operations
13//! - [`runtime`] - Event loop and runtime management
14
15#![no_std]
16
17pub extern crate alloc;
18#[macro_use]
19extern crate std;
20
21pub mod batch;
22mod cast;
23pub mod convert;
24pub mod encode;
25pub mod function;
26mod function_registry;
27mod intern;
28pub(crate) mod ipc;
29mod js_helpers;
30mod lazy;
31#[doc(hidden)]
32pub mod object_store;
33pub mod runtime;
34mod value;
35pub mod wry;
36
37pub use intern::*;
38
39/// Re-export of the Closure type for wasm-bindgen API compatibility.
40/// Allows `use wasm_bindgen::closure::Closure;`
41pub mod closure {
42    pub use crate::Closure;
43    pub use crate::WasmClosure;
44}
45
46/// Runtime module for wasm-bindgen compatibility.
47/// This module provides the wbg_cast function used for type casting.
48pub mod __rt {
49    use crate::{
50        __wry_submit_js_function, JsValue, LazyJsFunction,
51        encode::{BatchableResult, BinaryEncode, EncodeTypeDef},
52    };
53
54    /// Cast between types via the binary protocol.
55    ///
56    /// This is the wry-bindgen equivalent of wasm-bindgen's wbg_cast.
57    /// It encodes `value` using From's BinaryEncode, sends to JS as identity,
58    /// and decodes the result using To's BinaryDecode.
59    #[inline]
60    pub fn wbg_cast<From, To>(value: From) -> To
61    where
62        From: BinaryEncode + EncodeTypeDef,
63        To: BatchableResult + EncodeTypeDef,
64    {
65        let func: LazyJsFunction<fn(From) -> To> = __wry_submit_js_function!("(a0) => a0");
66        func.call(value)
67    }
68
69    /// Convert a panic value into a JsValue error.
70    ///
71    /// This is used by wasm-bindgen-futures to convert Rust panics into JS errors.
72    #[cfg(feature = "std")]
73    pub fn panic_to_panic_error(val: std::boxed::Box<dyn std::any::Any + Send>) -> JsValue {
74        let maybe_panic_msg: Option<&str> = if let Some(s) = val.downcast_ref::<&str>() {
75            Some(s)
76        } else if let Some(s) = val.downcast_ref::<std::string::String>() {
77            Some(s)
78        } else {
79            None
80        };
81        // Create an Error object with the panic message
82        JsValue::from_str(maybe_panic_msg.unwrap_or("Rust panic"))
83    }
84}
85
86macro_rules! cast {
87    (($from:ty => $to:ty) $val:expr) => {{ $crate::__rt::wbg_cast::<$from, $to>($val) }};
88}
89
90macro_rules! to_js_value {
91    ($ty:ty) => {
92        impl From<$ty> for $crate::JsValue {
93            fn from(val: $ty) -> Self {
94                cast! {($ty => $crate::JsValue) val}
95            }
96        }
97    };
98}
99
100macro_rules! from_js_value {
101    ($ty:ty) => {
102        impl From<$crate::JsValue> for $ty {
103            fn from(val: $crate::JsValue) -> Self {
104                cast! {($crate::JsValue => $ty) val}
105            }
106        }
107    };
108}
109
110impl TryFrom<JsValue> for u64 {
111    type Error = JsValue;
112
113    fn try_from(value: JsValue) -> Result<Self, Self::Error> {
114        eprintln!("TryFrom<JsValue> for u64 is likely wrong");
115        #[wasm_bindgen(crate = crate, inline_js = "export function BigIntAsU64(val) {
116            if (typeof val !== 'bigint') {
117                throw new Error('Value is not a BigInt');
118            }
119            return Number(val);
120        }")]
121        extern "C" {
122            #[wasm_bindgen(js_name = "BigIntAsU64")]
123            fn big_int_as_u64(val: &JsValue) -> Result<u64, JsValue>;
124        }
125
126        big_int_as_u64(&value)
127    }
128}
129
130impl TryFrom<JsValue> for i64 {
131    type Error = JsValue;
132
133    fn try_from(value: JsValue) -> Result<Self, Self::Error> {
134        eprintln!("TryFrom<JsValue> for u64 is likely wrong");
135        #[wasm_bindgen(crate = crate, inline_js = "export function BigIntAsU64(val) {
136            if (typeof val !== 'bigint') {
137                throw new Error('Value is not a BigInt');
138            }
139            return Number(val);
140        }")]
141        extern "C" {
142            #[wasm_bindgen(js_name = "BigIntAsU64")]
143            fn big_int_as_i64(val: &JsValue) -> Result<i64, JsValue>;
144        }
145
146        big_int_as_i64(&value)
147    }
148}
149
150impl TryFrom<JsValue> for f64 {
151    type Error = JsValue;
152
153    fn try_from(value: JsValue) -> Result<Self, Self::Error> {
154        value.as_f64().ok_or(value)
155    }
156}
157
158impl TryFrom<&JsValue> for f64 {
159    type Error = JsValue;
160
161    fn try_from(value: &JsValue) -> Result<Self, Self::Error> {
162        value.as_f64().ok_or_else(|| value.clone())
163    }
164}
165
166impl TryFrom<JsValue> for i128 {
167    type Error = JsValue;
168
169    fn try_from(value: JsValue) -> Result<Self, Self::Error> {
170        #[wasm_bindgen(crate = crate, inline_js = "export function BigIntAsI128(val) {
171            if (typeof val !== 'bigint') {
172                throw new Error('Value is not a BigInt');
173            }
174            return Number(val);
175        }")]
176        extern "C" {
177            #[wasm_bindgen(js_name = "BigIntAsI128")]
178            fn big_int_as_i128(val: &JsValue) -> Result<i128, JsValue>;
179        }
180
181        big_int_as_i128(&value)
182    }
183}
184
185impl TryFrom<JsValue> for u128 {
186    type Error = JsValue;
187
188    fn try_from(value: JsValue) -> Result<Self, Self::Error> {
189        #[wasm_bindgen(crate = crate, inline_js = "export function BigIntAsU128(val) {
190            if (typeof val !== 'bigint') {
191                throw new Error('Value is not a BigInt');
192            }
193            if (val < 0n) {
194                throw new Error('Value is negative');
195            }
196            return Number(val);
197        }")]
198        extern "C" {
199            #[wasm_bindgen(js_name = "BigIntAsU128")]
200            fn big_int_as_u128(val: &JsValue) -> Result<u128, JsValue>;
201        }
202
203        big_int_as_u128(&value)
204    }
205}
206
207impl TryFrom<JsValue> for String {
208    type Error = JsValue;
209
210    fn try_from(value: JsValue) -> Result<Self, Self::Error> {
211        value.as_string().ok_or(value)
212    }
213}
214
215to_js_value!(i8);
216from_js_value!(i8);
217to_js_value!(i16);
218from_js_value!(i16);
219to_js_value!(i32);
220from_js_value!(i32);
221to_js_value!(i64);
222to_js_value!(i128);
223to_js_value!(u8);
224from_js_value!(u8);
225to_js_value!(u16);
226from_js_value!(u16);
227to_js_value!(u32);
228from_js_value!(u32);
229to_js_value!(u64);
230to_js_value!(u128);
231to_js_value!(f32);
232from_js_value!(f32);
233to_js_value!(f64);
234to_js_value!(usize);
235from_js_value!(usize);
236to_js_value!(isize);
237from_js_value!(isize);
238impl From<&str> for JsValue {
239    fn from(val: &str) -> Self {
240        cast! {(String => JsValue) val.to_string()}
241    }
242}
243impl From<&String> for JsValue {
244    fn from(val: &String) -> Self {
245        cast! {(String => JsValue) val.clone()}
246    }
247}
248to_js_value!(String);
249to_js_value!(());
250from_js_value!(());
251
252/// Closure type for passing Rust closures to JavaScript.
253pub struct Closure<T: ?Sized> {
254    // careful: must be Box<T> not just T because unsized PhantomData
255    // seems to have weird interaction with Pin<>
256    _phantom: core::marker::PhantomData<Box<T>>,
257    pub(crate) value: JsValue,
258}
259
260impl<T: ?Sized> Closure<T> {
261    pub fn new<M, F: IntoClosure<M, Self>>(f: F) -> Self {
262        f.into_closure()
263    }
264
265    /// Create a `Closure` from a function that can only be called once.
266    ///
267    /// Since we have no way of enforcing that JS cannot attempt to call this
268    /// `FnOnce` more than once, this produces a `Closure<dyn FnMut(A...) -> R>`
269    /// that will panic if called more than once.
270    pub fn once<F, M>(fn_once: F) -> Closure<T>
271    where
272        F: WasmClosureFnOnce<T, M>,
273    {
274        fn_once.into_closure()
275    }
276
277    /// Forgets the closure, leaking it.
278    pub fn forget(self) {
279        core::mem::forget(self);
280    }
281
282    /// Wrap a raw closure. Only for use by generated code.
283    pub(crate) fn wrap_encode_decode<FnPtr>(
284        encode_decode: impl Fn(&mut DecodedData, &mut EncodedData) + 'static,
285    ) -> Self
286    where
287        CallbackKey<FnPtr>: BinaryEncode + EncodeTypeDef,
288    {
289        let key = insert_object(RustCallback::new_fn(encode_decode));
290        // Use wbg_cast with CallbackKey so param encodes as Callback type (JS creates RustFunction)
291        // Return type is Closure which encodes as HeapRef (JS inserts into heap)
292        crate::__rt::wbg_cast::<CallbackKey<FnPtr>, crate::Closure<T>>(CallbackKey::new(key))
293    }
294
295    /// Wrap a raw closure. Only for use by generated code.
296    pub(crate) fn wrap_encode_decode_mut<FnPtr>(
297        encode_decode: impl FnMut(&mut DecodedData, &mut EncodedData) + 'static,
298    ) -> Self
299    where
300        CallbackKey<FnPtr>: BinaryEncode + EncodeTypeDef,
301    {
302        let key = insert_object(RustCallback::new_fn_mut(encode_decode));
303        // Use wbg_cast with CallbackKey so param encodes as Callback type (JS creates RustFunction)
304        // Return type is Closure which encodes as HeapRef (JS inserts into heap)
305        crate::__rt::wbg_cast::<CallbackKey<FnPtr>, crate::Closure<T>>(CallbackKey::new(key))
306    }
307}
308
309/// A trait for converting an `FnOnce(A...) -> R` into a `Closure<dyn FnMut(A...) -> R>`.
310#[doc(hidden)]
311pub trait WasmClosureFnOnce<T: ?Sized, M>: Sized + 'static {
312    fn into_closure(self) -> Closure<T>;
313}
314
315impl<T: ?Sized> AsRef<JsValue> for Closure<T> {
316    fn as_ref(&self) -> &JsValue {
317        &self.value
318    }
319}
320
321impl<T: ?Sized> core::fmt::Debug for Closure<T> {
322    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
323        f.debug_struct("Closure")
324            .field("value", &self.value)
325            .finish()
326    }
327}
328
329/// Trait for closure types that can be wrapped and passed to JavaScript.
330/// This trait is implemented for all `dyn FnMut(...)` variants.
331pub trait WasmClosure<M> {
332    /// Create a Closure from a boxed closure.
333    fn into_js_closure(boxed: Box<Self>) -> Closure<Self>;
334}
335
336impl<T: ?Sized> Closure<T> {
337    /// Wrap a boxed closure to create a `Closure`.
338    ///
339    /// This is the classic wasm-bindgen API for creating closures from boxed trait objects.
340    pub fn wrap<M>(data: Box<T>) -> Closure<T>
341    where
342        T: WasmClosure<M>,
343    {
344        T::into_js_closure(data)
345    }
346
347    /// Converts the `Closure` into a `JsValue`.
348    pub fn into_js_value(self) -> JsValue {
349        let value = core::mem::ManuallyDrop::new(self);
350        // Clone the value to get ownership without triggering drop
351        value.value.clone()
352    }
353
354    /// Create a `Closure` from a function that can only be called once,
355    /// and return the underlying `JsValue` directly.
356    ///
357    /// This is a convenience method that combines `once` and `into_js_value`.
358    pub fn once_into_js<F, M>(fn_once: F) -> JsValue
359    where
360        F: WasmClosureFnOnce<T, M>,
361        T: Sized,
362    {
363        Closure::once(fn_once).into_js_value()
364    }
365}
366
367use alloc::boxed::Box;
368use alloc::string::{String, ToString};
369use core::ops::{Deref, DerefMut};
370// Re-export core types
371pub use cast::JsCast;
372pub use lazy::JsThreadLocal;
373pub use value::JsValue;
374
375/// A wrapper type around slices and vectors for binding the `Uint8ClampedArray` in JS.
376///
377/// Supported inner types:
378/// * `Clamped<&[u8]>`
379/// * `Clamped<&mut [u8]>`
380/// * `Clamped<Vec<u8>>`
381#[derive(Copy, Clone, PartialEq, Debug, Eq)]
382pub struct Clamped<T>(pub T);
383
384impl<T> Deref for Clamped<T> {
385    type Target = T;
386    fn deref(&self) -> &T {
387        &self.0
388    }
389}
390
391impl<T> DerefMut for Clamped<T> {
392    fn deref_mut(&mut self) -> &mut T {
393        &mut self.0
394    }
395}
396
397/// A JavaScript Error object.
398///
399/// This type is used to create JavaScript Error objects that can be thrown or returned.
400#[derive(Debug)]
401#[repr(transparent)]
402pub struct JsError {
403    value: JsValue,
404}
405
406impl JsError {
407    /// Create a new JavaScript Error with the given message.
408    pub fn new(message: &str) -> Self {
409        JsError {
410            value: __wry_call_js_function!(
411                "(msg) => new Error(msg)",
412                fn(&str) -> JsValue,
413                (message)
414            ),
415        }
416    }
417}
418
419impl From<JsError> for JsValue {
420    fn from(e: JsError) -> Self {
421        e.value
422    }
423}
424
425impl<T> From<Option<T>> for JsValue
426where
427    T: Into<JsValue>,
428{
429    fn from(s: Option<T>) -> JsValue {
430        match s {
431            Some(s) => s.into(),
432            None => JsValue::undefined(),
433        }
434    }
435}
436
437impl AsRef<JsValue> for JsError {
438    fn as_ref(&self) -> &JsValue {
439        &self.value
440    }
441}
442
443impl core::fmt::Display for JsError {
444    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
445        write!(f, "JsError")
446    }
447}
448
449impl core::error::Error for JsError {}
450
451impl JsCast for JsError {
452    fn instanceof(val: &JsValue) -> bool {
453        crate::js_helpers::js_is_error(val)
454    }
455
456    fn unchecked_from_js(val: JsValue) -> Self {
457        JsError { value: val }
458    }
459
460    fn unchecked_from_js_ref(val: &JsValue) -> &Self {
461        // SAFETY: #[repr(transparent)] guarantees same layout
462        unsafe { &*(val as *const JsValue as *const JsError) }
463    }
464}
465
466// Re-export commonly used items
467pub use batch::batch;
468pub use encode::{BatchableResult, BinaryDecode, BinaryEncode, EncodeTypeDef};
469pub use function::JSFunction;
470pub use ipc::{DecodeError, DecodedData, EncodedData};
471
472// Re-export the macros
473pub use wry_bindgen_macro::link_to;
474pub use wry_bindgen_macro::wasm_bindgen;
475
476// Re-export inventory for macro use
477pub use inventory;
478
479use crate::encode::{CallbackKey, IntoClosure};
480use crate::function::RustCallback;
481use crate::object_store::insert_object;
482
483// Re-export function registry types
484pub use function_registry::{
485    InlineJsModule, JsClassMemberKind, JsClassMemberSpec, JsExportSpec, JsFunctionSpec,
486    LazyJsFunction,
487};
488
489/// Macro to register and call a JavaScript function.
490///
491/// This macro encapsulates the common pattern of:
492/// 1. Creating a static JsFunctionSpec
493/// 2. Submitting it to inventory
494/// 3. Creating a LazyJsFunction with the given signature
495/// 4. Calling the function with the provided arguments
496///
497/// # Usage
498/// ```ignore
499/// __wry_call_js_function!("(a, b) => a + b", fn(i32, i32) -> i32, (x, y))
500/// ```
501#[macro_export]
502#[doc(hidden)]
503macro_rules! __wry_call_js_function {
504    ($js_code:expr, $fn_type:ty, ($($args:expr),*)) => {{
505        static __FUNC: $crate::LazyJsFunction<$fn_type> = $crate::__wry_submit_js_function!($js_code);
506
507        __FUNC.call($($args),*)
508    }};
509}
510
511/// Macro to register and call a JavaScript function.
512///
513/// This macro encapsulates the common pattern of:
514/// 1. Creating a static JsFunctionSpec
515/// 2. Submitting it to inventory
516/// 3. Creating a LazyJsFunction with the given signature
517///
518/// # Usage
519/// ```ignore
520/// __wry_submit_js_function!("(a, b) => a + b")
521/// ```
522#[macro_export]
523#[doc(hidden)]
524macro_rules! __wry_submit_js_function {
525    ($js_code:expr) => {{
526        static __SPEC: $crate::JsFunctionSpec =
527            $crate::JsFunctionSpec::new(|| $crate::alloc::format!($js_code));
528
529        $crate::inventory::submit! {
530            __SPEC
531        }
532
533        __SPEC.resolve_as()
534    }};
535}
536
537/// Extension trait for Option to unwrap or throw a JS error.
538/// This is API-compatible with wasm-bindgen's UnwrapThrowExt.
539pub trait UnwrapThrowExt<T>: Sized {
540    /// Unwrap the value or panic with a message.
541    fn unwrap_throw(self) -> T;
542
543    /// Unwrap the value or panic with a custom message.
544    fn expect_throw(self, message: &str) -> T;
545}
546
547impl<T> UnwrapThrowExt<T> for Option<T> {
548    fn unwrap_throw(self) -> T {
549        self.expect("called `Option::unwrap_throw()` on a `None` value")
550    }
551
552    fn expect_throw(self, message: &str) -> T {
553        self.expect(message)
554    }
555}
556
557impl<T, E> UnwrapThrowExt<T> for Result<T, E>
558where
559    E: core::fmt::Debug,
560{
561    fn unwrap_throw(self) -> T {
562        self.expect("called `Result::unwrap_throw()` on an `Err` value")
563    }
564
565    fn expect_throw(self, message: &str) -> T {
566        self.expect(message)
567    }
568}
569
570#[cold]
571#[inline(never)]
572pub fn throw_val(s: JsValue) -> ! {
573    panic!("{s:?}");
574}
575
576/// Throw a JS exception with the given message.
577///
578/// # Panics
579/// This function always panics when running outside of WASM.
580#[cold]
581#[inline(never)]
582pub fn throw_str(s: &str) -> ! {
583    panic!("cannot throw JS exception when running outside of wasm: {s}");
584}
585
586/// Returns the number of live externref objects.
587///
588/// # Panics
589/// This function always panics when running outside of WASM.
590pub fn externref_heap_live_count() -> u32 {
591    panic!("cannot introspect wasm memory when running outside of wasm")
592}
593
594/// Returns a handle to this Wasm instance's `WebAssembly.Module`.
595///
596/// # Panics
597/// This function always panics when running outside of WASM.
598pub fn module() -> JsValue {
599    panic!("cannot introspect wasm memory when running outside of wasm")
600}
601
602/// Returns a handle to this Wasm instance's `WebAssembly.Instance.prototype.exports`.
603///
604/// # Panics
605/// This function always panics when running outside of WASM.
606pub fn exports() -> JsValue {
607    panic!("cannot introspect wasm memory when running outside of wasm")
608}
609
610/// Returns a handle to this Wasm instance's `WebAssembly.Memory`.
611///
612/// # Panics
613/// This function always panics when running outside of WASM.
614pub fn memory() -> JsValue {
615    panic!("cannot introspect wasm memory when running outside of wasm")
616}
617
618/// Returns a handle to this Wasm instance's `WebAssembly.Table` (indirect function table).
619///
620/// # Panics
621/// This function always panics when running outside of WASM.
622pub fn function_table() -> JsValue {
623    panic!("cannot introspect wasm memory when running outside of wasm")
624}
625
626// Re-export extract_rust_handle from js_helpers
627pub use js_helpers::js_extract_rust_handle as extract_rust_handle;
628
629/// Prelude module for common imports
630pub mod prelude {
631    pub use crate::Clamped;
632    pub use crate::Closure;
633    pub use crate::JsError;
634    pub use crate::UnwrapThrowExt;
635    pub use crate::WasmClosure;
636    pub use crate::batch::batch;
637    pub use crate::cast::JsCast;
638    pub use crate::encode::{BatchableResult, BinaryDecode, BinaryEncode, EncodeTypeDef};
639    pub use crate::function::JSFunction;
640    pub use crate::lazy::JsThreadLocal;
641    pub use crate::value::JsValue;
642    pub use crate::wasm_bindgen;
643}