Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ test = ["spacetimedb-commitlog/test", "spacetimedb-datastore/test"]
perfmap = []

[dev-dependencies]
spacetimedb-lib = { path = "../lib", features = ["proptest"] }
spacetimedb-lib = { path = "../lib", features = ["proptest", "test"] }
spacetimedb-sats = { path = "../sats", features = ["proptest"] }
spacetimedb-commitlog = { path = "../commitlog", features = ["test"] }
spacetimedb-datastore = { path = "../datastore/", features = ["test"] }
Expand Down
70 changes: 10 additions & 60 deletions crates/core/src/host/v8/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use super::error::{exception_already_thrown, ExcResult, ExceptionThrown, ExceptionValue, Throwable, TypeError};
use super::from_value::{cast, FromValue};
use core::cell::RefCell;
use super::key_cache::{get_or_create_key_cache, KeyCache};
use core::fmt;
use core::iter::{repeat_n, RepeatN};
use core::marker::PhantomData;
Expand All @@ -11,20 +11,7 @@ use derive_more::From;
use spacetimedb_sats::de::{self, ArrayVisitor, DeserializeSeed, ProductVisitor, SliceVisitor, SumVisitor};
use spacetimedb_sats::{i256, u256};
use std::borrow::{Borrow, Cow};
use std::rc::Rc;
use v8::{Array, Global, HandleScope, Local, Name, Object, Uint8Array, Value};

/// Returns a `KeyCache` for the current `scope`.
///
/// Creates the cache in the scope if it doesn't exist yet.
pub(super) fn get_or_create_key_cache(scope: &mut HandleScope<'_>) -> Rc<RefCell<KeyCache>> {
let context = scope.get_current_context();
context.get_slot::<RefCell<KeyCache>>().unwrap_or_else(|| {
let cache = Rc::default();
context.set_slot(Rc::clone(&cache));
cache
})
}
use v8::{Array, HandleScope, Local, Name, Object, Uint8Array, Value};

/// Deserializes a `T` from `val` in `scope`, using `seed` for any context needed.
pub(super) fn deserialize_js_seed<'de, T: DeserializeSeed<'de>>(
Expand Down Expand Up @@ -109,51 +96,6 @@ fn scratch_buf<const N: usize>() -> [MaybeUninit<u8>; 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<Global<v8::String>>,
/// The `value` property for sum values in JS.
value: Option<Global<v8::String>>,
}

impl KeyCache {
/// Returns the `tag` property name.
pub(super) fn tag<'scope>(&mut self, scope: &mut HandleScope<'scope>) -> Local<'scope, v8::String> {
Self::get_or_create_key(scope, &mut self.tag, "tag")
}

/// Returns the `value` property name.
pub(super) fn value<'scope>(&mut self, scope: &mut HandleScope<'scope>) -> Local<'scope, v8::String> {
Self::get_or_create_key(scope, &mut self.value, "value")
}

/// Returns an interned string corresponding to `string`
/// and memoizes the creation on the v8 side.
fn get_or_create_key<'scope>(
scope: &mut HandleScope<'scope>,
slot: &mut Option<Global<v8::String>>,
string: &str,
) -> Local<'scope, 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<'scope>(scope: &mut HandleScope<'scope>, field: &str) -> Local<'scope, 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 `&'scope T` from an owned V8 [`Local<'scope, T>`].
///
/// The lifetime `'scope` is that of the [`HandleScope<'scope>`].
Expand Down Expand Up @@ -280,6 +222,14 @@ struct ProductAccess<'this, 'scope> {
index: usize,
}

// Creates an interned [`v8::String`].
pub(super) fn v8_interned_string<'scope>(scope: &mut HandleScope<'scope>, field: &str) -> Local<'scope, 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()
}

/// Normalizes `field` into an interned `v8::String`.
pub(super) fn intern_field_name<'scope>(
scope: &mut HandleScope<'scope>,
Expand Down
60 changes: 59 additions & 1 deletion crates/core/src/host/v8/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Utilities for error handling when dealing with V8.

use v8::{Exception, HandleScope, Local, Value};
use v8::{Exception, HandleScope, Local, TryCatch, Value};

/// The result of trying to convert a [`Value`] in scope `'scope` to some type `T`.
pub(super) type ValueResult<'scope, T> = Result<T, ExceptionValue<'scope>>;
Expand Down Expand Up @@ -86,3 +86,61 @@ impl<'scope, T: IntoException<'scope>> Throwable<'scope> for T {
exception_already_thrown()
}
}

/// Either an error outside V8 JS execution, or an exception inside.
#[derive(Debug)]
pub(super) enum ErrorOrException<Exc> {
Err(anyhow::Error),
Exception(Exc),
}

impl<E> From<anyhow::Error> for ErrorOrException<E> {
fn from(e: anyhow::Error) -> Self {
Self::Err(e)
}
}

impl From<ExceptionThrown> for ErrorOrException<ExceptionThrown> {
fn from(e: ExceptionThrown) -> Self {
Self::Exception(e)
}
}

impl From<ErrorOrException<JsError>> for anyhow::Error {
fn from(err: ErrorOrException<JsError>) -> Self {
match err {
ErrorOrException::Err(e) => e,
ErrorOrException::Exception(e) => e.into(),
}
}
}

/// A JS exception turned into an error.
#[derive(thiserror::Error, Debug)]
#[error("js error: {msg:?}")]
pub(super) struct JsError {
msg: String,
}

impl JsError {
/// Turns a caught JS exception in `scope` into a [`JSError`].
fn from_caught(scope: &mut TryCatch<'_, HandleScope<'_>>) -> Self {
let msg = match scope.message() {
Some(msg) => msg.get(scope).to_rust_string_lossy(scope),
None => "unknown error".to_owned(),
};
Self { msg }
}
}

/// Run `body` within a try-catch context and capture any JS exception thrown as a [`JsError`].
pub(super) fn catch_exception<'scope, T>(
scope: &mut HandleScope<'scope>,
body: impl FnOnce(&mut HandleScope<'scope>) -> Result<T, ErrorOrException<ExceptionThrown>>,
) -> Result<T, ErrorOrException<JsError>> {
let scope = &mut TryCatch::new(scope);
body(scope).map_err(|e| match e {
ErrorOrException::Err(e) => ErrorOrException::Err(e),
ErrorOrException::Exception(_) => ErrorOrException::Exception(JsError::from_caught(scope)),
})
}
60 changes: 60 additions & 0 deletions crates/core/src/host/v8/key_cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use super::de::v8_interned_string;
use core::cell::RefCell;
use std::rc::Rc;
use v8::{Global, HandleScope, Local};

/// Returns a `KeyCache` for the current `scope`.
///
/// Creates the cache in the scope if it doesn't exist yet.
pub(super) fn get_or_create_key_cache(scope: &mut HandleScope<'_>) -> Rc<RefCell<KeyCache>> {
let context = scope.get_current_context();
context.get_slot::<RefCell<KeyCache>>().unwrap_or_else(|| {
let cache = Rc::default();
context.set_slot(Rc::clone(&cache));
cache
})
}

/// 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<Global<v8::String>>,
/// The `value` property for sum values in JS.
value: Option<Global<v8::String>>,
/// The `describe_module` property on the global proxy object.
describe_module: Option<Global<v8::String>>,
}

impl KeyCache {
/// Returns the `tag` property name.
pub(super) fn tag<'scope>(&mut self, scope: &mut HandleScope<'scope>) -> Local<'scope, v8::String> {
Self::get_or_create_key(scope, &mut self.tag, "tag")
}

/// Returns the `value` property name.
pub(super) fn value<'scope>(&mut self, scope: &mut HandleScope<'scope>) -> Local<'scope, v8::String> {
Self::get_or_create_key(scope, &mut self.value, "value")
}

/// Returns the `describe_module` property name.
pub(super) fn describe_module<'scope>(&mut self, scope: &mut HandleScope<'scope>) -> Local<'scope, v8::String> {
Self::get_or_create_key(scope, &mut self.describe_module, "describe_module")
}

/// Returns an interned string corresponding to `string`
/// and memoizes the creation on the v8 side.
fn get_or_create_key<'scope>(
scope: &mut HandleScope<'scope>,
slot: &mut Option<Global<v8::String>>,
string: &str,
) -> Local<'scope, 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
}
}
}
93 changes: 83 additions & 10 deletions crates/core/src/host/v8/mod.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
use super::module_host::CallReducerParams;
use crate::{
host::{
module_common::{build_common_module_from_raw, ModuleCommon},
module_host::{DynModule, Module, ModuleInfo, ModuleInstance, ModuleRuntime},
Scheduler,
},
module_host_context::ModuleCreationContext,
replica_context::ReplicaContext,
};
#![allow(dead_code)]

use super::module_common::{build_common_module_from_raw, ModuleCommon};
use super::module_host::{CallReducerParams, DynModule, Module, ModuleInfo, ModuleInstance, ModuleRuntime};
use crate::host::v8::error::{exception_already_thrown, Throwable};
use crate::{host::Scheduler, module_host_context::ModuleCreationContext, replica_context::ReplicaContext};
use anyhow::anyhow;
use de::deserialize_js;
use error::catch_exception;
use from_value::cast;
use key_cache::get_or_create_key_cache;
use spacetimedb_datastore::locking_tx_datastore::MutTxId;
use spacetimedb_lib::RawModuleDef;
use std::sync::{Arc, LazyLock};
use v8::{Function, HandleScope};

mod de;
mod error;
mod from_value;
mod key_cache;
mod ser;
mod to_value;

Expand Down Expand Up @@ -130,3 +133,73 @@ impl ModuleInstance for JsInstance {
todo!()
}
}

// Calls the `describe_module` function on the global proxy object to extract a [`RawModuleDef`].
fn call_describe_module(scope: &mut HandleScope<'_>) -> anyhow::Result<RawModuleDef> {
// Get a cached version of the `describe_module` property.
let key_cache = get_or_create_key_cache(scope);
let describe_module_key = key_cache.borrow_mut().describe_module(scope).into();

catch_exception(scope, |scope| {
// Get the function on the global proxy object.
let object = scope
.get_current_context()
.global(scope)
.get(scope, describe_module_key)
.ok_or_else(exception_already_thrown)?;

// Convert to a function.
let fun =
cast!(scope, object, Function, "function export for `describe_module`").map_err(|e| e.throw(scope))?;

// Call the function.
let receiver = v8::undefined(scope).into();
let raw_mod_js = fun.call(scope, receiver, &[]).ok_or_else(exception_already_thrown)?;

// Deserialize the raw module.
let raw_mod: RawModuleDef = deserialize_js(scope, raw_mod_js)?;
Ok(raw_mod)
})
.map_err(Into::into)
}

#[cfg(test)]
mod test {
use super::*;
use crate::host::v8::to_value::test::with_scope;
use v8::{Local, Value};

fn with_script<R>(
code: &str,
logic: impl for<'scope> FnOnce(&mut HandleScope<'scope>, Local<'scope, Value>) -> R,
) -> R {
with_scope(|scope| {
let code = v8::String::new(scope, code).unwrap();
let script_val = v8::Script::compile(scope, code, None).unwrap().run(scope).unwrap();
logic(scope, script_val)
})
}

#[test]
fn call_describe_module_works() {
let code = r#"
function describe_module() {
return {
"tag": "V9",
"value": {
"typespace": {
"types": [],
},
"tables": [],
"reducers": [],
"types": [],
"misc_exports": [],
"row_level_security": [],
},
};
}
"#;
let raw_mod = with_script(code, |scope, _| call_describe_module(scope).unwrap());
assert_eq!(raw_mod, RawModuleDef::V9(<_>::default()));
}
}
3 changes: 2 additions & 1 deletion crates/core/src/host/v8/ser.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#![allow(dead_code)]

use super::de::{get_or_create_key_cache, intern_field_name, KeyCache};
use super::de::intern_field_name;
use super::error::{exception_already_thrown, ExcResult, ExceptionThrown, RangeError, Throwable, TypeError};
use super::key_cache::{get_or_create_key_cache, KeyCache};
use super::to_value::ToValue;
use derive_more::From;
use spacetimedb_sats::{
Expand Down
5 changes: 5 additions & 0 deletions crates/lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ impl TableDesc {
}

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

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

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

#[derive(Debug, Clone, SpacetimeType)]
#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))]
#[sats(crate = crate)]
pub struct TypeAlias {
pub name: String,
Expand Down
Loading