// 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 use std::future::Future; use std::num::ParseIntError; use std::ops::Deref; pub mod feature_flags; pub mod macros; pub mod modules; pub mod resource; pub mod v8; mod wrappable; pub use feature_flags::FeatureFlags; pub use resource::Rc; pub use resource::Resource; pub use resource::Weak; pub use v8::ArrayBuffer; pub use v8::ArrayBufferView; pub use v8::BackingStore; pub use v8::BigInt64Array; pub use v8::BigUint64Array; pub use v8::Float32Array; pub use v8::Float64Array; pub use v8::GcVisitor; pub use v8::Int8Array; pub use v8::Int16Array; pub use v8::Int32Array; pub use v8::IsolatePtr; pub use v8::Uint8Array; pub use v8::Uint16Array; pub use v8::Uint32Array; pub use v8::ffi::ExceptionType; pub use wrappable::FromJS; pub use wrappable::ToJS; pub use wrappable::Traced; #[cxx::bridge(namespace = "workerd::rust::jsg")] mod ffi { extern "Rust" { type Realm; /// Create a fully-initialized Realm with feature flags. /// `feature_flags_data` is canonical (single-segment, no segment table) Cap'n Proto /// bytes produced by `capnp::canonicalize()` on the C++ side. #[expect(clippy::unnecessary_box_returns)] unsafe fn realm_create(isolate: *mut Isolate, feature_flags_data: &[u8]) -> Box; } unsafe extern "C++" { include!("workerd/rust/jsg/ffi.h"); type Isolate = crate::v8::ffi::Isolate; // Realm pub unsafe fn realm_from_isolate(isolate: *mut Isolate) -> *mut Realm; } } pub type Result = std::result::Result; impl From<&str> for ExceptionType { fn from(value: &str) -> Self { match value { "OperationError" => Self::OperationError, "DataError" => Self::DataError, "DataCloneError" => Self::DataCloneError, "InvalidAccessError" => Self::InvalidAccessError, "InvalidStateError" => Self::InvalidStateError, "InvalidCharacterError" => Self::InvalidCharacterError, "NotSupportedError" => Self::NotSupportedError, "SyntaxError" => Self::SyntaxError, "TimeoutError" => Self::TimeoutError, "TypeMismatchError" => Self::TypeMismatchError, "AbortError" => Self::AbortError, "NotFoundError" => Self::NotFoundError, "TypeError" => Self::TypeError, "RangeError" => Self::RangeError, "ReferenceError" => Self::ReferenceError, _ => Self::Error, } } } #[derive(Debug, Clone)] pub struct Error { pub name: ExceptionType, pub message: String, } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}: {}", self.name, self.message) } } /// Generates constructor methods for each `ExceptionType` variant. /// e.g., `new_type_error("message")` creates an Error with `ExceptionType::TypeError` macro_rules! impl_error_constructors { ($($variant:ident => $fn_name:ident),* $(,)?) => { impl Error { $( pub fn $fn_name(message: impl Into) -> Self { Self { name: ExceptionType::$variant, message: message.into(), } } )* } }; } impl_error_constructors! { OperationError => new_operation_error, DataError => new_data_error, DataCloneError => new_data_clone_error, InvalidAccessError => new_invalid_access_error, InvalidStateError => new_invalid_state_error, InvalidCharacterError => new_invalid_character_error, NotSupportedError => new_not_supported_error, SyntaxError => new_syntax_error, TimeoutError => new_timeout_error, TypeMismatchError => new_type_mismatch_error, AbortError => new_abort_error, NotFoundError => new_not_found_error, TypeError => new_type_error, Error => new_error, RangeError => new_range_error, ReferenceError => new_reference_error, } impl FromJS for Error { type ResultType = Self; /// Creates an Error from a V8 value (typically an exception). /// /// If the value is a native error, extracts the name and message properties. /// Otherwise, converts the value to a string for the message. fn from_js(lock: &mut Lock, value: v8::Local) -> Result { if value.is_native_error() { let obj: v8::Local = value.into(); let name = obj .get(lock, "name") .and_then(|v| String::from_js(lock, v).ok()); let message = obj .get(lock, "message") .and_then(|v| String::from_js(lock, v).ok()) .unwrap_or_else(|| "Unknown error".to_owned()); Ok(Self { name: name.map_or(ExceptionType::Error, |n| ExceptionType::from(n.as_str())), message, }) } else { Err(Self::new_type_error("Unknown error")) } } } impl Error { pub fn new(name: &str, message: &str) -> Self { Self { name: ExceptionType::from(name), message: message.to_owned(), } } /// Creates a V8 exception from this error. pub fn to_local<'a>(&self, isolate: v8::IsolatePtr) -> v8::Local<'a, v8::Value> { // SAFETY: isolate is valid and locked (guaranteed by caller). unsafe { v8::Local::from_ffi( isolate, v8::ffi::exception_create(isolate.as_ffi(), self.name, &self.message), ) } } } impl From for Error { fn from(err: ParseIntError) -> Self { Self::new_range_error(format!("Failed to parse integer: {err}")) } } /// A wrapper type that prevents automatic type coercion when unwrapping from JavaScript. /// /// JavaScript automatically coerces types in certain contexts. For instance, when a JavaScript /// API expects a string, calling it with the value `null` will result in the null being coerced /// into the string value `"null"`. /// /// `NonCoercible` can be used to disable automatic type coercion in APIs. For instance, /// `NonCoercible` can be used to accept a value only if the input is already a string. /// If the input is the value `null`, then an error is thrown rather than silently coercing to /// `"null"`. /// /// # Supported Types /// /// Any type implementing the [`Type`] trait can be used with `NonCoercible`. Built-in /// implementations include: /// /// - `NonCoercible` - only accepts JavaScript strings /// - `NonCoercible` - only accepts JavaScript booleans /// - `NonCoercible` - only accepts JavaScript numbers /// /// # Example /// /// ```ignore /// use jsg::NonCoercible; /// /// // This function will only accept actual strings, not values that can be coerced to strings /// #[jsg_method] /// pub fn process_string(&self, param: NonCoercible) -> Result<(), Error> { /// let s: &String = param.as_ref(); /// // or use Deref: let s: &str = &*param; /// // ... /// } /// ``` /// /// # Important Notes /// /// Using `NonCoercible` runs counter to Web IDL and general JavaScript API conventions. /// In nearly all cases, APIs should allow coercion to occur and should deal with the coerced /// input accordingly to avoid being a source of user confusion. Only use `NonCoercible` if /// you have a good reason to disable coercion. #[derive(Debug, Clone, PartialEq, Eq)] pub struct NonCoercible { value: T, } impl NonCoercible { /// Creates a new `NonCoercible` wrapper around the given value. pub fn new(value: T) -> Self { Self { value } } /// Consumes the wrapper and returns the inner value. pub fn into_inner(self) -> T { self.value } } impl From for NonCoercible { fn from(value: T) -> Self { Self::new(value) } } impl AsRef for NonCoercible { fn as_ref(&self) -> &T { &self.value } } impl Deref for NonCoercible { type Target = T; fn deref(&self) -> &Self::Target { &self.value } } /// A wrapper type for JavaScript numbers (IEEE 754 double-precision floats). /// /// `Number` represents JavaScript's `number` type, which is always a 64-bit /// floating-point value. This wrapper type is used instead of raw `f64` to /// distinguish between JavaScript numbers and Rust's `f64` type used for /// `Float64Array` elements. /// /// # Usage /// /// Use `Number` when you need to accept or return JavaScript numbers in your API: /// /// ```ignore /// use jsg::Number; /// /// #[jsg_method] /// pub fn add(&self, a: Number, b: Number) -> Number { /// Number::new(a.value() + b.value()) /// } /// ``` /// /// # Type Mapping /// /// | Rust Type | JavaScript Type | /// |-----------|-----------------| /// | `jsg::Number` | `number` | /// | `f64` | Used for `Float64Array` elements | /// | `Vec` | `Float64Array` | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)] pub struct Number { value: f64, } impl Number { /// The largest integer that can be represented exactly in JavaScript (2^53 - 1). /// /// [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER) pub const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0; // 2^53 - 1 /// The smallest integer that can be represented exactly in JavaScript (-(2^53 - 1)). /// /// [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER) pub const MIN_SAFE_INTEGER: f64 = -9_007_199_254_740_991.0; // -(2^53 - 1) /// Creates a new `Number` from an `f64` value. #[inline] pub fn new(value: f64) -> Self { Self { value } } /// Returns the underlying `f64` value. #[inline] pub fn value(&self) -> f64 { self.value } /// Consumes the wrapper and returns the inner `f64` value. #[inline] pub fn into_inner(self) -> f64 { self.value } /// Determines whether the value is a finite number. /// /// Returns `true` if the value is finite (not `Infinity`, `-Infinity`, or `NaN`). /// /// [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isFinite) #[inline] pub fn is_finite(&self) -> bool { self.value.is_finite() } /// Determines whether the value is an integer. /// /// Returns `true` if the value is finite and has no fractional part. /// /// [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger) #[inline] #[expect(clippy::float_cmp)] // Exact comparison is correct here - we want trunc(x) == x pub fn is_integer(&self) -> bool { self.value.is_finite() && self.value.trunc() == self.value } /// Determines whether the value is `NaN`. /// /// This is more robust than the global `isNaN()` because it doesn't coerce /// the value to a number first. /// /// [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN) #[inline] pub fn is_nan(&self) -> bool { self.value.is_nan() } /// Determines whether the value is a safe integer. /// /// A safe integer is an integer that: /// - Can be exactly represented as an IEEE-754 double precision number /// - Has an IEEE-754 representation that cannot be the result of rounding any other integer /// /// Safe integers range from -(2^53 - 1) to 2^53 - 1, inclusive. /// /// [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isSafeInteger) #[inline] pub fn is_safe_integer(&self) -> bool { self.is_integer() && self.value >= Self::MIN_SAFE_INTEGER && self.value <= Self::MAX_SAFE_INTEGER } } impl From for Number { fn from(value: f64) -> Self { Self::new(value) } } impl From for f64 { fn from(num: Number) -> Self { num.value } } impl From for Number { fn from(value: i32) -> Self { Self::new(f64::from(value)) } } impl From for Number { fn from(value: u32) -> Self { Self::new(f64::from(value)) } } impl std::fmt::Display for Number { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.value) } } /// A wrapper type that accepts `null`, `undefined`, or a value of type `T`. /// /// `Nullable` is similar to `Option` but also accepts `undefined` as a null-ish value. /// This is useful for JavaScript APIs where both `null` and `undefined` represent /// the absence of a value. /// /// # Behavior /// /// - `null` → `Nullable::Null` /// - `undefined` → `Nullable::Undefined` /// - `T` → `Nullable::Some(T)` /// /// # Example /// /// ```ignore /// use jsg::Nullable; /// /// #[jsg_method] /// pub fn process(&self, value: Nullable) -> Result<(), Error> { /// match value { /// Nullable::Some(s) => println!("Got value: {}", s), /// Nullable::Null => println!("Got null"), /// Nullable::Undefined => println!("Got undefined"), /// } /// Ok(()) /// } /// ``` #[derive(Debug, Clone, PartialEq, Eq)] pub enum Nullable { Some(T), Null, Undefined, } impl Nullable { /// Returns `true` if the nullable contains a value. pub fn is_some(&self) -> bool { matches!(self, Self::Some(_)) } /// Returns `true` if the nullable is `Null`. pub fn is_null(&self) -> bool { matches!(self, Self::Null) } /// Returns `true` if the nullable is `Undefined`. pub fn is_undefined(&self) -> bool { matches!(self, Self::Undefined) } /// Returns `true` if the nullable is `Null` or `Undefined`. pub fn is_null_or_undefined(&self) -> bool { matches!(self, Self::Null | Self::Undefined) } /// Returns a reference to the contained value, or `None` if null or undefined. pub fn as_ref(&self) -> Option<&T> { match self { Self::Some(v) => Some(v), Self::Null | Self::Undefined => None, } } } impl From> for Nullable { fn from(opt: Option) -> Self { match opt { Some(v) => Self::Some(v), None => Self::Null, } } } impl From> for Option { fn from(nullable: Nullable) -> Self { match nullable { Nullable::Some(v) => Some(v), Nullable::Null | Nullable::Undefined => None, } } } /// Proof that the V8 isolate is valid and exclusively locked by the current thread. /// /// A `Lock` instance can only be created when the C++ `jsg::Lock` (or equivalent) /// is held, meaning: /// - The `v8::Isolate` pointer is valid and has not been disposed. /// - The current thread holds the isolate lock (`v8::Locker` is active). /// - No other thread can enter the isolate concurrently. /// /// `Lock` is passed to resource methods and callbacks to perform V8 operations /// like creating objects, wrapping values, and accessing the `Realm`. It is /// analogous to `jsg::Lock&` in C++ JSG. /// /// `Lock` is neither `Send` nor `Sync` — it cannot escape the thread that /// created it. pub struct Lock { isolate: v8::IsolatePtr, } impl Lock { /// # Safety /// The caller must ensure that `args` is a valid pointer to `FunctionCallbackInfo`. pub unsafe fn from_args(args: *mut v8::ffi::FunctionCallbackInfo) -> Self { // SAFETY: args is a valid FunctionCallbackInfo pointer (guaranteed by caller). unsafe { Self::from_isolate_ptr(v8::ffi::fci_get_isolate(args)) } } /// Creates a Lock from a raw isolate pointer. /// /// # Safety /// The caller must ensure that `isolate` is a valid pointer to an `Isolate`. /// /// # Panics /// Panics if the isolate is not currently locked. pub unsafe fn from_isolate_ptr(isolate: *mut v8::ffi::Isolate) -> Self { // SAFETY: isolate pointer is valid (guaranteed by caller). let isolate = unsafe { v8::IsolatePtr::from_ffi(isolate) }; // SAFETY: Lock guarantees the isolate is valid assert!(unsafe { isolate.is_locked() }); Self { isolate } } /// Returns the isolate associated with this lock. pub fn isolate(&self) -> v8::IsolatePtr { self.isolate } pub fn new_object<'a>(&mut self) -> v8::Local<'a, v8::Object> { // SAFETY: Lock guarantees the isolate is valid and locked unsafe { v8::Local::from_ffi( self.isolate(), v8::ffi::local_new_object(self.isolate().as_ffi()), ) } } pub fn throw_error(&mut self, message: &str) { // SAFETY: Lock guarantees the isolate is valid and locked unsafe { v8::ffi::isolate_throw_error(self.isolate().as_ffi(), message) } } pub fn await_io(self, _fut: F, _callback: C) -> Result where F: Future, C: FnOnce(Self, I) -> Result, { unimplemented!("Lock::await_io is not yet implemented for Rust resources") } pub(crate) fn realm(&mut self) -> &mut Realm { // SAFETY: isolate is valid and locked (guaranteed by Lock); Realm pointer is valid. unsafe { &mut *crate::ffi::realm_from_isolate(self.isolate().as_ffi()) } } /// Returns the current worker's compatibility flags reader. /// /// ```ignore /// if lock.feature_flags().get_node_js_compat() { /// // Node.js compatibility behavior /// } /// ``` pub fn feature_flags(&mut self) -> compatibility_date_capnp::compatibility_flags::Reader<'_> { self.realm().feature_flags.reader() } /// Throws an error as a V8 exception. pub fn throw_exception(&mut self, err: &Error) { // SAFETY: isolate is valid and locked (guaranteed by Lock). unsafe { v8::ffi::isolate_throw_exception( self.isolate().as_ffi(), err.to_local(self.isolate()).into_ffi(), ); } } /// Throws an internal error as a V8 exception, matching `makeInternalError()` in C++. /// /// Generates a unique error reference ID, logs `internal_message` with the ID via /// `KJ_LOG(ERROR)` (reaching Sentry), and schedules a generic /// `"Error: internal error; reference = "` JS exception on the isolate so the /// internal message is never exposed to JavaScript. pub fn throw_internal_error(&mut self, internal_message: &str) { // SAFETY: isolate is valid and locked (guaranteed by Lock). unsafe { v8::ffi::isolate_throw_internal_error(self.isolate().as_ffi(), internal_message); } } /// Signals that JavaScript execution on this isolate should be terminated immediately. /// /// Calls `IsolateBase::TerminateExecution()`, so V8 raises an /// uncatchable termination exception that unwinds all JS call frames back /// to the top-level C++ entry point. /// /// This mirrors what C++ `KJ_ASSERT` / `KJ_FAIL_ASSERT` effectively do when they fire /// inside an isolate context: they abort further JS execution rather than letting the /// isolate continue in a potentially inconsistent state. pub fn terminate_execution(&mut self) { // SAFETY: isolate is valid and locked (guaranteed by Lock). unsafe { v8::ffi::isolate_terminate_execution(self.isolate().as_ffi()); } } } /// Provides metadata about Rust types exposed to JavaScript. /// /// This trait provides type information used for error messages, memory tracking, /// and type validation (for `NonCoercible`). The actual conversion logic is in /// `ToJS` (Rust → JS) and `FromJS` (JS → Rust). /// /// TODO: Implement `memory_info(jsg::MemoryTracker)` pub trait Type: Sized { /// The JavaScript class name for this type (used in error messages). fn class_name() -> &'static str; /// Same as jsgGetMemorySelfSize fn memory_self_size() -> usize { std::mem::size_of::() } /// Returns true if the V8 value is exactly this type (no coercion). /// Used by `NonCoercible` to reject values that would require coercion. fn is_exact(value: &v8::Local) -> bool; } /// Represents a constant value that can be exposed to JavaScript. pub enum ConstantValue { Number(f64), } macro_rules! impl_constant_value_from_lossless { ($($t:ty),*) => { $( impl From<$t> for ConstantValue { fn from(v: $t) -> Self { Self::Number(f64::from(v)) } } )* }; } impl_constant_value_from_lossless!(i8, i16, i32, u8, u16, u32, f32, f64); // i64/u64 can lose precision in f64 (52-bit mantissa), but JavaScript numbers are // always f64 so this is inherent to the language boundary. impl From for ConstantValue { #[expect(clippy::cast_precision_loss)] fn from(v: i64) -> Self { Self::Number(v as f64) } } impl From for ConstantValue { #[expect(clippy::cast_precision_loss)] fn from(v: u64) -> Self { Self::Number(v as f64) } } /// Where a [`Member::Property`] is attached on the JavaScript object. /// /// This is a re-export of the CXX bridge type in [`v8::ffi`] so that callers /// do not need to import `jsg::v8::ffi` directly. The three variants behave /// as follows: /// /// - `Prototype` — accessor on the prototype chain; enumerable. /// - `Instance` — own accessor on every instance; enumerable. /// - `Inspect` — symbol-keyed on the prototype; hidden from normal enumeration, /// surfaced by `node:util` `inspect()`. pub use crate::v8::ffi::PropertyKind; pub enum Member { Constructor { callback: unsafe extern "C" fn(*mut v8::ffi::FunctionCallbackInfo), }, Method { name: String, callback: unsafe extern "C" fn(*mut v8::ffi::FunctionCallbackInfo), }, /// A property accessor with configurable placement. `setter_callback = None` /// makes the property read-only; `Inspect` properties are always read-only. Property { name: String, kind: PropertyKind, getter_callback: unsafe extern "C" fn(*mut v8::ffi::FunctionCallbackInfo), /// `None` for read-only properties. setter_callback: Option, }, StaticMethod { name: String, callback: unsafe extern "C" fn(*mut v8::ffi::FunctionCallbackInfo), }, StaticConstant { name: String, value: ConstantValue, }, } /// Trait for types that participate in V8 garbage collection as tracked resources. /// /// Extends [`Traced`] (which provides the `trace` method for visiting nested /// GC-visible references) with a class name for heap snapshot tooling. /// /// `#[jsg_resource]` auto-derives both `Traced` and `GarbageCollected`. /// Use `#[jsg_resource(custom_trace)]` to suppress the generated `Traced` /// impl and provide your own. pub trait GarbageCollected: Traced { /// Class name for heap snapshots / debugging. /// /// Returns a `&'static CStr` — always a compile-time literal. This lets the C++ side /// construct a `kj::StringPtr` directly from the NUL-terminated pointer without any /// allocation or caching. Implementations must not access `self`. fn memory_name(&self) -> &'static std::ffi::CStr; } /// Rust types that are deep-copied into JavaScript as value types. /// /// Unlike resource types, struct types are copied entirely into JavaScript objects with no /// further Rust involvement after wrapping. This is analogous to `JSG_STRUCT` in C++ JSG. pub trait Struct: Type {} /// Per-isolate state for Rust resources exposed to JavaScript. /// /// A Realm is created for each V8 isolate and stored in the isolate's data slot. It holds /// cached function templates and tracks resource instances. When all Rust `Ref` handles to a /// resource are dropped and no JS references remain, V8 GC collects the wrapper and the /// `CppgcShim` destruction triggers cleanup of the underlying `Wrappable`. pub struct Realm { isolate: v8::IsolatePtr, pub(crate) resources: resource::Resources, /// Parsed `CompatibilityFlags` capnp message, initialized at construction. feature_flags: FeatureFlags, } impl Realm { /// Creates a new Realm with its feature flags. pub fn new(isolate: v8::IsolatePtr, feature_flags: FeatureFlags) -> Self { Self { isolate, resources: resource::Resources::default(), feature_flags, } } pub fn isolate(&self) -> v8::IsolatePtr { self.isolate } } impl Drop for Realm { fn drop(&mut self) { debug_assert!( // SAFETY: isolate pointer is valid (guaranteed by Realm construction). unsafe { self.isolate.is_locked() }, "Realm must be dropped while holding the isolate lock" ); } } #[expect(clippy::unnecessary_box_returns)] unsafe fn realm_create(isolate: *mut v8::ffi::Isolate, feature_flags_data: &[u8]) -> Box { let feature_flags = FeatureFlags::from_bytes(feature_flags_data); // SAFETY: isolate pointer is valid (guaranteed by C++ caller). unsafe { Box::new(Realm::new(v8::IsolatePtr::from_ffi(isolate), feature_flags)) } } /// Executes `f`, catching any panic and converting it to a JS internal error. /// /// `extern "C"` V8 callbacks generated by `jsg-macros` call this so that a /// Rust panic is handled the same way as `KJ_ASSERT(false)` in C++ JSG /// handlers: the internal panic message is logged via `KJ_LOG(ERROR)` (reaching /// Sentry) and the JS caller receives a generic /// `"Error: internal error; reference = "` exception with a unique /// reference ID, keeping the internal message out of JS-visible output. /// /// After logging the error, `terminate_execution()` is called so that V8 raises /// an uncatchable termination exception and unwinds all JS call frames. This /// mirrors what C++ `KJ_ASSERT` effectively does inside an isolate context and /// closes the window where a panicked resource with partially-mutated `jsg::Rc` /// fields could be observed by a subsequent GC trace before isolate teardown. #[doc(hidden)] pub fn catch_panic(lock: &mut Lock, f: F) { // SAFETY: `f` is called from an `extern "C"` V8 callback generated by // jsg-macros. Raw pointers captured by `f` are valid for the entire // duration of the callback (V8 guarantees this), and V8 is // single-threaded, so there is no aliasing hazard on unwind. // `AssertUnwindSafe` is therefore correct here. if let Err(payload) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) { let msg = if let Some(s) = payload.downcast_ref::<&str>() { *s } else if let Some(s) = payload.downcast_ref::() { s.as_str() } else { "" }; lock.throw_internal_error(msg); lock.terminate_execution(); } }