Skip to content

Commit 2539824

Browse files
authored
V8: Define and test call_describe_module (#3161)
# Description of Changes Defines a building piece `call_describe_module` that can be used to call the expected ABI function `describe_module` in JS. There might be further changes to this but this is a first working pass at it. # API and ABI breaking changes None # Expected complexity level and risk 2, this does not integrate with existing code reachable in production, but the exception handling required some googling/reading docs. # Testing A test `call_describe_module_works` is added that tests returning an empty `RawModuleDef` from JS.
1 parent b445620 commit 2539824

File tree

7 files changed

+220
-73
lines changed

7 files changed

+220
-73
lines changed

crates/core/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ test = ["spacetimedb-commitlog/test", "spacetimedb-datastore/test"]
138138
perfmap = []
139139

140140
[dev-dependencies]
141-
spacetimedb-lib = { path = "../lib", features = ["proptest"] }
141+
spacetimedb-lib = { path = "../lib", features = ["proptest", "test"] }
142142
spacetimedb-sats = { path = "../sats", features = ["proptest"] }
143143
spacetimedb-commitlog = { path = "../commitlog", features = ["test"] }
144144
spacetimedb-datastore = { path = "../datastore/", features = ["test"] }

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

Lines changed: 10 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
use super::error::{exception_already_thrown, ExcResult, ExceptionThrown, ExceptionValue, Throwable, TypeError};
44
use super::from_value::{cast, FromValue};
5-
use core::cell::RefCell;
5+
use super::key_cache::{get_or_create_key_cache, KeyCache};
66
use core::fmt;
77
use core::iter::{repeat_n, RepeatN};
88
use core::marker::PhantomData;
@@ -11,20 +11,7 @@ use derive_more::From;
1111
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;
15-
use v8::{Array, Global, HandleScope, Local, Name, Object, Uint8Array, Value};
16-
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-
}
14+
use v8::{Array, HandleScope, Local, Name, Object, Uint8Array, Value};
2815

2916
/// Deserializes a `T` from `val` in `scope`, using `seed` for any context needed.
3017
pub(super) fn deserialize_js_seed<'de, T: DeserializeSeed<'de>>(
@@ -109,51 +96,6 @@ fn scratch_buf<const N: usize>() -> [MaybeUninit<u8>; N] {
10996
[const { MaybeUninit::uninit() }; N]
11097
}
11198

112-
/// A cache for frequently used strings to avoid re-interning them.
113-
#[derive(Default)]
114-
pub(super) struct KeyCache {
115-
/// The `tag` property for sum values in JS.
116-
tag: Option<Global<v8::String>>,
117-
/// The `value` property for sum values in JS.
118-
value: Option<Global<v8::String>>,
119-
}
120-
121-
impl KeyCache {
122-
/// Returns the `tag` property name.
123-
pub(super) fn tag<'scope>(&mut self, scope: &mut HandleScope<'scope>) -> Local<'scope, v8::String> {
124-
Self::get_or_create_key(scope, &mut self.tag, "tag")
125-
}
126-
127-
/// Returns the `value` property name.
128-
pub(super) fn value<'scope>(&mut self, scope: &mut HandleScope<'scope>) -> Local<'scope, v8::String> {
129-
Self::get_or_create_key(scope, &mut self.value, "value")
130-
}
131-
132-
/// Returns an interned string corresponding to `string`
133-
/// and memoizes the creation on the v8 side.
134-
fn get_or_create_key<'scope>(
135-
scope: &mut HandleScope<'scope>,
136-
slot: &mut Option<Global<v8::String>>,
137-
string: &str,
138-
) -> Local<'scope, v8::String> {
139-
if let Some(s) = &*slot {
140-
v8::Local::new(scope, s)
141-
} else {
142-
let s = v8_interned_string(scope, string);
143-
*slot = Some(v8::Global::new(scope, s));
144-
s
145-
}
146-
}
147-
}
148-
149-
// Creates an interned [`v8::String`].
150-
fn v8_interned_string<'scope>(scope: &mut HandleScope<'scope>, field: &str) -> Local<'scope, v8::String> {
151-
// Internalized v8 strings are significantly faster than "normal" v8 strings
152-
// since v8 deduplicates re-used strings minimizing new allocations
153-
// see: https://github.com/v8/v8/blob/14ac92e02cc3db38131a57e75e2392529f405f2f/include/v8.h#L3165-L3171
154-
v8::String::new_from_utf8(scope, field.as_ref(), v8::NewStringType::Internalized).unwrap()
155-
}
156-
15799
/// Extracts a reference `&'scope T` from an owned V8 [`Local<'scope, T>`].
158100
///
159101
/// The lifetime `'scope` is that of the [`HandleScope<'scope>`].
@@ -280,6 +222,14 @@ struct ProductAccess<'this, 'scope> {
280222
index: usize,
281223
}
282224

225+
// Creates an interned [`v8::String`].
226+
pub(super) fn v8_interned_string<'scope>(scope: &mut HandleScope<'scope>, field: &str) -> Local<'scope, v8::String> {
227+
// Internalized v8 strings are significantly faster than "normal" v8 strings
228+
// since v8 deduplicates re-used strings minimizing new allocations
229+
// see: https://github.com/v8/v8/blob/14ac92e02cc3db38131a57e75e2392529f405f2f/include/v8.h#L3165-L3171
230+
v8::String::new_from_utf8(scope, field.as_ref(), v8::NewStringType::Internalized).unwrap()
231+
}
232+
283233
/// Normalizes `field` into an interned `v8::String`.
284234
pub(super) fn intern_field_name<'scope>(
285235
scope: &mut HandleScope<'scope>,

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

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Utilities for error handling when dealing with V8.
22
3-
use v8::{Exception, HandleScope, Local, Value};
3+
use v8::{Exception, HandleScope, Local, TryCatch, Value};
44

55
/// The result of trying to convert a [`Value`] in scope `'scope` to some type `T`.
66
pub(super) type ValueResult<'scope, T> = Result<T, ExceptionValue<'scope>>;
@@ -86,3 +86,61 @@ impl<'scope, T: IntoException<'scope>> Throwable<'scope> for T {
8686
exception_already_thrown()
8787
}
8888
}
89+
90+
/// Either an error outside V8 JS execution, or an exception inside.
91+
#[derive(Debug)]
92+
pub(super) enum ErrorOrException<Exc> {
93+
Err(anyhow::Error),
94+
Exception(Exc),
95+
}
96+
97+
impl<E> From<anyhow::Error> for ErrorOrException<E> {
98+
fn from(e: anyhow::Error) -> Self {
99+
Self::Err(e)
100+
}
101+
}
102+
103+
impl From<ExceptionThrown> for ErrorOrException<ExceptionThrown> {
104+
fn from(e: ExceptionThrown) -> Self {
105+
Self::Exception(e)
106+
}
107+
}
108+
109+
impl From<ErrorOrException<JsError>> for anyhow::Error {
110+
fn from(err: ErrorOrException<JsError>) -> Self {
111+
match err {
112+
ErrorOrException::Err(e) => e,
113+
ErrorOrException::Exception(e) => e.into(),
114+
}
115+
}
116+
}
117+
118+
/// A JS exception turned into an error.
119+
#[derive(thiserror::Error, Debug)]
120+
#[error("js error: {msg:?}")]
121+
pub(super) struct JsError {
122+
msg: String,
123+
}
124+
125+
impl JsError {
126+
/// Turns a caught JS exception in `scope` into a [`JSError`].
127+
fn from_caught(scope: &mut TryCatch<'_, HandleScope<'_>>) -> Self {
128+
let msg = match scope.message() {
129+
Some(msg) => msg.get(scope).to_rust_string_lossy(scope),
130+
None => "unknown error".to_owned(),
131+
};
132+
Self { msg }
133+
}
134+
}
135+
136+
/// Run `body` within a try-catch context and capture any JS exception thrown as a [`JsError`].
137+
pub(super) fn catch_exception<'scope, T>(
138+
scope: &mut HandleScope<'scope>,
139+
body: impl FnOnce(&mut HandleScope<'scope>) -> Result<T, ErrorOrException<ExceptionThrown>>,
140+
) -> Result<T, ErrorOrException<JsError>> {
141+
let scope = &mut TryCatch::new(scope);
142+
body(scope).map_err(|e| match e {
143+
ErrorOrException::Err(e) => ErrorOrException::Err(e),
144+
ErrorOrException::Exception(_) => ErrorOrException::Exception(JsError::from_caught(scope)),
145+
})
146+
}

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use super::de::v8_interned_string;
2+
use core::cell::RefCell;
3+
use std::rc::Rc;
4+
use v8::{Global, HandleScope, Local};
5+
6+
/// Returns a `KeyCache` for the current `scope`.
7+
///
8+
/// Creates the cache in the scope if it doesn't exist yet.
9+
pub(super) fn get_or_create_key_cache(scope: &mut HandleScope<'_>) -> Rc<RefCell<KeyCache>> {
10+
let context = scope.get_current_context();
11+
context.get_slot::<RefCell<KeyCache>>().unwrap_or_else(|| {
12+
let cache = Rc::default();
13+
context.set_slot(Rc::clone(&cache));
14+
cache
15+
})
16+
}
17+
18+
/// A cache for frequently used strings to avoid re-interning them.
19+
#[derive(Default)]
20+
pub(super) struct KeyCache {
21+
/// The `tag` property for sum values in JS.
22+
tag: Option<Global<v8::String>>,
23+
/// The `value` property for sum values in JS.
24+
value: Option<Global<v8::String>>,
25+
/// The `describe_module` property on the global proxy object.
26+
describe_module: Option<Global<v8::String>>,
27+
}
28+
29+
impl KeyCache {
30+
/// Returns the `tag` property name.
31+
pub(super) fn tag<'scope>(&mut self, scope: &mut HandleScope<'scope>) -> Local<'scope, v8::String> {
32+
Self::get_or_create_key(scope, &mut self.tag, "tag")
33+
}
34+
35+
/// Returns the `value` property name.
36+
pub(super) fn value<'scope>(&mut self, scope: &mut HandleScope<'scope>) -> Local<'scope, v8::String> {
37+
Self::get_or_create_key(scope, &mut self.value, "value")
38+
}
39+
40+
/// Returns the `describe_module` property name.
41+
pub(super) fn describe_module<'scope>(&mut self, scope: &mut HandleScope<'scope>) -> Local<'scope, v8::String> {
42+
Self::get_or_create_key(scope, &mut self.describe_module, "describe_module")
43+
}
44+
45+
/// Returns an interned string corresponding to `string`
46+
/// and memoizes the creation on the v8 side.
47+
fn get_or_create_key<'scope>(
48+
scope: &mut HandleScope<'scope>,
49+
slot: &mut Option<Global<v8::String>>,
50+
string: &str,
51+
) -> Local<'scope, v8::String> {
52+
if let Some(s) = &*slot {
53+
v8::Local::new(scope, s)
54+
} else {
55+
let s = v8_interned_string(scope, string);
56+
*slot = Some(v8::Global::new(scope, s));
57+
s
58+
}
59+
}
60+
}

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

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
1-
use super::module_host::CallReducerParams;
2-
use crate::{
3-
host::{
4-
module_common::{build_common_module_from_raw, ModuleCommon},
5-
module_host::{DynModule, Module, ModuleInfo, ModuleInstance, ModuleRuntime},
6-
Scheduler,
7-
},
8-
module_host_context::ModuleCreationContext,
9-
replica_context::ReplicaContext,
10-
};
1+
#![allow(dead_code)]
2+
3+
use super::module_common::{build_common_module_from_raw, ModuleCommon};
4+
use super::module_host::{CallReducerParams, DynModule, Module, ModuleInfo, ModuleInstance, ModuleRuntime};
5+
use crate::host::v8::error::{exception_already_thrown, Throwable};
6+
use crate::{host::Scheduler, module_host_context::ModuleCreationContext, replica_context::ReplicaContext};
117
use anyhow::anyhow;
8+
use de::deserialize_js;
9+
use error::catch_exception;
10+
use from_value::cast;
11+
use key_cache::get_or_create_key_cache;
1212
use spacetimedb_datastore::locking_tx_datastore::MutTxId;
13+
use spacetimedb_lib::RawModuleDef;
1314
use std::sync::{Arc, LazyLock};
15+
use v8::{Function, HandleScope};
1416

1517
mod de;
1618
mod error;
1719
mod from_value;
20+
mod key_cache;
1821
mod ser;
1922
mod to_value;
2023

@@ -130,3 +133,73 @@ impl ModuleInstance for JsInstance {
130133
todo!()
131134
}
132135
}
136+
137+
// Calls the `describe_module` function on the global proxy object to extract a [`RawModuleDef`].
138+
fn call_describe_module(scope: &mut HandleScope<'_>) -> anyhow::Result<RawModuleDef> {
139+
// Get a cached version of the `describe_module` property.
140+
let key_cache = get_or_create_key_cache(scope);
141+
let describe_module_key = key_cache.borrow_mut().describe_module(scope).into();
142+
143+
catch_exception(scope, |scope| {
144+
// Get the function on the global proxy object.
145+
let object = scope
146+
.get_current_context()
147+
.global(scope)
148+
.get(scope, describe_module_key)
149+
.ok_or_else(exception_already_thrown)?;
150+
151+
// Convert to a function.
152+
let fun =
153+
cast!(scope, object, Function, "function export for `describe_module`").map_err(|e| e.throw(scope))?;
154+
155+
// Call the function.
156+
let receiver = v8::undefined(scope).into();
157+
let raw_mod_js = fun.call(scope, receiver, &[]).ok_or_else(exception_already_thrown)?;
158+
159+
// Deserialize the raw module.
160+
let raw_mod: RawModuleDef = deserialize_js(scope, raw_mod_js)?;
161+
Ok(raw_mod)
162+
})
163+
.map_err(Into::into)
164+
}
165+
166+
#[cfg(test)]
167+
mod test {
168+
use super::*;
169+
use crate::host::v8::to_value::test::with_scope;
170+
use v8::{Local, Value};
171+
172+
fn with_script<R>(
173+
code: &str,
174+
logic: impl for<'scope> FnOnce(&mut HandleScope<'scope>, Local<'scope, Value>) -> R,
175+
) -> R {
176+
with_scope(|scope| {
177+
let code = v8::String::new(scope, code).unwrap();
178+
let script_val = v8::Script::compile(scope, code, None).unwrap().run(scope).unwrap();
179+
logic(scope, script_val)
180+
})
181+
}
182+
183+
#[test]
184+
fn call_describe_module_works() {
185+
let code = r#"
186+
function describe_module() {
187+
return {
188+
"tag": "V9",
189+
"value": {
190+
"typespace": {
191+
"types": [],
192+
},
193+
"tables": [],
194+
"reducers": [],
195+
"types": [],
196+
"misc_exports": [],
197+
"row_level_security": [],
198+
},
199+
};
200+
}
201+
"#;
202+
let raw_mod = with_script(code, |scope, _| call_describe_module(scope).unwrap());
203+
assert_eq!(raw_mod, RawModuleDef::V9(<_>::default()));
204+
}
205+
}

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

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

3-
use super::de::{get_or_create_key_cache, intern_field_name, KeyCache};
3+
use super::de::intern_field_name;
44
use super::error::{exception_already_thrown, ExcResult, ExceptionThrown, RangeError, Throwable, TypeError};
5+
use super::key_cache::{get_or_create_key_cache, KeyCache};
56
use super::to_value::ToValue;
67
use derive_more::From;
78
use spacetimedb_sats::{

crates/lib/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ impl TableDesc {
119119
}
120120

121121
#[derive(Debug, Clone, SpacetimeType)]
122+
#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))]
122123
#[sats(crate = crate)]
123124
pub struct ReducerDef {
124125
pub name: Box<str>,
@@ -189,6 +190,7 @@ impl_serialize!([] ReducerArgsWithSchema<'_>, (self, ser) => {
189190

190191
//WARNING: Change this structure (or any of their members) is an ABI change.
191192
#[derive(Debug, Clone, Default, SpacetimeType)]
193+
#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))]
192194
#[sats(crate = crate)]
193195
pub struct RawModuleDefV8 {
194196
pub typespace: sats::Typespace,
@@ -213,6 +215,7 @@ impl RawModuleDefV8 {
213215
///
214216
/// This is what is actually returned by the module when `__describe_module__` is called, serialized to BSATN.
215217
#[derive(Debug, Clone, SpacetimeType)]
218+
#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))]
216219
#[sats(crate = crate)]
217220
#[non_exhaustive]
218221
pub enum RawModuleDef {
@@ -340,12 +343,14 @@ impl TypespaceBuilder for ModuleDefBuilder {
340343

341344
// an enum to keep it extensible without breaking abi
342345
#[derive(Debug, Clone, SpacetimeType)]
346+
#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))]
343347
#[sats(crate = crate)]
344348
pub enum MiscModuleExport {
345349
TypeAlias(TypeAlias),
346350
}
347351

348352
#[derive(Debug, Clone, SpacetimeType)]
353+
#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))]
349354
#[sats(crate = crate)]
350355
pub struct TypeAlias {
351356
pub name: String,

0 commit comments

Comments
 (0)