Skip to content

Commit c8af62b

Browse files
committed
v8: add serialize_to_js & deserialize_js[_seed]
1 parent e6ef699 commit c8af62b

4 files changed

Lines changed: 150 additions & 35 deletions

File tree

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

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +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;
911
use spacetimedb_sats::de::{self, ArrayVisitor, DeserializeSeed, ProductVisitor, SliceVisitor, SumVisitor};
1012
use spacetimedb_sats::{i256, u256};
1113
use std::borrow::{Borrow, Cow};
14+
use std::rc::Rc;
1215
use v8::{Array, Global, HandleScope, Local, Name, Object, Uint8Array, Value};
1316

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+
1449
/// Deserializes from V8 values.
15-
pub(super) struct Deserializer<'this, 'scope> {
50+
struct Deserializer<'this, 'scope> {
1651
common: DeserializerCommon<'this, 'scope>,
1752
input: Local<'scope, Value>,
1853
}
1954

2055
impl<'this, 'scope> Deserializer<'this, 'scope> {
2156
/// Creates a new deserializer from `input` in `scope`.
22-
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 {
2358
let input = Local::new(scope, input);
2459
let common = DeserializerCommon { scope, key_cache };
2560
Deserializer { input, common }
@@ -47,12 +82,22 @@ impl<'scope> DeserializerCommon<'_, 'scope> {
4782

4883
/// The possible errors that [`Deserializer`] can produce.
4984
#[derive(Debug, From)]
50-
pub(super) enum Error<'scope> {
51-
Value(Local<'scope, Value>),
52-
Exception(ExceptionThrown),
85+
enum Error<'scope> {
86+
Unthrown(ExceptionValue<'scope>),
87+
Thrown(ExceptionThrown),
5388
Custom(String),
5489
}
5590

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+
56101
impl de::Error for Error<'_> {
57102
fn custom(msg: impl fmt::Display) -> Self {
58103
Self::Custom(msg.to_string())
@@ -102,7 +147,7 @@ impl KeyCache {
102147
}
103148

104149
// Creates an interned [`v8::String`].
105-
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> {
106151
// Internalized v8 strings are significantly faster than "normal" v8 strings
107152
// since v8 deduplicates re-used strings minimizing new allocations
108153
// see: https://github.com/v8/v8/blob/14ac92e02cc3db38131a57e75e2392529f405f2f/include/v8.h#L3165-L3171
@@ -126,7 +171,7 @@ fn deref_local<'scope, T>(local: Local<'scope, T>) -> &'scope T {
126171
macro_rules! deserialize_primitive {
127172
($dmethod:ident, $t:ty) => {
128173
fn $dmethod(self) -> Result<$t, Self::Error> {
129-
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)
130175
}
131176
};
132177
}
@@ -287,7 +332,7 @@ impl<'de, 'scope: 'de> de::NamedProductAccess<'de> for ProductAccess<'_, 'scope>
287332
Ok(None)
288333
}
289334

290-
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> {
291336
let common = self.common.reborrow();
292337
// Extract the field's value.
293338
let input = self
@@ -336,7 +381,7 @@ impl<'de, 'this, 'scope: 'de> de::SumAccess<'de> for SumAccess<'this, 'scope> {
336381
impl<'de, 'this, 'scope: 'de> de::VariantAccess<'de> for Deserializer<'this, 'scope> {
337382
type Error = Error<'scope>;
338383

339-
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> {
340385
seed.deserialize(self)
341386
}
342387
}

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

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

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
#![allow(dead_code)]
22

3-
use super::de::{intern_field_name, KeyCache};
4-
use super::error::{exception_already_thrown, ExceptionThrown};
3+
use super::de::{get_or_create_key_cache, intern_field_name, KeyCache};
4+
use super::error::{exception_already_thrown, ExcResult, ExceptionThrown, RangeError, Throwable, TypeError};
55
use super::to_value::ToValue;
6-
use core::num::TryFromIntError;
76
use derive_more::From;
87
use spacetimedb_sats::{
98
i256,
@@ -12,8 +11,20 @@ use spacetimedb_sats::{
1211
};
1312
use v8::{Array, ArrayBuffer, HandleScope, IntegrityLevel, Local, Object, Uint8Array, Value};
1413

14+
/// Serializes `value` into a V8 into `scope`.
15+
pub(super) fn serialize_to_js<'scope>(
16+
scope: &mut HandleScope<'scope>,
17+
value: &impl Serialize,
18+
) -> ExcResult<Local<'scope, Value>> {
19+
let key_cache = get_or_create_key_cache(scope);
20+
let key_cache = &mut *key_cache.borrow_mut();
21+
value
22+
.serialize(Serializer::new(scope, key_cache))
23+
.map_err(|e| e.throw(scope))
24+
}
25+
1526
/// Deserializes to V8 values.
16-
pub(super) struct Serializer<'this, 'scope> {
27+
struct Serializer<'this, 'scope> {
1728
/// The scope to serialize values into.
1829
scope: &'this mut HandleScope<'scope>,
1930
/// A cache for frequently used strings.
@@ -36,11 +47,28 @@ impl<'this, 'scope> Serializer<'this, 'scope> {
3647

3748
/// The possible errors that [`Serializer`] can produce.
3849
#[derive(Debug, From)]
39-
pub(super) enum Error {
50+
enum Error {
4051
Custom(String),
41-
Exception(ExceptionThrown),
52+
Thrown(ExceptionThrown),
53+
#[from(ignore)]
4254
StringTooLarge(usize),
43-
ArrayLengthTooLarge(TryFromIntError),
55+
#[from(ignore)]
56+
ArrayLengthTooLarge(usize),
57+
}
58+
59+
impl<'scope> Throwable<'scope> for Error {
60+
fn throw(self, scope: &mut HandleScope<'scope>) -> ExceptionThrown {
61+
match self {
62+
Self::StringTooLarge(len) => {
63+
RangeError(format!("`{len}` bytes is too large to be a JS string")).throw(scope)
64+
}
65+
Self::ArrayLengthTooLarge(len) => {
66+
RangeError(format!("`{len}` elements are too many for a JS array")).throw(scope)
67+
}
68+
Self::Thrown(thrown) => thrown,
69+
Self::Custom(msg) => TypeError(msg).throw(scope),
70+
}
71+
}
4472
}
4573

4674
impl ser::Error for Error {
@@ -108,7 +136,7 @@ impl<'this, 'scope> ser::Serializer for Serializer<'this, 'scope> {
108136
}
109137

110138
fn serialize_array(self, len: usize) -> Result<Self::SerializeArray, Self::Error> {
111-
let len = len.try_into()?;
139+
let len = len.try_into().map_err(|_| Error::ArrayLengthTooLarge(len))?;
112140
Ok(SerializeArray {
113141
array: Array::new(self.scope, len),
114142
inner: self,
@@ -157,7 +185,7 @@ impl<'this, 'scope> ser::Serializer for Serializer<'this, 'scope> {
157185
}
158186

159187
/// Serializes array elements and finalizes the JS array.
160-
pub(super) struct SerializeArray<'this, 'scope> {
188+
struct SerializeArray<'this, 'scope> {
161189
inner: Serializer<'this, 'scope>,
162190
array: Local<'scope, Array>,
163191
next_index: u32,
@@ -187,7 +215,7 @@ impl<'scope> ser::SerializeArray for SerializeArray<'_, 'scope> {
187215
}
188216

189217
/// Serializes into JS objects where field names are turned into property names.
190-
pub(super) struct SerializeNamedProduct<'this, 'scope> {
218+
struct SerializeNamedProduct<'this, 'scope> {
191219
inner: Serializer<'this, 'scope>,
192220
object: Local<'scope, Object>,
193221
next_index: usize,
@@ -240,7 +268,8 @@ impl<'scope> ser::SerializeNamedProduct for SerializeNamedProduct<'_, 'scope> {
240268

241269
#[cfg(test)]
242270
mod test {
243-
use super::super::de::Deserializer;
271+
use crate::host::v8::de::deserialize_js_seed;
272+
244273
use super::super::to_value::test::with_scope;
245274
use super::*;
246275
use core::fmt::Debug;
@@ -259,15 +288,11 @@ mod test {
259288
seed: impl for<'de> DeserializeSeed<'de, Output = B>,
260289
) {
261290
with_scope(|scope| {
262-
let key_cache = &mut KeyCache::default();
263-
264291
// Convert to JS...
265-
let ser = Serializer::new(scope, key_cache);
266-
let js_val = rust_val.serialize(ser).unwrap();
292+
let js_val = serialize_to_js(scope, &rust_val).unwrap();
267293

268294
// ...and then back to Rust.
269-
let de = Deserializer::new(scope, js_val, key_cache);
270-
let rust_val_prime = seed.deserialize(de).unwrap();
295+
let rust_val_prime = deserialize_js_seed(scope, js_val, seed).unwrap();
271296

272297
// We should end up where we started.
273298
assert_eq!(rust_val, rust_val_prime);

0 commit comments

Comments
 (0)