// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. //! This module provides traits and macros that facilitate the conversion of Rust objects into v8 objects. //! //! The traits combine various behaviors, including converting values into v8 objects, efficiently placing them //! into a `v8::ReturnValue`, and handling potential conversion failures. //! //! These traits are a result of the intersection of two factors: whether the conversion can be directly stored //! in a return value and whether the conversion process can fail. //! //! For instance, the `RustToV8` and `RustToV8RetVal` traits respectively indicate the ability to convert a Rust value //! into a v8 object, and to store it in a `v8::ReturnValue`. Similarly, the `RustToV8Fallible` and `RustToV8RetValFallible` //! traits manage the same conversions that might encounter errors. //! //! To illustrate, consider the conversion of a `u32` value. It can be cheaply placed in a `v8::ReturnValue` //! without any conversion risk. Conversely, storing a string directly in a `v8::ReturnValue` (without using //! the `set(v8::Local)` function) is not possible. Moreover, converting a Rust string to a `v8::String` //! might fail, particularly if its size surpasses v8's limitations. //! //! The `#[op2]` proc macro maps the distinct calling patterns to the appropriate trait interfaces. //! This mapping accounts for conversion nature and potential error occurrences. When a conversion is inherently //! expected to succeed, `#[op2]` negates the necessity for explicit error checks, potentially boosting execution //! efficiency. Furthermore, `#[op2]` strives to leverage `v8::ReturnValue` setters when possible, eliminating //! the need for explicit object handle allocation. This optimization can favorably impact performance and //! memory management. use bytes::BytesMut; use libc::c_void; use std::borrow::Cow; use std::marker::PhantomData; use std::rc::Rc; /// Convert a value to a `v8::Local`, potentially allocating. pub trait RustToV8<'a> { fn to_v8(self, scope: &mut v8::HandleScope<'a>) -> v8::Local<'a, v8::Value>; } /// Convert a value to a `v8::Local`, not allocating. This is generally not used for /// anything other than `v8::Local`s themselves, so there is no additional macro support. pub trait RustToV8NoScope<'a> { fn to_v8(self) -> v8::Local<'a, v8::Value>; } /// Places a value in a `v8::ReturnValue`, non-allocating. pub trait RustToV8RetVal<'a>: RustToV8<'a> { fn to_v8_rv(self, rv: &mut v8::ReturnValue<'a>); } /// Convert a value to a `v8::Local`, potentially allocating or failing. pub trait RustToV8Fallible<'a> { fn to_v8_fallible( self, scope: &mut v8::HandleScope<'a>, ) -> serde_v8::Result>; } /// Places a value in a `v8::ReturnValue`, potentially allocating or failing. pub trait RustToV8RetValFallible<'a>: RustToV8Fallible<'a> { fn to_v8_rv_fallible( self, scope: &mut v8::HandleScope<'a>, rv: &mut v8::ReturnValue<'a>, ) -> serde_v8::Result<()>; } /// Implement [`RustToV8`] for an `Option` of [`RustToV8`]. impl<'a, T> RustToV8<'a> for Option where T: RustToV8<'a>, { #[inline(always)] fn to_v8(self, scope: &mut v8::HandleScope<'a>) -> v8::Local<'a, v8::Value> { if let Some(value) = self { value.to_v8(scope) } else { v8::null(scope).into() } } } /// Implement [`RustToV8RetVal`] for an `Option` of [`RustToV8RetVal`]. impl<'a, T> RustToV8RetVal<'a> for Option where T: RustToV8RetVal<'a>, { #[inline(always)] fn to_v8_rv(self, rv: &mut v8::ReturnValue<'a>) { if let Some(value) = self { value.to_v8_rv(rv) } else { rv.set_null() } } } /// Implement [`RustToV8Fallible`] for an `Option` of [`RustToV8Fallible`]. impl<'a, T> RustToV8Fallible<'a> for Option where T: RustToV8Fallible<'a>, { #[inline(always)] fn to_v8_fallible( self, scope: &mut v8::HandleScope<'a>, ) -> serde_v8::Result> { if let Some(value) = self { value.to_v8_fallible(scope) } else { Ok(v8::null(scope).into()) } } } /// Implement [`RustToV8RetValFallible`] for an `Option` of [`RustToV8RetValFallible`]. impl<'a, T> RustToV8RetValFallible<'a> for Option where T: RustToV8RetValFallible<'a>, { #[inline(always)] fn to_v8_rv_fallible( self, scope: &mut v8::HandleScope<'a>, rv: &mut v8::ReturnValue<'a>, ) -> serde_v8::Result<()> { if let Some(value) = self { value.to_v8_rv_fallible(scope, rv) } else { rv.set_null(); Ok(()) } } } /// Transparent marker struct that can be used to alter the conversion in various ways. The /// main uses of this are to mark something as being `serde`-serializable. This may also be /// useful to alter the serialization of various types. For example, a `NumberMarker` and /// `BigIntMarker` could control the serialization of `i64`/`u64`/`f64` as `v8::Number` or /// `v8::BigInt` objects. #[repr(transparent)] pub struct RustToV8Marker(T, PhantomData); impl From for RustToV8Marker { #[inline(always)] fn from(value: T) -> Self { RustToV8Marker(value, PhantomData) } } /// This struct should be serialized using `serde`. pub struct SerdeMarker; impl Marker for SerdeMarker {} /// This primitive should be serialized as an SMI. pub struct SmiMarker; impl Marker for SmiMarker {} /// This primitive should be serialized as a number. pub struct NumberMarker; impl Marker for NumberMarker {} /// This buffer should be serialized as an ArrayBuffer. pub struct ArrayBufferMarker; impl Marker for ArrayBufferMarker {} trait Marker {} /// Helper macro for [`RustToV8`] to reduce boilerplate. /// /// Implements a Rust-to-v8 conversion that cannot fail. Multiple types may be specified /// to apply the same implementation. /// /// ```no_compile /// to_v8!(bool: |value, scope| v8::Boolean::new(scope, value as _)); /// ``` macro_rules! to_v8 { (( $( $ty:ty ),+ ) : |$value:ident, $scope:ident| $block:expr) => { $( impl <'a> RustToV8<'a> for $ty { #[inline(always)] fn to_v8(self, scope: &mut v8::HandleScope<'a>) -> v8::Local<'a, v8::Value> { let $value = self; let $scope = scope; v8::Local::::from($block) } } )+ }; ($ty:ty : |$value:ident, $scope:ident| $block:expr) => { to_v8!(( $ty ) : |$value, $scope| $block); }; } /// Helper macro for [`RustToV8RetVal`] to reduce boilerplate. /// /// Implements a Rust-to-v8 conversion that cannot allocate or fail. Multiple types may be specified /// to apply the same implementation. Places the return value in a `v8::ReturnValue`. macro_rules! to_v8_retval { (( $( $ty:ty ),+ ) : |$value:ident, $rv:ident| $block:expr) => { $( impl <'a> RustToV8RetVal<'a> for $ty { #[inline(always)] fn to_v8_rv(self, rv: &mut v8::ReturnValue<'a>) { let $value = self; let $rv = rv; $block } } )+ }; ($ty:ty : |$rv:ident, $scope:ident| $block:expr) => { to_v8_retval!(( $ty ) : |$rv, $scope| $block); }; } /// Helper macro for [`RustToV8Fallible`] to reduce boilerplate. /// /// Implements a Rust-to-v8 conversion that can fail. Multiple types may be specified /// to apply the same implementation. /// /// ```no_compile /// to_v8_fallible!(serde_v8::V8Slice: |value, scope| { /// // Implementation /// Ok(result) /// }); /// ``` macro_rules! to_v8_fallible { (( $( $ty:ty ),+ ) : |$value:ident, $scope:ident| $block:expr) => { $( #[allow(clippy::needless_borrow)] impl <'a> RustToV8Fallible<'a> for $ty { #[inline(always)] fn to_v8_fallible(self, scope: &mut v8::HandleScope<'a>) -> serde_v8::Result> { let $value = self; let $scope = scope; let res = $block; match res { Ok(v) => Ok(v.into()), Err(err) => Err(err), } } } )+ }; ($ty:ty : |$value:ident, $scope:ident| $block:expr) => { to_v8_fallible!(( $ty ) : |$value, $scope| $block); }; } /// Helper macro for [`RustToV8RetValFallible`] to reduce boilerplate. /// /// Implements a Rust-to-v8 conversion that can fail. Multiple types may be specified /// to apply the same implementation. Will place the output in a `v8::ReturnValue`, but /// will not allocate. macro_rules! to_v8_retval_fallible { (( $( $ty:ty ),+ ) : |$value:ident, $scope: ident, $rv:ident| $block:expr) => { $( impl <'a> RustToV8RetValFallible<'a> for $ty { #[inline(always)] fn to_v8_rv_fallible(self, scope: &mut v8::HandleScope<'a>, rv: &mut v8::ReturnValue<'a>) -> serde_v8::Result<()>{ let $value = self; let $scope = scope; let $rv = rv; $block } } )+ }; ($ty:ty : |$value:ident, $scope: ident, $rv:ident| $block:expr) => { to_v8_retval_fallible!(( $ty ) : |$value, $scope, $rv| $block); }; } // // Integer/primitive conversions (cheap) // to_v8!((): |_value, scope| v8::null(scope)); to_v8_retval!((): |_value, rv| rv.set_null()); to_v8!(bool: |value, scope| v8::Boolean::new(scope, value as _)); to_v8_retval!(bool: |value, rv| rv.set_bool(value)); to_v8!((u8, u16, u32): |value, scope| v8::Integer::new_from_unsigned(scope, value as _)); to_v8_retval!((u8, u16, u32): |value, rv| rv.set_uint32(value as _)); to_v8!((i8, i16, i32): |value, scope| v8::Integer::new(scope, value as _)); to_v8_retval!((i8, i16, i32): |value, rv| rv.set_int32(value as _)); to_v8!((f32, f64): |value, scope| v8::Number::new(scope, value as _)); to_v8_retval!((f32, f64): |value, rv| rv.set_double(value as _)); // // Heavier primitives with no retval shortcuts // to_v8!((*const c_void, *mut c_void): |value, scope| { if value.is_null() { v8::Local::::from(v8::null(scope)) } else { v8::Local::::from(v8::External::new(scope, value as _)) } }); to_v8!((u64, usize): |value, scope| v8::BigInt::new_from_u64(scope, value as _)); to_v8!((i64, isize): |value, scope| v8::BigInt::new_from_i64(scope, value as _)); // // Strings // to_v8_fallible!((String, Cow<'a, str>, &'a str): |value, scope| v8::String::new(scope, &value).ok_or_else(|| serde_v8::Error::Message("failed to allocate string; buffer exceeds maximum length".into()))); to_v8_retval_fallible!((String, Cow<'a, str>, &'a str): |value, scope, rv| { if value.is_empty() { rv.set_empty_string(); } else { rv.set(value.to_v8_fallible(scope)?); } Ok(()) }); to_v8_fallible!(Cow<'a, [u8]>: |value, scope| v8::String::new_from_one_byte(scope, &value, v8::NewStringType::Normal).ok_or_else(|| serde_v8::Error::Message("failed to allocate string; buffer exceeds maximum length".into()))); to_v8_retval_fallible!(Cow<'a, [u8]>: |value, scope, rv| { if value.is_empty() { rv.set_empty_string(); } else { rv.set(value.to_v8_fallible(scope)?); } Ok(()) }); // // Buffers // to_v8_fallible!(serde_v8::V8Slice: |value, scope| { value.into_v8_local(scope).ok_or_else(|| serde_v8::Error::Message("failed to allocate array".into())) }); to_v8!(RustToV8Marker>: |value, scope| { value.0.into_v8_unsliced_arraybuffer_local(scope) }); to_v8_fallible!(serde_v8::V8Slice: |value, scope| { value.into_v8_local(scope).ok_or_else(|| serde_v8::Error::Message("failed to allocate array".into())) }); to_v8!(RustToV8Marker: |value, scope| { RustToV8Marker::::from(value.0.into_parts()).to_v8(scope) }); to_v8_fallible!(serde_v8::JsBuffer: |value, scope| { value.into_parts().to_v8_fallible(scope) }); to_v8!(RustToV8Marker>: |buf, scope| { let buf = buf.0; if buf.is_empty() { v8::ArrayBuffer::new(scope, 0) } else { let backing_store = v8::ArrayBuffer::new_backing_store_from_boxed_slice(buf); let backing_store_shared = backing_store.make_shared(); v8::ArrayBuffer::with_backing_store(scope, &backing_store_shared) } }); to_v8_fallible!(Box<[u8]>: |buf, scope| { let len = buf.len(); let ab = unsafe { v8::Local::cast(RustToV8Marker::::from(buf).to_v8(scope)) }; v8::Uint8Array::new(scope, ab, 0, len).ok_or_else(|| serde_v8::Error::Message("failed to allocate array".into())) }); to_v8!(RustToV8Marker>: |value, scope| { RustToV8Marker::::from(value.0.into_boxed_slice()).to_v8(scope) }); to_v8_fallible!(Vec: |value, scope| value.into_boxed_slice().to_v8_fallible(scope)); to_v8!(RustToV8Marker: |value, scope| { let value = value.0; let ptr = value.as_ptr(); let len = value.len() as _; let rc = Rc::into_raw(Rc::new(value)) as *const c_void; extern "C" fn drop_rc(_ptr: *mut c_void, _len: usize, data: *mut c_void) { // SAFETY: We know that data is a raw Rc from above unsafe { drop(Rc::::from_raw(data as _)) } } // SAFETY: We are using the BytesMut backing store here let backing_store_shared = unsafe { v8::ArrayBuffer::new_backing_store_from_ptr( ptr as _, len, drop_rc, rc as _, ) } .make_shared(); v8::ArrayBuffer::with_backing_store(scope, &backing_store_shared) }); to_v8_fallible!(BytesMut: |buf, scope| { let len = buf.len(); let ab = unsafe { v8::Local::cast(RustToV8Marker::::from(buf).to_v8(scope)) }; v8::Uint8Array::new(scope, ab, 0, len).ok_or_else(|| serde_v8::Error::Message("failed to allocate array".into())) }); // // Serde // impl<'a, T: serde::Serialize> RustToV8Fallible<'a> for RustToV8Marker { #[inline(always)] fn to_v8_fallible( self, scope: &mut v8::HandleScope<'a>, ) -> serde_v8::Result> { serde_v8::to_v8(scope, self.0) } } // // SMI // /// Implement an infallible SMI conversion from each of the integer types. SMI has a subset /// of the range of a normal integer, so we just do a bitwise cast. // TODO(mmastrac): We should be able to make these things much faster and without a scope macro_rules! smi_to_v8 { ($($ty:ident),*) => { $( impl<'a> RustToV8<'a> for RustToV8Marker { #[inline(always)] fn to_v8(self, scope: &mut v8::HandleScope<'a>) -> v8::Local<'a, v8::Value> { v8::Integer::new(scope, self.0 as _).into() } } impl <'a> RustToV8RetVal<'a> for RustToV8Marker { #[inline(always)] fn to_v8_rv(self, rv: &mut v8::ReturnValue<'a>) { rv.set_int32(self.0 as _) } } )* }; } smi_to_v8!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize); // // 64-bit numbers // /// Implement an infallible SMI conversion from each of the integer types. SMI has a subset /// of the range of a normal integer, so we just do a bitwise cast. // TODO(mmastrac): We should be able to make these things much faster and without a scope macro_rules! number_64_bit_to_v8 { ($($ty:ident),*) => { $( impl<'a> RustToV8<'a> for RustToV8Marker { #[inline(always)] fn to_v8(self, scope: &mut v8::HandleScope<'a>) -> v8::Local<'a, v8::Value> { v8::Number::new(scope, self.0 as _).into() } } impl <'a> RustToV8RetVal<'a> for RustToV8Marker { #[inline(always)] fn to_v8_rv(self, rv: &mut v8::ReturnValue<'a>) { rv.set_double(self.0 as _) } } )* }; } number_64_bit_to_v8!(u64, usize, i64, isize); // // Globals // impl<'a, T> RustToV8<'a> for v8::Global where v8::Local<'a, v8::Value>: From>, { fn to_v8(self, scope: &mut v8::HandleScope<'a>) -> v8::Local<'a, v8::Value> { v8::Local::new(scope, self).into() } } // // Locals // impl<'a, T> RustToV8NoScope<'a> for v8::Local<'a, T> where v8::Local<'a, v8::Value>: From>, { #[inline(always)] fn to_v8(self) -> v8::Local<'a, v8::Value> { self.into() } } impl<'a, T> RustToV8<'a> for Option> where v8::Local<'a, v8::Value>: From>, { #[inline(always)] fn to_v8(self, scope: &mut v8::HandleScope<'a>) -> v8::Local<'a, v8::Value> { if let Some(v) = self { v.to_v8() } else { v8::null(scope).into() } } } impl<'a, T> RustToV8RetVal<'a> for Option> where v8::Local<'a, v8::Value>: From>, { fn to_v8_rv(self, rv: &mut v8::ReturnValue<'a>) { if let Some(v) = self { rv.set(v.to_v8()) } else { rv.set_null() } } }