Skip to content

Commit 1d1c271

Browse files
authored
V8: use ES modules instead of global (#3361)
# Description of Changes Tweaks V8 module support to use JS modules for `spacetimedb_sys` and the user module rather than using scripts and the global object. To this end, the code is also made more modular so that it e.g., cares less avoid global vs. modules. An `Object` with the functions is e.g., enough for the lowest level `call_call_reducer`. Some `.unwrap()`s are also removed. Also, `run_timeout_and_cb_every` is disabled, as it currently leads to UB due to bugs in the `v8` crate. # API and ABI breaking changes None # Expected complexity level and risk 2 # Testing Future work.
1 parent eac6c23 commit 1d1c271

File tree

12 files changed

+518
-315
lines changed

12 files changed

+518
-315
lines changed

crates/core/src/host/module_common.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,11 @@ impl ModuleCommon {
9292

9393
/// Runs the describer of modules in `run` and does some logging around it.
9494
pub(crate) fn run_describer<T>(
95-
log_traceback: impl FnOnce(&str, &str, &anyhow::Error),
95+
log_traceback: impl Copy + FnOnce(&str, &str, &anyhow::Error),
9696
run: impl FnOnce() -> anyhow::Result<T>,
9797
) -> Result<T, DescribeError> {
9898
let describer_func_name = DESCRIBE_MODULE_DUNDER;
99+
99100
let start = std::time::Instant::now();
100101
log::trace!("Start describer \"{describer_func_name}\"...");
101102

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

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
#![allow(dead_code)]
2-
31
use super::error::{exception_already_thrown, ExcResult, ExceptionThrown, ExceptionValue, Throwable, TypeError};
42
use super::from_value::{cast, FromValue};
5-
use super::string_const::{TAG, VALUE};
3+
use super::string::{TAG, VALUE};
4+
use super::FnRet;
65
use core::fmt;
76
use core::iter::{repeat_n, RepeatN};
87
use core::marker::PhantomData;
@@ -158,16 +157,12 @@ impl<'de, 'this, 'scope: 'de> de::Deserializer<'de> for Deserializer<'this, 'sco
158157
let object = cast!(scope, self.input, Object, "object for sum type `{}`", sum_name)?;
159158

160159
// Extract the `tag` field. It needs to contain a string.
161-
let tag = object
162-
.get(scope, tag_field.into())
163-
.ok_or_else(exception_already_thrown)?;
160+
let tag = property(scope, object, tag_field)?;
164161
let tag = cast!(scope, tag, v8::String, "string for sum tag of `{}`", sum_name)?;
165162

166163
// Extract the `value` field.
167164
let value_field = VALUE.string(scope);
168-
let value = object
169-
.get(scope, value_field.into())
170-
.ok_or_else(exception_already_thrown)?;
165+
let value = property(scope, object, value_field)?;
171166

172167
// Stitch it all together.
173168
visitor.visit_sum(SumAccess {
@@ -236,6 +231,15 @@ pub(super) fn intern_field_name<'scope>(
236231
v8_interned_string(scope, &field).into()
237232
}
238233

234+
/// Returns the property for `key` on `object`.
235+
pub(super) fn property<'scope>(
236+
scope: &PinScope<'scope, '_>,
237+
object: Local<'scope, Object>,
238+
key: impl Into<Local<'scope, Value>>,
239+
) -> FnRet<'scope> {
240+
object.get(scope, key.into()).ok_or_else(exception_already_thrown)
241+
}
242+
239243
impl<'de, 'scope: 'de> de::NamedProductAccess<'de> for ProductAccess<'_, 'scope, '_> {
240244
type Error = Error<'scope>;
241245

@@ -262,10 +266,7 @@ impl<'de, 'scope: 'de> de::NamedProductAccess<'de> for ProductAccess<'_, 'scope,
262266
}
263267

264268
// Extract the next value, to be processed in `get_field_value_seed`.
265-
let val = self
266-
.object
267-
.get(scope, key.into())
268-
.ok_or_else(exception_already_thrown)?;
269+
let val = property(scope, self.object, key)?;
269270
self.next_value = Some(val);
270271

271272
drop(field_names);

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

Lines changed: 64 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,16 @@
11
//! Utilities for error handling when dealing with V8.
22
3-
use crate::database_logger::{BacktraceFrame, BacktraceProvider, ModuleBacktrace};
4-
3+
use super::de::scratch_buf;
54
use super::serialize_to_js;
5+
use super::string::IntoJsString;
6+
use crate::database_logger::{BacktraceFrame, BacktraceProvider, ModuleBacktrace};
67
use core::fmt;
78
use spacetimedb_sats::Serialize;
89
use v8::{tc_scope, Exception, HandleScope, Local, PinScope, PinnedRef, StackFrame, StackTrace, TryCatch, Value};
910

1011
/// The result of trying to convert a [`Value`] in scope `'scope` to some type `T`.
1112
pub(super) type ValueResult<'scope, T> = Result<T, ExceptionValue<'scope>>;
1213

13-
/// Types that can convert into a JS string type.
14-
pub(super) trait IntoJsString {
15-
/// Converts `self` into a JS string.
16-
fn into_string<'scope>(self, scope: &PinScope<'scope, '_>) -> Local<'scope, v8::String>;
17-
}
18-
19-
impl IntoJsString for String {
20-
fn into_string<'scope>(self, scope: &PinScope<'scope, '_>) -> Local<'scope, v8::String> {
21-
v8::String::new(scope, &self).unwrap()
22-
}
23-
}
24-
2514
/// A JS exception value.
2615
///
2716
/// Newtyped for additional type safety and to track JS exceptions in the type system.
@@ -46,19 +35,74 @@ pub struct TypeError<M>(pub M);
4635

4736
impl<'scope, M: IntoJsString> IntoException<'scope> for TypeError<M> {
4837
fn into_exception(self, scope: &PinScope<'scope, '_>) -> ExceptionValue<'scope> {
49-
let msg = self.0.into_string(scope);
50-
ExceptionValue(Exception::type_error(scope, msg))
38+
match self.0.into_string(scope) {
39+
Ok(msg) => ExceptionValue(Exception::type_error(scope, msg)),
40+
Err(err) => err.into_range_error().into_exception(scope),
41+
}
5142
}
5243
}
5344

45+
/// Returns a "module not found" exception to be thrown.
46+
pub fn module_exception(scope: &mut PinScope<'_, '_>, spec: Local<'_, v8::String>) -> TypeError<String> {
47+
let mut buf = scratch_buf::<32>();
48+
let spec = spec.to_rust_cow_lossy(scope, &mut buf);
49+
TypeError(format!("Could not find module {spec:?}"))
50+
}
51+
5452
/// A type converting into a JS `RangeError` exception.
5553
#[derive(Copy, Clone)]
5654
pub struct RangeError<M>(pub M);
5755

5856
impl<'scope, M: IntoJsString> IntoException<'scope> for RangeError<M> {
5957
fn into_exception(self, scope: &PinScope<'scope, '_>) -> ExceptionValue<'scope> {
60-
let msg = self.0.into_string(scope);
61-
ExceptionValue(Exception::range_error(scope, msg))
58+
match self.0.into_string(scope) {
59+
Ok(msg) => ExceptionValue(Exception::range_error(scope, msg)),
60+
// This is not an infinite recursion.
61+
// The `r: RangeError<String>` that `StringTooLongError` produces
62+
// will always enter the branch above, as `r.0` is shorter than the maximum allowed.
63+
Err(err) => err.into_range_error().into_exception(scope),
64+
}
65+
}
66+
}
67+
68+
/// A non-JS string couldn't convert to JS as it was to long.
69+
#[derive(Debug)]
70+
pub(super) struct StringTooLongError {
71+
/// The length of the string that was too long (`len >` [`v8::String::MAX_LENGTH`]).
72+
pub(super) len: usize,
73+
/// A prefix of the string for the purpose of rendering an exception that aids the module dev.
74+
pub(super) prefix: String,
75+
}
76+
77+
impl StringTooLongError {
78+
/// Returns a new error that keeps a prefix of `string` and records its length.
79+
pub(super) fn new(string: &str) -> Self {
80+
let len = string.len();
81+
let prefix = string[0..16.max(len)].to_owned();
82+
Self { len, prefix }
83+
}
84+
85+
/// Converts the error to a [`RangeError<String>`].
86+
pub(super) fn into_range_error(self) -> RangeError<String> {
87+
let Self { len, prefix } = self;
88+
RangeError(format!(
89+
r#"The string "`{prefix}..`" of `{len}` bytes is too long for JS"#
90+
))
91+
}
92+
}
93+
94+
/// A non-JS array couldn't convert to JS as it was to long.
95+
#[derive(Debug)]
96+
pub(super) struct ArrayTooLongError {
97+
/// The length of the array that was too long (`len >` [`i32::MAX`]).
98+
pub(super) len: usize,
99+
}
100+
101+
impl ArrayTooLongError {
102+
/// Converts the error to a [`RangeError<String>`].
103+
pub(super) fn into_range_error(self) -> RangeError<String> {
104+
let Self { len } = self;
105+
RangeError(format!("`{len}` elements are too many for a JS array"))
62106
}
63107
}
64108

@@ -69,7 +113,7 @@ pub(super) struct TerminationError {
69113
}
70114

71115
impl TerminationError {
72-
/// Convert `anyhow::Error` to a termination error.
116+
/// Converts [`anyhow::Error`] to a termination error.
73117
pub(super) fn from_error<'scope>(
74118
scope: &PinScope<'scope, '_>,
75119
error: &anyhow::Error,
@@ -124,9 +168,6 @@ pub(crate) struct ExceptionThrown {
124168
/// A result where the error indicates that an exception has already been thrown in V8.
125169
pub(crate) type ExcResult<T> = Result<T, ExceptionThrown>;
126170

127-
/// The return type of a module -> host syscall.
128-
pub(super) type FnRet<'scope> = ExcResult<Local<'scope, Value>>;
129-
130171
/// Indicates that the JS side had thrown an exception.
131172
pub(super) fn exception_already_thrown() -> ExceptionThrown {
132173
ExceptionThrown { _priv: () }
@@ -252,6 +293,7 @@ impl ModuleBacktrace for JsStackTrace {
252293
pub(super) struct JsStackTraceFrame {
253294
line: usize,
254295
column: usize,
296+
#[allow(dead_code)]
255297
script_id: usize,
256298
script_name: Option<String>,
257299
fn_name: Option<String>,

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
#![allow(dead_code)]
2-
3-
use crate::host::v8::error::ExceptionValue;
4-
5-
use super::error::{IntoException as _, TypeError, ValueResult};
1+
use super::error::{ExceptionValue, IntoException as _, TypeError, ValueResult};
62
use bytemuck::{AnyBitPattern, NoUninit};
73
use spacetimedb_sats::{i256, u256};
84
use v8::{BigInt, Boolean, Int32, Local, Number, PinScope, Uint32, Value};

0 commit comments

Comments
 (0)