Skip to content

Commit 0b3ad6f

Browse files
authored
V8: Ser/de finishing touches (#3153)
# Description of Changes This does 2 things: 1. simplifies the deserialization of optional sums so that they are treated as regular sums. 2. adds higher level v8 ser/de interfaces and privatizes what can be private. # API and ABI breaking changes None # Expected complexity level and risk 1 # Testing Existing ser/de tests are adjusted to use the new interfaces.
1 parent 4921983 commit 0b3ad6f

File tree

4 files changed

+154
-72
lines changed

4 files changed

+154
-72
lines changed

crates/core/src/host/v8/de.rs

Lines changed: 59 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,60 @@
11
#![allow(dead_code)]
22

3-
use super::error::{exception_already_thrown, ExceptionThrown};
3+
use super::error::{exception_already_thrown, ExcResult, ExceptionThrown, ExceptionValue, Throwable, TypeError};
44
use super::from_value::{cast, FromValue};
5+
use core::cell::RefCell;
56
use core::fmt;
67
use core::iter::{repeat_n, RepeatN};
8+
use core::marker::PhantomData;
79
use core::mem::MaybeUninit;
810
use derive_more::From;
9-
use spacetimedb_sats::de::{
10-
self, ArrayVisitor, DeserializeSeed, NoneAccess, ProductVisitor, SliceVisitor, SomeAccess, SumVisitor,
11-
};
11+
use spacetimedb_sats::de::{self, ArrayVisitor, DeserializeSeed, ProductVisitor, SliceVisitor, SumVisitor};
1212
use spacetimedb_sats::{i256, u256};
1313
use std::borrow::{Borrow, Cow};
14+
use std::rc::Rc;
1415
use v8::{Array, Global, HandleScope, Local, Name, Object, Uint8Array, Value};
1516

17+
/// Returns a `KeyCache` for the current `scope`.
18+
///
19+
/// Creates the cache in the scope if it doesn't exist yet.
20+
pub(super) fn get_or_create_key_cache(scope: &mut HandleScope<'_>) -> Rc<RefCell<KeyCache>> {
21+
let context = scope.get_current_context();
22+
context.get_slot::<RefCell<KeyCache>>().unwrap_or_else(|| {
23+
let cache = Rc::default();
24+
context.set_slot(Rc::clone(&cache));
25+
cache
26+
})
27+
}
28+
29+
/// Deserializes a `T` from `val` in `scope`, using `seed` for any context needed.
30+
pub(super) fn deserialize_js_seed<'de, T: DeserializeSeed<'de>>(
31+
scope: &mut HandleScope<'de>,
32+
val: Local<'_, Value>,
33+
seed: T,
34+
) -> ExcResult<T::Output> {
35+
let key_cache = get_or_create_key_cache(scope);
36+
let key_cache = &mut *key_cache.borrow_mut();
37+
let de = Deserializer::new(scope, val, key_cache);
38+
seed.deserialize(de).map_err(|e| e.throw(scope))
39+
}
40+
41+
/// Deserializes a `T` from `val` in `scope`.
42+
pub(super) fn deserialize_js<'de, T: de::Deserialize<'de>>(
43+
scope: &mut HandleScope<'de>,
44+
val: Local<'_, Value>,
45+
) -> ExcResult<T> {
46+
deserialize_js_seed(scope, val, PhantomData)
47+
}
48+
1649
/// Deserializes from V8 values.
17-
pub(super) struct Deserializer<'this, 'scope> {
50+
struct Deserializer<'this, 'scope> {
1851
common: DeserializerCommon<'this, 'scope>,
1952
input: Local<'scope, Value>,
2053
}
2154

2255
impl<'this, 'scope> Deserializer<'this, 'scope> {
2356
/// Creates a new deserializer from `input` in `scope`.
24-
pub fn new(scope: &'this mut HandleScope<'scope>, input: Local<'_, Value>, key_cache: &'this mut KeyCache) -> Self {
57+
fn new(scope: &'this mut HandleScope<'scope>, input: Local<'_, Value>, key_cache: &'this mut KeyCache) -> Self {
2558
let input = Local::new(scope, input);
2659
let common = DeserializerCommon { scope, key_cache };
2760
Deserializer { input, common }
@@ -49,12 +82,22 @@ impl<'scope> DeserializerCommon<'_, 'scope> {
4982

5083
/// The possible errors that [`Deserializer`] can produce.
5184
#[derive(Debug, From)]
52-
pub(super) enum Error<'scope> {
53-
Value(Local<'scope, Value>),
54-
Exception(ExceptionThrown),
85+
enum Error<'scope> {
86+
Unthrown(ExceptionValue<'scope>),
87+
Thrown(ExceptionThrown),
5588
Custom(String),
5689
}
5790

91+
impl<'scope> Throwable<'scope> for Error<'scope> {
92+
fn throw(self, scope: &mut HandleScope<'scope>) -> ExceptionThrown {
93+
match self {
94+
Self::Unthrown(exception) => exception.throw(scope),
95+
Self::Thrown(thrown) => thrown,
96+
Self::Custom(msg) => TypeError(msg).throw(scope),
97+
}
98+
}
99+
}
100+
58101
impl de::Error for Error<'_> {
59102
fn custom(msg: impl fmt::Display) -> Self {
60103
Self::Custom(msg.to_string())
@@ -104,7 +147,7 @@ impl KeyCache {
104147
}
105148

106149
// Creates an interned [`v8::String`].
107-
pub(super) fn v8_interned_string<'scope>(scope: &mut HandleScope<'scope>, field: &str) -> Local<'scope, v8::String> {
150+
fn v8_interned_string<'scope>(scope: &mut HandleScope<'scope>, field: &str) -> Local<'scope, v8::String> {
108151
// Internalized v8 strings are significantly faster than "normal" v8 strings
109152
// since v8 deduplicates re-used strings minimizing new allocations
110153
// see: https://github.com/v8/v8/blob/14ac92e02cc3db38131a57e75e2392529f405f2f/include/v8.h#L3165-L3171
@@ -128,7 +171,7 @@ fn deref_local<'scope, T>(local: Local<'scope, T>) -> &'scope T {
128171
macro_rules! deserialize_primitive {
129172
($dmethod:ident, $t:ty) => {
130173
fn $dmethod(self) -> Result<$t, Self::Error> {
131-
FromValue::from_value(self.input, self.common.scope).map_err(Error::Value)
174+
FromValue::from_value(self.input, self.common.scope).map_err(Error::Unthrown)
132175
}
133176
};
134177
}
@@ -175,40 +218,9 @@ impl<'de, 'this, 'scope: 'de> de::Deserializer<'de> for Deserializer<'this, 'sco
175218
let sum_name = visitor.sum_name().unwrap_or("<unknown>");
176219

177220
// We expect a canonical representation of a sum value in JS to be
178-
// `{ tag: "foo", value: a_value_for_foo }`
179-
// with special convenience for optionals
180-
// where we also accept `null`, `undefined` and an object without `tag`.
181-
let (object, tag_field) = 'treat_as_regular_sum: {
182-
// Optionals receive some special handling for added convenience in JS.
183-
if visitor.is_option() {
184-
// If we don't have an object at all,
185-
// it's either `null | undefined` which means `none`
186-
// or it is `some(the_value)`.
187-
if let Some(object) = self.input.to_object(scope) {
188-
// If there is `tag` field, treat this as a normal sum.
189-
// Otherwise, we have `some(the_value)`.
190-
let tag_field = self.common.key_cache.tag(scope);
191-
if object
192-
.has_own_property(scope, tag_field.into())
193-
.ok_or_else(exception_already_thrown)?
194-
{
195-
break 'treat_as_regular_sum (object, tag_field);
196-
}
197-
} else if self.input.is_null_or_undefined() {
198-
// JS has support for `undefined` and `null` values.
199-
// It's reasonable to interpret these as `None`
200-
// when we're deserializing to an optional value
201-
// rust-side, such as `Option<T>`.
202-
return visitor.visit_sum(NoneAccess::new());
203-
}
204-
205-
return visitor.visit_sum(SomeAccess::new(self));
206-
} else {
207-
let tag_field = self.common.key_cache.tag(scope);
208-
let val = cast!(scope, self.input, Object, "object for sum type `{}`", sum_name)?;
209-
(val, tag_field)
210-
}
211-
};
221+
// `{ tag: "foo", value: a_value_for_foo }`.
222+
let tag_field = self.common.key_cache.tag(scope);
223+
let object = cast!(scope, self.input, Object, "object for sum type `{}`", sum_name)?;
212224

213225
// Extract the `tag` field. It needs to contain a string.
214226
let tag = object
@@ -320,7 +332,7 @@ impl<'de, 'scope: 'de> de::NamedProductAccess<'de> for ProductAccess<'_, 'scope>
320332
Ok(None)
321333
}
322334

323-
fn get_field_value_seed<T: de::DeserializeSeed<'de>>(&mut self, seed: T) -> Result<T::Output, Self::Error> {
335+
fn get_field_value_seed<T: DeserializeSeed<'de>>(&mut self, seed: T) -> Result<T::Output, Self::Error> {
324336
let common = self.common.reborrow();
325337
// Extract the field's value.
326338
let input = self
@@ -369,7 +381,7 @@ impl<'de, 'this, 'scope: 'de> de::SumAccess<'de> for SumAccess<'this, 'scope> {
369381
impl<'de, 'this, 'scope: 'de> de::VariantAccess<'de> for Deserializer<'this, 'scope> {
370382
type Error = Error<'scope>;
371383

372-
fn deserialize_seed<T: de::DeserializeSeed<'de>>(self, seed: T) -> Result<T::Output, Self::Error> {
384+
fn deserialize_seed<T: DeserializeSeed<'de>>(self, seed: T) -> Result<T::Output, Self::Error> {
373385
seed.deserialize(self)
374386
}
375387
}

crates/core/src/host/v8/error.rs

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use v8::{Exception, HandleScope, Local, Value};
44

55
/// The result of trying to convert a [`Value`] in scope `'scope` to some type `T`.
6-
pub(super) type ValueResult<'scope, T> = Result<T, Local<'scope, Value>>;
6+
pub(super) type ValueResult<'scope, T> = Result<T, ExceptionValue<'scope>>;
77

88
/// Types that can convert into a JS string type.
99
pub(super) trait IntoJsString {
@@ -17,20 +17,43 @@ impl IntoJsString for String {
1717
}
1818
}
1919

20+
/// A JS exception value.
21+
///
22+
/// Newtyped for additional type safety and to track JS exceptions in the type system.
23+
#[derive(Debug)]
24+
pub(super) struct ExceptionValue<'scope>(Local<'scope, Value>);
25+
2026
/// Error types that can convert into JS exception values.
21-
pub(super) trait IntoException {
27+
pub(super) trait IntoException<'scope> {
2228
/// Converts `self` into a JS exception value.
23-
fn into_exception<'scope>(self, scope: &mut HandleScope<'scope>) -> Local<'scope, Value>;
29+
fn into_exception(self, scope: &mut HandleScope<'scope>) -> ExceptionValue<'scope>;
30+
}
31+
32+
impl<'scope> IntoException<'scope> for ExceptionValue<'scope> {
33+
fn into_exception(self, _: &mut HandleScope<'scope>) -> ExceptionValue<'scope> {
34+
self
35+
}
2436
}
2537

2638
/// A type converting into a JS `TypeError` exception.
2739
#[derive(Copy, Clone)]
2840
pub struct TypeError<M>(pub M);
2941

30-
impl<M: IntoJsString> IntoException for TypeError<M> {
31-
fn into_exception<'scope>(self, scope: &mut HandleScope<'scope>) -> Local<'scope, Value> {
42+
impl<'scope, M: IntoJsString> IntoException<'scope> for TypeError<M> {
43+
fn into_exception(self, scope: &mut HandleScope<'scope>) -> ExceptionValue<'scope> {
3244
let msg = self.0.into_string(scope);
33-
Exception::type_error(scope, msg)
45+
ExceptionValue(Exception::type_error(scope, msg))
46+
}
47+
}
48+
49+
/// A type converting into a JS `RangeError` exception.
50+
#[derive(Copy, Clone)]
51+
pub struct RangeError<M>(pub M);
52+
53+
impl<'scope, M: IntoJsString> IntoException<'scope> for RangeError<M> {
54+
fn into_exception(self, scope: &mut HandleScope<'scope>) -> ExceptionValue<'scope> {
55+
let msg = self.0.into_string(scope);
56+
ExceptionValue(Exception::range_error(scope, msg))
3457
}
3558
}
3659

@@ -39,7 +62,27 @@ pub(super) struct ExceptionThrown {
3962
_priv: (),
4063
}
4164

65+
/// A result where the error indicates that an exception has already been thrown in V8.
66+
pub(super) type ExcResult<T> = Result<T, ExceptionThrown>;
67+
4268
/// Indicates that the JS side had thrown an exception.
4369
pub(super) fn exception_already_thrown() -> ExceptionThrown {
4470
ExceptionThrown { _priv: () }
4571
}
72+
73+
/// Types that can be thrown as a V8 exception.
74+
pub(super) trait Throwable<'scope> {
75+
/// Throw `self` into the V8 engine as an exception.
76+
///
77+
/// If an exception has already been thrown,
78+
/// [`ExceptionThrown`] can be returned directly.
79+
fn throw(self, scope: &mut HandleScope<'scope>) -> ExceptionThrown;
80+
}
81+
82+
impl<'scope, T: IntoException<'scope>> Throwable<'scope> for T {
83+
fn throw(self, scope: &mut HandleScope<'scope>) -> ExceptionThrown {
84+
let ExceptionValue(exception) = self.into_exception(scope);
85+
scope.throw_exception(exception);
86+
exception_already_thrown()
87+
}
88+
}

crates/core/src/host/v8/from_value.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#![allow(dead_code)]
22

3+
use crate::host::v8::error::ExceptionValue;
4+
35
use super::error::{IntoException as _, TypeError, ValueResult};
46
use bytemuck::{AnyBitPattern, NoUninit};
57
use spacetimedb_sats::{i256, u256};
@@ -48,13 +50,13 @@ pub(super) use cast;
4850

4951
/// Returns a JS exception value indicating that a value overflowed
5052
/// when converting to the type `rust_ty`.
51-
fn value_overflowed<'scope>(rust_ty: &str, scope: &mut HandleScope<'scope>) -> Local<'scope, Value> {
53+
fn value_overflowed<'scope>(rust_ty: &str, scope: &mut HandleScope<'scope>) -> ExceptionValue<'scope> {
5254
TypeError(format!("Value overflowed `{rust_ty}`")).into_exception(scope)
5355
}
5456

5557
/// Returns a JS exception value indicating that a value underflowed
5658
/// when converting to the type `rust_ty`.
57-
fn value_underflowed<'scope>(rust_ty: &str, scope: &mut HandleScope<'scope>) -> Local<'scope, Value> {
59+
fn value_underflowed<'scope>(rust_ty: &str, scope: &mut HandleScope<'scope>) -> ExceptionValue<'scope> {
5860
TypeError(format!("Value underflowed `{rust_ty}`")).into_exception(scope)
5961
}
6062

0 commit comments

Comments
 (0)