From aa8249c32cdc74fe595807b8a926652cf84745c1 Mon Sep 17 00:00:00 2001 From: Mazdak Farrokhzad Date: Thu, 24 Jul 2025 19:34:12 +0200 Subject: [PATCH 1/3] define a Deserializer from JS values --- crates/bindings-macro/src/sats.rs | 6 +- crates/core/src/host/v8/de.rs | 421 ++++++++++++++++++++++++++ crates/core/src/host/v8/error.rs | 10 + crates/core/src/host/v8/from_value.rs | 4 +- crates/core/src/host/v8/mod.rs | 1 + crates/sats/src/de.rs | 35 ++- crates/sats/src/de/impls.rs | 9 +- 7 files changed, 473 insertions(+), 13 deletions(-) create mode 100644 crates/core/src/host/v8/de.rs diff --git a/crates/bindings-macro/src/sats.rs b/crates/bindings-macro/src/sats.rs index 2d919431f98..8bad39da837 100644 --- a/crates/bindings-macro/src/sats.rs +++ b/crates/bindings-macro/src/sats.rs @@ -454,10 +454,10 @@ pub(crate) fn derive_deserialize(ty: &SatsType<'_>) -> TokenStream { } } - fn visit_seq<__E: #spacetimedb_lib::de::Error>(self, index: usize, name: &str) -> Result { + fn visit_seq(self, index: usize) -> Self::Output { match index { - #(#iter_n4 => Ok(__ProductFieldIdent::#field_names),)* - _ => Err(#spacetimedb_lib::de::Error::unknown_field_name(name, &self)), + #(#iter_n4 => __ProductFieldIdent::#field_names,)* + _ => core::unreachable!(), } } } diff --git a/crates/core/src/host/v8/de.rs b/crates/core/src/host/v8/de.rs new file mode 100644 index 00000000000..5d73633a36a --- /dev/null +++ b/crates/core/src/host/v8/de.rs @@ -0,0 +1,421 @@ +#![allow(dead_code)] + +use super::error::{exception_already_thrown, ExceptionThrown}; +use super::from_value::{cast, FromValue}; +use core::fmt; +use core::iter::{repeat_n, RepeatN}; +use core::mem::MaybeUninit; +use derive_more::From; +use spacetimedb_sats::de::{ + self, ArrayVisitor, DeserializeSeed, NoneAccess, ProductVisitor, SliceVisitor, SomeAccess, SumVisitor, +}; +use spacetimedb_sats::{i256, u256}; +use std::borrow::{Borrow, Cow}; +use v8::{Array, Global, HandleScope, Local, Object, Uint8Array, Value}; + +/// Deserializes from V8 values. +pub(super) struct Deserializer<'a, 's> { + common: DeserializerCommon<'a, 's>, + input: Local<'s, Value>, +} + +impl<'a, 's> Deserializer<'a, 's> { + /// Creates a new deserializer from `input` in `scope`. + pub fn new(scope: &'a mut HandleScope<'s>, input: Local<'_, Value>, key_cache: &'a mut KeyCache) -> Self { + let input = Local::new(scope, input); + let common = DeserializerCommon { scope, key_cache }; + Deserializer { input, common } + } +} + +/// Things shared between various [`Deserializer`]s. +/// +/// The lifetime `'s` is that of the scope of values deserialized. +struct DeserializerCommon<'a, 's> { + /// The scope of values to deserialize. + scope: &'a mut HandleScope<'s>, + /// A cache for frequently used strings. + key_cache: &'a mut KeyCache, +} + +impl<'s> DeserializerCommon<'_, 's> { + fn reborrow(&mut self) -> DeserializerCommon<'_, 's> { + DeserializerCommon { + scope: self.scope, + key_cache: self.key_cache, + } + } +} + +/// The possible errors that [`Deserializer`] can produce. +#[derive(From)] +pub(super) enum Error<'s> { + Value(Local<'s, Value>), + Exception(ExceptionThrown), + Custom(String), +} + +impl de::Error for Error<'_> { + fn custom(msg: impl fmt::Display) -> Self { + Self::Custom(msg.to_string()) + } +} + +/// Returns a scratch buffer to fill when deserializing strings. +fn scratch_buf() -> [MaybeUninit; N] { + [const { MaybeUninit::uninit() }; N] +} + +/// A cache for frequently used strings to avoid re-interning them. +#[derive(Default)] +pub(super) struct KeyCache { + /// The `tag` property for sum values in JS. + tag: Option>, + /// The `value` property for sum values in JS. + value: Option>, +} + +impl KeyCache { + /// Returns the `tag` property name. + pub(super) fn tag<'s>(&mut self, scope: &mut HandleScope<'s>) -> Local<'s, v8::String> { + Self::get_or_create_key(scope, &mut self.tag, "tag") + } + + /// Returns the `value` property name. + pub(super) fn value<'s>(&mut self, scope: &mut HandleScope<'s>) -> Local<'s, v8::String> { + Self::get_or_create_key(scope, &mut self.tag, "value") + } + + /// Returns an interned string corresponding to `string` + /// and memoizes the creation on the v8 side. + fn get_or_create_key<'s>( + scope: &mut HandleScope<'s>, + slot: &mut Option>, + string: &str, + ) -> Local<'s, v8::String> { + if let Some(s) = &*slot { + v8::Local::new(scope, s) + } else { + let s = v8_interned_string(scope, string); + *slot = Some(v8::Global::new(scope, s)); + s + } + } +} + +// Creates an interned [`v8::String`]. +fn v8_interned_string<'s>(scope: &mut HandleScope<'s>, field: &str) -> Local<'s, v8::String> { + // Internalized v8 strings are significantly faster than "normal" v8 strings + // since v8 deduplicates re-used strings minimizing new allocations + // see: https://github.com/v8/v8/blob/14ac92e02cc3db38131a57e75e2392529f405f2f/include/v8.h#L3165-L3171 + v8::String::new_from_utf8(scope, field.as_ref(), v8::NewStringType::Internalized).unwrap() +} + +/// Extracts a reference `&'s T` from an owned V8 [`Local<'s, T>`]. +/// +/// The lifetime `'s` is that of the [`HandleScope<'s>`]. +/// This ensures that the reference to `T` won't outlive the `HandleScope`. +fn deref_local<'s, T>(local: Local<'s, T>) -> &'s T { + let reference = local.borrow(); + // SAFETY: Lifetime extend `'0` to `'s`. + // This is safe as the returned reference `&'s T` + // will not outlive its `HandleScope<'s, _>`, + // as both are tied to the lifetime `'s`. + unsafe { core::mem::transmute::<&T, &'s T>(reference) } +} + +/// Deserializes a primitive via [`FromValue`]. +macro_rules! deserialize_primitive { + ($dmethod:ident, $t:ty) => { + fn $dmethod(self) -> Result<$t, Self::Error> { + FromValue::from_value(self.input, self.common.scope).map_err(Error::Value) + } + }; +} + +impl<'de, 'a, 's: 'de> de::Deserializer<'de> for Deserializer<'a, 's> { + type Error = Error<'s>; + + // Deserialization of primitive types defers to `FromValue`. + deserialize_primitive!(deserialize_bool, bool); + deserialize_primitive!(deserialize_u8, u8); + deserialize_primitive!(deserialize_u16, u16); + deserialize_primitive!(deserialize_u32, u32); + deserialize_primitive!(deserialize_u64, u64); + deserialize_primitive!(deserialize_u128, u128); + deserialize_primitive!(deserialize_u256, u256); + deserialize_primitive!(deserialize_i8, i8); + deserialize_primitive!(deserialize_i16, i16); + deserialize_primitive!(deserialize_i32, i32); + deserialize_primitive!(deserialize_i64, i64); + deserialize_primitive!(deserialize_i128, i128); + deserialize_primitive!(deserialize_i256, i256); + deserialize_primitive!(deserialize_f64, f64); + deserialize_primitive!(deserialize_f32, f32); + + fn deserialize_product>(self, visitor: V) -> Result { + let object = cast!( + self.common.scope, + self.input, + Object, + "object for product type `{}`", + visitor.product_name().unwrap_or("") + )?; + + visitor.visit_named_product(ProductAccess { + common: self.common, + object, + next_value: None, + index: 0, + }) + } + + fn deserialize_sum>(self, visitor: V) -> Result { + let scope = &mut *self.common.scope; + let sum_name = visitor.sum_name().unwrap_or(""); + + // We expect a canonical representation of a sum value in JS to be + // `{ tag: "foo", value: a_value_for_foo }` + // with special convenience for optionals + // where we also accept `null`, `undefined` and an object without `tag`. + let (object, tag_field) = 'treat_as_regular_sum: { + // Optionals receive some special handling for added convenience in JS. + if visitor.is_option() { + // If we don't have an object at all, + // it's either `null | undefined` which means `none` + // or it is `some(the_value)`. + if let Some(object) = self.input.to_object(scope) { + // If there is `tag` field, treat this as a normal sum. + // Otherwise, we have `some(the_value)`. + let tag_field = self.common.key_cache.tag(scope); + if object + .has_own_property(scope, tag_field.into()) + .ok_or_else(exception_already_thrown)? + { + break 'treat_as_regular_sum (object, tag_field); + } + } else if self.input.is_null_or_undefined() { + // JS has support for `undefined` and `null` values. + // It's reasonable to interpret these as `None` + // when we're deserializing to an optional value + // rust-side, such as `Option`. + return visitor.visit_sum(NoneAccess::new()); + } + + return visitor.visit_sum(SomeAccess::new(self)); + } else { + let tag_field = self.common.key_cache.tag(scope); + let val = cast!(scope, self.input, Object, "object for sum type `{}`", sum_name)?; + (val, tag_field) + } + }; + + // Extract the `tag` field. It needs to contain a string. + let tag = object + .get(scope, tag_field.into()) + .ok_or_else(exception_already_thrown)?; + let tag = cast!(scope, tag, v8::String, "string for sum tag of `{}`", sum_name)?; + + // Extract the `value` field. + let value_field = self.common.key_cache.value(scope); + let value = object + .get(scope, value_field.into()) + .ok_or_else(exception_already_thrown)?; + + // Stitch it all together. + visitor.visit_sum(SumAccess { + common: self.common, + tag, + value, + }) + } + + fn deserialize_str>(self, visitor: V) -> Result { + let val = cast!(self.common.scope, self.input, v8::String, "`string`")?; + let mut buf = scratch_buf::<64>(); + match val.to_rust_cow_lossy(self.common.scope, &mut buf) { + Cow::Borrowed(s) => visitor.visit(s), + Cow::Owned(string) => visitor.visit_owned(string), + } + } + + fn deserialize_bytes>(self, visitor: V) -> Result { + let arr = cast!(self.common.scope, self.input, Uint8Array, "`Uint8Array` for bytes")?; + let storage: &'static mut [u8] = &mut [0; v8::TYPED_ARRAY_MAX_SIZE_IN_HEAP]; + let bytes: &'s [u8] = deref_local(arr).get_contents(storage); + visitor.visit_borrowed(bytes) + } + + fn deserialize_array_seed, T: DeserializeSeed<'de> + Clone>( + self, + visitor: V, + seed: T, + ) -> Result { + let arr = cast!(self.common.scope, self.input, v8::Array, "`Array`")?; + visitor.visit(ArrayAccess::new(arr, self.common, seed)) + } +} + +struct ProductAccess<'a, 's> { + common: DeserializerCommon<'a, 's>, + /// The input object being deserialized. + object: Local<'s, Object>, + /// A field's value, to deserialize next in [`NamedProductAccess::get_field_value_seed`]. + next_value: Option>, + /// The index in the product to + index: usize, +} + +/// Map from integer keys to their `str` representation, +/// for small numbers up to 12. +static INT_TO_STR: &[&str] = &["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]; + +impl<'de, 's: 'de> de::NamedProductAccess<'de> for ProductAccess<'_, 's> { + type Error = Error<'s>; + + fn get_field_ident>(&mut self, visitor: V) -> Result, Self::Error> { + let scope = &mut *self.common.scope; + let mut field_names = visitor.field_names(); + while let Some(field) = field_names.nth(self.index) { + // Get and advance the current index. + let index = self.index; + self.index += 1; + + // Normalize the field name. + // Integer keys are converted to strings, + // as that is supported on JS objects. + // TODO(centril, perf): consider replacing this with `itoa::Buffer`. + let mut string_mem = None; + let field = match field { + Some(field) => field, + None if index <= 12 => INT_TO_STR[index], + None => string_mem.insert(format!("{index}")), + }; + + // Check that such a field/key exists. + let key = v8_interned_string(scope, field).into(); + if !self + .object + .has_own_property(scope, key) + .ok_or_else(exception_already_thrown)? + { + continue; + } + + // Extract the next value, to be processed in `get_field_value_seed`. + let val = self + .object + .get(scope, key.into()) + .ok_or_else(exception_already_thrown)?; + self.next_value = Some(val); + + drop(field_names); + return Ok(Some(visitor.visit_seq(index))); + } + + Ok(None) + } + + fn get_field_value_seed>(&mut self, seed: T) -> Result { + let common = self.common.reborrow(); + // Extract the field's value. + let input = self + .next_value + .take() + .expect("Call next_key_seed before next_value_seed"); + // Deserialize the field's value. + seed.deserialize(Deserializer { common, input }) + } +} + +struct SumAccess<'a, 's> { + common: DeserializerCommon<'a, 's>, + /// The tag of the sum value. + tag: Local<'s, v8::String>, + /// The value of the sum value. + value: Local<'s, Value>, +} + +impl<'de, 'a, 's: 'de> de::SumAccess<'de> for SumAccess<'a, 's> { + type Error = Error<'s>; + type Variant = Deserializer<'a, 's>; + + fn variant>(self, visitor: V) -> Result<(V::Output, Self::Variant), Self::Error> { + // Read the `tag` property in JS. + // We generously provide 32 bytes for inline reading of the property + // before resorting to the heap. + let mut buf = scratch_buf::<32>(); + let name = self.tag.to_rust_cow_lossy(self.common.scope, &mut buf); + + // Select the variant to deserialize. + let variant = visitor.visit_name::(&name)?; + + // Prepare the deserialization of the payload. + let dpayload = Deserializer { + common: self.common, + input: self.value, + }; + + Ok((variant, dpayload)) + } +} + +impl<'de, 'a, 's: 'de> de::VariantAccess<'de> for Deserializer<'a, 's> { + type Error = Error<'s>; + + fn deserialize_seed>(self, seed: T) -> Result { + seed.deserialize(self) + } +} + +struct ArrayAccess<'a, 's, T> { + common: DeserializerCommon<'a, 's>, + arr: Local<'s, Array>, + seeds: RepeatN, + index: u32, +} + +impl<'de, 'a, 's, T> ArrayAccess<'a, 's, T> +where + T: DeserializeSeed<'de> + Clone, +{ + fn new(arr: Local<'s, Array>, common: DeserializerCommon<'a, 's>, seed: T) -> Self { + Self { + arr, + common, + seeds: repeat_n(seed, arr.length() as usize), + index: 0, + } + } +} + +impl<'de, 's: 'de, T: DeserializeSeed<'de> + Clone> de::ArrayAccess<'de> for ArrayAccess<'_, 's, T> { + type Element = T::Output; + type Error = Error<'s>; + + fn next_element(&mut self) -> Result, Self::Error> { + self.seeds + .next() + .map(|seed| { + // Extract the array element. + let val = self + .arr + .get_index(self.common.scope, self.index) + .ok_or_else(exception_already_thrown)?; + + // Deserialize the element. + let val = seed.deserialize(Deserializer { + common: self.common.reborrow(), + input: val, + })?; + + self.index += 1; + Ok(val) + }) + .transpose() + } + + fn size_hint(&self) -> Option { + Some(self.seeds.len()) + } +} diff --git a/crates/core/src/host/v8/error.rs b/crates/core/src/host/v8/error.rs index 7142f1b0683..fa682a749fa 100644 --- a/crates/core/src/host/v8/error.rs +++ b/crates/core/src/host/v8/error.rs @@ -33,3 +33,13 @@ impl IntoException for TypeError { Exception::type_error(scope, msg) } } + +#[derive(Debug)] +pub(super) struct ExceptionThrown { + _priv: (), +} + +/// Indicates that the JS side had thrown an exception. +pub(super) fn exception_already_thrown() -> ExceptionThrown { + ExceptionThrown { _priv: () } +} diff --git a/crates/core/src/host/v8/from_value.rs b/crates/core/src/host/v8/from_value.rs index ecf90024484..802f5523141 100644 --- a/crates/core/src/host/v8/from_value.rs +++ b/crates/core/src/host/v8/from_value.rs @@ -23,7 +23,7 @@ macro_rules! impl_from_value { } /// Tries to cast `Value` into `T` or raises a JS exception as a returned `Err` value. -fn try_cast<'a, 'b, T>( +pub(super) fn try_cast<'a, 'b, T>( scope: &mut HandleScope<'a>, val: Local<'b, Value>, on_err: impl FnOnce(&str) -> String, @@ -38,7 +38,7 @@ where /// Tries to cast `Value` into `T` or raises a JS exception as a returned `Err` value. macro_rules! cast { ($scope:expr, $val:expr, $js_ty:ty, $expected:literal $(, $args:expr)* $(,)?) => {{ - try_cast::<$js_ty>($scope, $val, |got| format!(concat!("Expected ", $expected, ", got {__got}"), $($args,)* __got = got)) + $crate::host::v8::from_value::try_cast::<$js_ty>($scope, $val, |got| format!(concat!("Expected ", $expected, ", got {__got}"), $($args,)* __got = got)) }}; } pub(super) use cast; diff --git a/crates/core/src/host/v8/mod.rs b/crates/core/src/host/v8/mod.rs index fd49d1e5a3c..ba128aa7588 100644 --- a/crates/core/src/host/v8/mod.rs +++ b/crates/core/src/host/v8/mod.rs @@ -12,6 +12,7 @@ use anyhow::anyhow; use spacetimedb_datastore::locking_tx_datastore::MutTxId; use std::sync::{Arc, LazyLock}; +mod de; mod error; mod from_value; mod to_value; diff --git a/crates/sats/src/de.rs b/crates/sats/src/de.rs index 552518aeabf..fa86624bf6f 100644 --- a/crates/sats/src/de.rs +++ b/crates/sats/src/de.rs @@ -329,8 +329,8 @@ pub trait NamedProductAccess<'de> { /// The error type that can be returned if some error occurs during deserialization. type Error: Error; - /// Deserializes field name of type `V::Output` from the input using a visitor - /// provided by the deserializer. + /// Deserializes field name of type `V::Output` + /// from the input using a visitor provided by the deserializer. fn get_field_ident>(&mut self, visitor: V) -> Result, Self::Error>; /// Deserializes field value of type `T` from the input. @@ -368,8 +368,9 @@ pub trait FieldNameVisitor<'de> { /// Deserializes the name of a field using `index`. /// - /// The `name` is provided for error messages. - fn visit_seq(self, index: usize, name: &str) -> Result; + /// Should only be called when `index` is already known to exist + /// and is expected to panic otherwise. + fn visit_seq(self, index: usize) -> Self::Output; } /// A visitor walking through a [`Deserializer`] for sums. @@ -721,3 +722,29 @@ impl<'de, E: Error> VariantAccess<'de> for NoneAccess { }) } } + +/// Deserializes `some` variant of an optional value. +pub struct SomeAccess(D); + +impl SomeAccess { + /// Returns a new [`SomeAccess`] with a given deserializer for the `some` variant. + pub fn new(de: D) -> Self { + Self(de) + } +} + +impl<'de, D: Deserializer<'de>> SumAccess<'de> for SomeAccess { + type Error = D::Error; + type Variant = Self; + + fn variant>(self, visitor: V) -> Result<(V::Output, Self::Variant), Self::Error> { + visitor.visit_name("some").map(|var| (var, self)) + } +} + +impl<'de, D: Deserializer<'de>> VariantAccess<'de> for SomeAccess { + type Error = D::Error; + fn deserialize_seed>(self, seed: T) -> Result { + seed.deserialize(self.0) + } +} diff --git a/crates/sats/src/de/impls.rs b/crates/sats/src/de/impls.rs index 1fa3f1a0869..be177c6de40 100644 --- a/crates/sats/src/de/impls.rs +++ b/crates/sats/src/de/impls.rs @@ -600,7 +600,7 @@ pub fn visit_named_product<'de, A: super::NamedProductAccess<'de>>( // This is worst case quadratic in complexity // as fields can be specified out of order (value side) compared to `elems` (type side). for _ in 0..elems.len() { - // Deserialize a field name, match against the element types, . + // Deserialize a field name, match against the element types. let index = tup.get_field_ident(TupleNameVisitor { elems, kind })?.ok_or_else(|| { // Couldn't deserialize a field name. // Find the first field name we haven't filled an element for. @@ -659,12 +659,13 @@ impl FieldNameVisitor<'_> for TupleNameVisitor<'_> { .ok_or_else(|| Error::unknown_field_name(name, &self)) } - fn visit_seq(self, index: usize, name: &str) -> Result { + fn visit_seq(self, index: usize) -> Self::Output { + // Confirm that the index exists. self.elems .get(index) - .ok_or_else(|| Error::unknown_field_name(name, &self))?; + .expect("`index` should exist when `visit_seq` is called"); - Ok(index) + index } } From d6a051d894b8e97852dc31385dcd024000c08fc0 Mon Sep 17 00:00:00 2001 From: Mazdak Farrokhzad Date: Mon, 28 Jul 2025 21:42:49 +0200 Subject: [PATCH 2/3] address Tyler's review comments --- crates/core/src/host/v8/de.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/crates/core/src/host/v8/de.rs b/crates/core/src/host/v8/de.rs index 5d73633a36a..0a17cf05e24 100644 --- a/crates/core/src/host/v8/de.rs +++ b/crates/core/src/host/v8/de.rs @@ -83,7 +83,7 @@ impl KeyCache { /// Returns the `value` property name. pub(super) fn value<'s>(&mut self, scope: &mut HandleScope<'s>) -> Local<'s, v8::String> { - Self::get_or_create_key(scope, &mut self.tag, "value") + Self::get_or_create_key(scope, &mut self.value, "value") } /// Returns an interned string corresponding to `string` @@ -256,6 +256,8 @@ impl<'de, 'a, 's: 'de> de::Deserializer<'de> for Deserializer<'a, 's> { } } +/// Provides access to the field names and values in a JS object +/// under the assumption that it's a product. struct ProductAccess<'a, 's> { common: DeserializerCommon<'a, 's>, /// The input object being deserialized. @@ -266,10 +268,6 @@ struct ProductAccess<'a, 's> { index: usize, } -/// Map from integer keys to their `str` representation, -/// for small numbers up to 12. -static INT_TO_STR: &[&str] = &["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]; - impl<'de, 's: 'de> de::NamedProductAccess<'de> for ProductAccess<'_, 's> { type Error = Error<'s>; @@ -284,16 +282,13 @@ impl<'de, 's: 'de> de::NamedProductAccess<'de> for ProductAccess<'_, 's> { // Normalize the field name. // Integer keys are converted to strings, // as that is supported on JS objects. - // TODO(centril, perf): consider replacing this with `itoa::Buffer`. - let mut string_mem = None; let field = match field { - Some(field) => field, - None if index <= 12 => INT_TO_STR[index], - None => string_mem.insert(format!("{index}")), + Some(field) => Cow::Borrowed(field), + None => Cow::Owned(format!("{index}")), }; // Check that such a field/key exists. - let key = v8_interned_string(scope, field).into(); + let key = v8_interned_string(scope, &field).into(); if !self .object .has_own_property(scope, key) @@ -328,6 +323,8 @@ impl<'de, 's: 'de> de::NamedProductAccess<'de> for ProductAccess<'_, 's> { } } +/// Used in `Deserializer::deserialize_sum` to translate a `tag` property of a JS object +/// to a variant and to provide a deserializer for its value/payload. struct SumAccess<'a, 's> { common: DeserializerCommon<'a, 's>, /// The tag of the sum value. @@ -368,6 +365,8 @@ impl<'de, 'a, 's: 'de> de::VariantAccess<'de> for Deserializer<'a, 's> { } } +/// Used by an `ArrayVisitor` to deserialize every element of a JS array +/// to a SATS array. struct ArrayAccess<'a, 's, T> { common: DeserializerCommon<'a, 's>, arr: Local<'s, Array>, From a11f5765d30b3c516a2a5e075e753c8a06f4666c Mon Sep 17 00:00:00 2001 From: Mazdak Farrokhzad Date: Wed, 30 Jul 2025 22:49:48 +0200 Subject: [PATCH 3/3] address phoebe's review re. comments --- crates/core/src/host/v8/de.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/core/src/host/v8/de.rs b/crates/core/src/host/v8/de.rs index 0a17cf05e24..94ec77e0b34 100644 --- a/crates/core/src/host/v8/de.rs +++ b/crates/core/src/host/v8/de.rs @@ -114,13 +114,22 @@ fn v8_interned_string<'s>(scope: &mut HandleScope<'s>, field: &str) -> Local<'s, /// Extracts a reference `&'s T` from an owned V8 [`Local<'s, T>`]. /// /// The lifetime `'s` is that of the [`HandleScope<'s>`]. -/// This ensures that the reference to `T` won't outlive the `HandleScope`. +/// This ensures that the reference to `T` won't outlive the `HandleScope`, +/// that owns the `x: T` and keeps it alive. fn deref_local<'s, T>(local: Local<'s, T>) -> &'s T { let reference = local.borrow(); // SAFETY: Lifetime extend `'0` to `'s`. // This is safe as the returned reference `&'s T` - // will not outlive its `HandleScope<'s, _>`, + // will not outlive its `HandleScope<'s, _>` // as both are tied to the lifetime `'s`. + // The scope is what owns the `x: T`. + // + // See https://docs.rs/v8/latest/v8/struct.Local.html for more details. + // > It is safe to extract the object stored in the handle by + // > dereferencing the handle (for instance, to extract the `*Object` from + // > a `Local`); the value will still be governed by a handle + // > behind the scenes and the same rules apply to these values as to + // > their handles. unsafe { core::mem::transmute::<&T, &'s T>(reference) } } @@ -264,7 +273,7 @@ struct ProductAccess<'a, 's> { object: Local<'s, Object>, /// A field's value, to deserialize next in [`NamedProductAccess::get_field_value_seed`]. next_value: Option>, - /// The index in the product to + /// The field index to deserialize next in the product. index: usize, }