// Copyright (c) 2026 Cloudflare, Inc. // Licensed under the Apache 2.0 license found in the LICENSE file or at: // https://opensource.org/licenses/Apache-2.0 #![allow(clippy::allow_attributes)] //! Traits for converting between Rust and JavaScript values. //! //! # Supported Types //! //! ## Primitive Types //! //! | Rust Type | JavaScript Type | //! |-----------|-----------------| //! | `()` | `undefined` | //! | `String` | `string` | //! | `&str` | `string` | //! | `bool` | `boolean` | //! | `Number` | `number` | //! | `Option` | `T` or `undefined` | //! | `Nullable` | `T`, `null`, or `undefined` | //! | `Result` | `T` or throws | //! | `NonCoercible` | `T` (strict type checking) | //! | `T: Struct` | `object` | //! | `Vec` | `Array` | //! | `&[T]` | `Array` (parameter only) | //! //! ## `TypedArray` Types //! //! These specialized `Vec` and slice types map directly to JavaScript `TypedArray`s //! for efficient binary data transfer: //! //! | Rust Type | JavaScript Type | //! |-----------|-----------------| //! | `Vec` | `Uint8Array` | //! | `Vec` | `Uint16Array` | //! | `Vec` | `Uint32Array` | //! | `Vec` | `Int8Array` | //! | `Vec` | `Int16Array` | //! | `Vec` | `Int32Array` | //! | `Vec` | `Float32Array` | //! | `Vec` | `Float64Array` | //! | `Vec` | `BigInt64Array` | //! | `Vec` | `BigUint64Array` | //! | `&[u8]` | `Uint8Array` (parameter only) | //! | `&[u16]` | `Uint16Array` (parameter only) | //! | `&[u32]` | `Uint32Array` (parameter only) | //! | `&[i8]` | `Int8Array` (parameter only) | //! | `&[i16]` | `Int16Array` (parameter only) | //! | `&[i32]` | `Int32Array` (parameter only) | //! | `&[f32]` | `Float32Array` (parameter only) | //! | `&[f64]` | `Float64Array` (parameter only) | //! | `&[i64]` | `BigInt64Array` (parameter only) | //! | `&[u64]` | `BigUint64Array` (parameter only) | //! //! ## Integer Parameter Types //! //! Integer types (`u8`, `u16`, `u32`, `i8`, `i16`, `i32`) can be used as method //! parameters. JavaScript numbers are converted via truncation: //! //! - Values are truncated toward zero (e.g., `3.7` → `3`, `-2.9` → `-2`) //! - Out-of-range values wrap (e.g., `256.0` → `0` for `u8`) //! - `NaN` becomes `0` //! //! For strict validation, wrap parameters in `NonCoercible` or validate manually. use crate::Error; use crate::Lock; use crate::NonCoercible; use crate::Nullable; use crate::Number; use crate::Type; use crate::v8; use crate::v8::ToLocalValue; // ============================================================================= // Traced trait — GC tracing for field types // ============================================================================= /// Trait for types that can be visited during V8 garbage collection tracing. /// /// Every type that can appear as a field in a `#[jsg_resource]` struct must /// implement `Traced`. The generated `Traced::trace` body calls /// `Traced::trace` on every field — types with no GC-visible references /// simply use the default no-op implementation. /// /// # Built-in implementations /// /// - **Primitives** (`bool`, `String`, integers, floats, `()`) — no-op. /// - **`Option`**, **`Nullable`** — delegates to the inner value if present. /// - **Collections** (`Vec`, `HashMap`, `BTreeMap`, `HashSet`, /// `BTreeSet`) — iterates elements/values and traces each. /// - **`Cell`** — reads through `as_ptr()` (sound under single-threaded GC). /// - **`jsg::Rc`** — visits the reference via `GcVisitor::visit_rc`. /// - **`jsg::Weak`** — no-op (weak refs don't keep targets alive). /// - **`jsg::v8::Global`** — visits via `GcVisitor::visit_global`. pub trait Traced { /// Visit GC-visible references held by this value. /// /// The default implementation is a no-op, suitable for types with no /// GC-visible references (primitives, plain data structs, etc.). fn trace(&self, _visitor: &mut crate::v8::GcVisitor) {} } /// Generates no-op `Traced` implementations for types with no GC-visible references. macro_rules! impl_traced_noop { ($($t:ty),* $(,)?) => { $(impl Traced for $t {})* }; } impl_traced_noop!( (), bool, String, crate::Error, u8, u16, u32, u64, usize, i8, i16, i32, i64, isize, f32, f64, crate::v8::ArrayBuffer, crate::v8::ArrayBufferView, crate::v8::BackingStore, crate::v8::BigInt64Array, crate::v8::BigUint64Array, crate::v8::Float32Array, crate::v8::Float64Array, crate::v8::Int8Array, crate::v8::Int16Array, crate::v8::Int32Array, crate::v8::Uint8Array, crate::v8::Uint16Array, crate::v8::Uint32Array, &str, Number, ); impl Traced for Option { fn trace(&self, visitor: &mut crate::v8::GcVisitor) { if let Some(inner) = self { inner.trace(visitor); } } } impl Traced for Nullable { fn trace(&self, visitor: &mut crate::v8::GcVisitor) { if let Self::Some(inner) = self { inner.trace(visitor); } } } impl Traced for NonCoercible { fn trace(&self, visitor: &mut crate::v8::GcVisitor) { self.as_ref().trace(visitor); } } impl Traced for Vec { fn trace(&self, visitor: &mut crate::v8::GcVisitor) { for item in self { item.trace(visitor); } } } impl Traced for std::collections::HashMap { fn trace(&self, visitor: &mut crate::v8::GcVisitor) { for value in self.values() { value.trace(visitor); } } } impl Traced for std::collections::BTreeMap { fn trace(&self, visitor: &mut crate::v8::GcVisitor) { for value in self.values() { value.trace(visitor); } } } impl Traced for std::collections::HashSet { fn trace(&self, visitor: &mut crate::v8::GcVisitor) { for item in self { item.trace(visitor); } } } impl Traced for std::collections::BTreeSet { fn trace(&self, visitor: &mut crate::v8::GcVisitor) { for item in self { item.trace(visitor); } } } impl Traced for std::cell::Cell { fn trace(&self, visitor: &mut crate::v8::GcVisitor) { // SAFETY: V8 GC tracing is single-threaded within an isolate and never // re-entrant on the same object during a single GC cycle. We only read // through the pointer. unsafe { (*self.as_ptr()).trace(visitor); } } } impl Traced for std::cell::RefCell { fn trace(&self, visitor: &mut crate::v8::GcVisitor) { // Use `try_borrow()` to avoid panicking across the FFI boundary if a // mutable borrow is active during GC. if let Ok(inner) = self.try_borrow() { inner.trace(visitor); } } } impl Traced for std::rc::Rc { fn trace(&self, visitor: &mut crate::v8::GcVisitor) { (**self).trace(visitor); } } // ============================================================================= // ToJS trait (Rust → JavaScript) // ============================================================================= /// Trait for converting Rust values to JavaScript. /// /// Provides Rust → JavaScript conversion. pub trait ToJS: Sized { /// Converts this Rust value into a JavaScript value. fn to_js<'a, 'b>(self, lock: &'a mut Lock) -> v8::Local<'b, v8::Value> where 'b: 'a; } // ============================================================================= // FromJS trait (JavaScript → Rust) // ============================================================================= /// Trait for converting JavaScript values to Rust. /// /// Provides JS → Rust conversion. The `try_unwrap` method is used by macros /// to unwrap function parameters with proper error handling. pub trait FromJS: Sized { type ResultType; /// Converts a JavaScript value into this Rust type. fn from_js(lock: &mut Lock, value: v8::Local) -> Result; /// Tries to convert only if the JavaScript type matches exactly. /// Returns `None` if the type doesn't match, `Some(result)` if conversion was attempted. /// Used by `#[jsg_oneof]` macro to try each variant without coercion. fn try_from_js_exact( lock: &mut Lock, value: &v8::Local, ) -> Option> where Self: Type, { if Self::is_exact(value) { Some(Self::from_js(lock, value.clone())) } else { None } } } // ============================================================================= // Primitive type implementations // ============================================================================= /// Implements `Type`, `ToJS`, and `FromJS` for primitive types. macro_rules! impl_primitive { { $type:ty, $class_name:literal, $is_exact:ident, $unwrap_fn:ident } => { impl Type for $type { fn class_name() -> &'static str { $class_name } fn is_exact(value: &v8::Local) -> bool { value.$is_exact() } } impl ToJS for $type { fn to_js<'a, 'b>(self, lock: &'a mut Lock) -> v8::Local<'b, v8::Value> where 'b: 'a, { self.to_local(lock) } } impl FromJS for $type { type ResultType = Self; fn from_js(lock: &mut Lock, value: v8::Local) -> Result { // SAFETY: The isolate is locked and value is a valid V8 local handle. Ok(unsafe { v8::ffi::$unwrap_fn(lock.isolate().as_ffi(), value.into_ffi()) }) } } }; } impl_primitive!(std::string::String, "string", is_string, unwrap_string); impl_primitive!(bool, "boolean", is_boolean, unwrap_boolean); // Number implementation for JavaScript numbers impl Type for Number { fn class_name() -> &'static str { "number" } fn is_exact(value: &v8::Local) -> bool { value.is_number() } } impl ToJS for Number { fn to_js<'a, 'b>(self, lock: &'a mut Lock) -> v8::Local<'b, v8::Value> where 'b: 'a, { self.to_local(lock) } } impl FromJS for Number { type ResultType = Self; fn from_js(lock: &mut Lock, value: v8::Local) -> Result { // SAFETY: The isolate is locked and value is a valid V8 local handle. let f64_value = unsafe { v8::ffi::unwrap_number(lock.isolate().as_ffi(), value.into_ffi()) }; Ok(Self::new(f64_value)) } } // Special implementation for &str - allows functions to accept &str parameters // by converting JavaScript strings to owned Strings, then borrowing. // The macro handles passing &arg instead of arg for reference types. impl Type for &str { fn class_name() -> &'static str { "string" } fn is_exact(value: &v8::Local) -> bool { value.is_string() } } impl FromJS for &str { type ResultType = String; fn from_js(lock: &mut Lock, value: v8::Local) -> Result { // SAFETY: The isolate is locked and value is a valid V8 local handle. Ok(unsafe { v8::ffi::unwrap_string(lock.isolate().as_ffi(), value.into_ffi()) }) } } impl> FromJS for &T { type ResultType = T; fn from_js(lock: &mut Lock, value: v8::Local) -> Result { T::from_js(lock, value) } } // Slice type - allows functions to accept &[T] parameters. // JavaScript arrays are converted to Vec, then borrowed as &[T] by the macro. impl Type for &[T] { fn class_name() -> &'static str { "Array" } fn is_exact(value: &v8::Local) -> bool { value.is_array() } } impl> FromJS for &[T] { type ResultType = Vec; fn from_js(lock: &mut Lock, value: v8::Local) -> Result { Vec::::from_js(lock, value) } } // Integer types - JavaScript numbers are IEEE 754 doubles (f64) // // Conversion behavior: // - Values are truncated toward zero (e.g., 3.7 → 3, -2.9 → -2) // - Values outside the target type's range wrap around (e.g., 256.0 → 0 for u8) // - NaN becomes 0 // - Infinity wraps to 0 for unsigned types, or type MIN/MAX for signed types // // This matches JavaScript's behavior for TypedArray element assignment. // For strict validation, use `NonCoercible` or validate in your method. macro_rules! impl_integer_from_js { ($($type:ty),*) => { $( impl FromJS for $type { type ResultType = Self; #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn from_js(lock: &mut Lock, value: v8::Local) -> Result { // SAFETY: The isolate is locked and value is a valid V8 local handle. let num = unsafe { v8::ffi::unwrap_number(lock.isolate().as_ffi(), value.into_ffi()) }; Ok(num as $type) } } )* }; } impl_integer_from_js!(u8, u16, u32, i8, i16, i32); // ============================================================================= // Wrapper type implementations // ============================================================================= impl ToJS for () { fn to_js<'a, 'b>(self, lock: &'a mut Lock) -> v8::Local<'b, v8::Value> where 'b: 'a, { v8::Local::::undefined(lock) } } impl ToJS for Option { fn to_js<'a, 'b>(self, lock: &'a mut Lock) -> v8::Local<'b, v8::Value> where 'b: 'a, { match self { Some(value) => value.to_js(lock), None => v8::Local::::undefined(lock), } } } impl ToJS for NonCoercible { fn to_js<'a, 'b>(self, lock: &'a mut Lock) -> v8::Local<'b, v8::Value> where 'b: 'a, { self.into_inner().to_js(lock) } } impl ToJS for Nullable { fn to_js<'a, 'b>(self, lock: &'a mut Lock) -> v8::Local<'b, v8::Value> where 'b: 'a, { match self { Self::Some(value) => value.to_js(lock), Self::Null => v8::Local::::null(lock), Self::Undefined => v8::Local::::undefined(lock), } } } impl FromJS for Option { type ResultType = Option; fn from_js(lock: &mut Lock, value: v8::Local) -> Result { if value.is_null() { let msg = format!("Expected {} or undefined but got null", T::class_name()); Err(Error::new_type_error(msg)) } else if value.is_undefined() { Ok(None) } else { Ok(Some(T::from_js(lock, value)?)) } } } impl FromJS for NonCoercible { type ResultType = NonCoercible; fn from_js(lock: &mut Lock, value: v8::Local) -> Result { if !T::is_exact(&value) { let error_msg = format!( "Expected a {} value but got {}", T::class_name(), value.type_of() ); return Err(Error::new_type_error(error_msg)); } Ok(::new(T::from_js(lock, value)?)) } } impl FromJS for Nullable { type ResultType = Nullable; fn from_js(lock: &mut Lock, value: v8::Local) -> Result { if value.is_null() { Ok(Nullable::Null) } else if value.is_undefined() { Ok(Nullable::Undefined) } else { Ok(Nullable::Some(T::from_js(lock, value)?)) } } } // ============================================================================= // Array type implementations (Vec) // ============================================================================= impl Type for Vec { fn class_name() -> &'static str { "Array" } fn is_exact(value: &v8::Local) -> bool { value.is_array() } } impl ToJS for Vec { fn to_js<'a, 'b>(self, lock: &'a mut Lock) -> v8::Local<'b, v8::Value> where 'b: 'a, { let mut array = v8::Array::new(lock, self.len()); for (i, item) in self.into_iter().enumerate() { array.set(i, item.to_js(lock)); } array.into() } } impl> FromJS for Vec { type ResultType = Self; fn from_js(lock: &mut Lock, value: v8::Local) -> Result { let type_name = value.type_of(); let array = value .try_as::() .ok_or_else(|| Error::new_type_error(format!("Expected Array but got {type_name}")))?; let globals = array.iterate(); let mut result = Self::with_capacity(globals.len()); for global in globals { let local = global.as_local(lock); result.push(T::from_js(lock, local)?); } Ok(result) } } // ============================================================================= // TypedArray implementations // ============================================================================= // // This macro generates implementations for TypedArray types: // - `v8::*Array` marker types: `Type` trait for type-safe V8 handles // - `Vec`: `Type`, `ToJS`, `FromJS` for bidirectional conversion // - `&[T]`: `Type`, `FromJS` for parameter-only conversion (delegates to Vec) // // Supported mappings: // - `Vec` / `&[u8]` <-> `Uint8Array` // - `Vec` / `&[u16]` <-> `Uint16Array` // - `Vec` / `&[u32]` <-> `Uint32Array` // - `Vec` / `&[i8]` <-> `Int8Array` // - `Vec` / `&[i16]` <-> `Int16Array` // - `Vec` / `&[i32]` <-> `Int32Array` // - `Vec` / `&[f32]` <-> `Float32Array` // - `Vec` / `&[f64]` <-> `Float64Array` // - `Vec` / `&[i64]` <-> `BigInt64Array` // - `Vec` / `&[u64]` <-> `BigUint64Array` macro_rules! impl_typed_array { ($elem:ty, $marker:ident, $is_check:ident, $new_fn:ident, $unwrap_fn:ident) => { impl Type for v8::$marker { fn class_name() -> &'static str { stringify!($marker) } fn is_exact(value: &v8::Local) -> bool { value.$is_check() } } impl Type for Vec<$elem> { fn class_name() -> &'static str { stringify!($marker) } fn is_exact(value: &v8::Local) -> bool { value.$is_check() } } impl Type for &[$elem] { fn class_name() -> &'static str { stringify!($marker) } fn is_exact(value: &v8::Local) -> bool { value.$is_check() } } impl ToJS for Vec<$elem> { fn to_js<'a, 'b>(self, lock: &'a mut Lock) -> v8::Local<'b, v8::Value> where 'b: 'a, { let isolate = lock.isolate(); // SAFETY: Lock guarantees the isolate is locked and a HandleScope is active. unsafe { v8::Local::from_ffi( isolate, v8::ffi::$new_fn(isolate.as_ffi(), self.as_ptr(), self.len()), ) } } } impl FromJS for Vec<$elem> { type ResultType = Self; fn from_js(lock: &mut Lock, value: v8::Local) -> Result { if !value.$is_check() { return Err(Error::new_type_error(format!( "Expected {} but got {}", stringify!($marker), value.type_of() ))); } // SAFETY: The isolate is locked and value is a valid V8 local handle of the correct TypedArray type. Ok(unsafe { v8::ffi::$unwrap_fn(lock.isolate().as_ffi(), value.into_ffi()) }) } } impl FromJS for &[$elem] { type ResultType = Vec<$elem>; fn from_js(lock: &mut Lock, value: v8::Local) -> Result, Error> { Vec::<$elem>::from_js(lock, value) } } }; } impl_typed_array!( u8, Uint8Array, is_uint8_array, local_new_uint8_array, unwrap_uint8_array ); impl_typed_array!( u16, Uint16Array, is_uint16_array, local_new_uint16_array, unwrap_uint16_array ); impl_typed_array!( u32, Uint32Array, is_uint32_array, local_new_uint32_array, unwrap_uint32_array ); impl_typed_array!( i8, Int8Array, is_int8_array, local_new_int8_array, unwrap_int8_array ); impl_typed_array!( i16, Int16Array, is_int16_array, local_new_int16_array, unwrap_int16_array ); impl_typed_array!( i32, Int32Array, is_int32_array, local_new_int32_array, unwrap_int32_array ); impl_typed_array!( f32, Float32Array, is_float32_array, local_new_float32_array, unwrap_float32_array ); impl_typed_array!( f64, Float64Array, is_float64_array, local_new_float64_array, unwrap_float64_array ); impl_typed_array!( i64, BigInt64Array, is_bigint64_array, local_new_bigint64_array, unwrap_bigint64_array ); impl_typed_array!( u64, BigUint64Array, is_biguint64_array, local_new_biguint64_array, unwrap_biguint64_array ); // ============================================================================= // ArrayBuffer, ArrayBufferView, SharedArrayBuffer // ============================================================================= // // These three types only implement `Type` (used by `NonCoercible` for type // checking). They do not have element-typed `ToJS`/`FromJS` conversions and // do not fit the `impl_typed_array!` macro. impl Type for v8::ArrayBuffer { fn class_name() -> &'static str { "ArrayBuffer" } fn is_exact(value: &v8::Local) -> bool { value.is_array_buffer() } } impl Type for v8::ArrayBufferView { fn class_name() -> &'static str { "ArrayBufferView" } fn is_exact(value: &v8::Local) -> bool { value.is_array_buffer_view() } }