Skip to content

Commit 9a6e35a

Browse files
coolreader18Centrilbfops
authored
Add proper V8 sys module versioning (#3527)
# Description of Changes The previous `syscall.rs` is now in `syscall/v1.rs`, and there's proper parsing of the `[email protected]` version number done now. This lets us more easily extend the ABI in the future. I also changed how slots are stored in the Context embedder data, to make it easier to add more in the future and more performant to fetch them. # Expected complexity level and risk 1 # Testing n/a, reorganization --------- Signed-off-by: Noa <[email protected]> Co-authored-by: Mazdak Farrokhzad <[email protected]> Co-authored-by: Zeke Foppa <[email protected]>
1 parent 1874b0e commit 9a6e35a

File tree

7 files changed

+306
-144
lines changed

7 files changed

+306
-144
lines changed

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

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
//! Utilities for error handling when dealing with V8.
22
3-
use super::de::scratch_buf;
43
use super::serialize_to_js;
54
use super::string::IntoJsString;
65
use crate::{
@@ -46,13 +45,6 @@ impl<'scope, M: IntoJsString> IntoException<'scope> for TypeError<M> {
4645
}
4746
}
4847

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

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

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use self::error::{
55
};
66
use self::ser::serialize_to_js;
77
use self::string::{str_from_ident, IntoJsString};
8-
use self::syscall::{call_call_reducer, call_describe_module, call_reducer_fun, resolve_sys_module, FnRet};
8+
use self::syscall::{
9+
call_call_reducer, call_describe_module, get_hook, resolve_sys_module, FnRet, HookFunction, ModuleHookKey,
10+
};
911
use super::module_common::{build_common_module_from_raw, run_describer, ModuleCommon};
1012
use super::module_host::{CallProcedureParams, CallReducerParams, Module, ModuleInfo, ModuleRuntime};
1113
use super::UpdateDatabaseResult;
@@ -18,6 +20,7 @@ use crate::host::{ReducerCallResult, Scheduler};
1820
use crate::module_host_context::{ModuleCreationContext, ModuleCreationContextLimited};
1921
use crate::replica_context::ReplicaContext;
2022
use crate::util::asyncify;
23+
use anyhow::Context as _;
2124
use core::str;
2225
use itertools::Either;
2326
use spacetimedb_datastore::locking_tx_datastore::MutTxId;
@@ -329,12 +332,13 @@ fn startup_instance_worker<'scope>(
329332
scope: &mut PinScope<'scope, '_>,
330333
program: Arc<str>,
331334
module_or_mcc: Either<ModuleCommon, ModuleCreationContextLimited>,
332-
) -> anyhow::Result<(Local<'scope, Function>, Either<ModuleCommon, ModuleCommon>)> {
335+
) -> anyhow::Result<(HookFunction<'scope>, Either<ModuleCommon, ModuleCommon>)> {
333336
// Start-up the user's module.
334337
eval_user_module_catch(scope, &program).map_err(DescribeError::Setup)?;
335338

336339
// Find the `__call_reducer__` function.
337-
let call_reducer_fun = catch_exception(scope, |scope| Ok(call_reducer_fun(scope)?)).map_err(|(e, _)| e)?;
340+
let call_reducer_fun =
341+
get_hook(scope, ModuleHookKey::CallReducer).context("The `spacetimedb/server` module was never imported")?;
338342

339343
// If we don't have a module, make one.
340344
let module_common = match module_or_mcc {
@@ -578,7 +582,7 @@ fn call_reducer<'scope>(
578582
instance_common: &mut InstanceCommon,
579583
replica_ctx: &ReplicaContext,
580584
scope: &mut PinScope<'scope, '_>,
581-
fun: Local<'scope, Function>,
585+
fun: HookFunction<'_>,
582586
tx: Option<MutTxId>,
583587
params: CallReducerParams,
584588
) -> (super::ReducerCallResult, bool) {
@@ -655,26 +659,30 @@ fn extract_description<'scope>(
655659
|a, b, c| log_traceback(replica_ctx, a, b, c),
656660
|| {
657661
catch_exception(scope, |scope| {
658-
let def = call_describe_module(scope)?;
662+
let describe_module = get_hook(scope, ModuleHookKey::DescribeModule)
663+
.context("The `spacetimedb/server` package was never imported into the module")?;
664+
let def = call_describe_module(scope, describe_module)?;
659665
Ok(def)
660666
})
661667
.map_err(|(e, _)| e)
662668
.map_err(Into::into)
663669
},
664670
)
665671
}
672+
666673
#[cfg(test)]
667674
mod test {
668675
use super::to_value::test::with_scope;
669676
use super::*;
677+
use crate::host::v8::error::{ErrorOrException, ExceptionThrown};
670678
use crate::host::wasm_common::module_host_actor::ReducerOp;
671679
use crate::host::ArgsTuple;
672680
use spacetimedb_lib::{ConnectionId, Identity};
673681
use spacetimedb_primitives::ReducerId;
674682

675683
fn with_module_catch<T>(
676684
code: &str,
677-
logic: impl for<'scope> FnOnce(&mut PinScope<'scope, '_>) -> ExcResult<T>,
685+
logic: impl for<'scope> FnOnce(&mut PinScope<'scope, '_>) -> Result<T, ErrorOrException<ExceptionThrown>>,
678686
) -> anyhow::Result<T> {
679687
with_scope(|scope| {
680688
eval_user_module_catch(scope, code).unwrap();
@@ -691,7 +699,7 @@ mod test {
691699
fn call_call_reducer_works() {
692700
let call = |code| {
693701
with_module_catch(code, |scope| {
694-
let fun = call_reducer_fun(scope)?;
702+
let fun = get_hook(scope, ModuleHookKey::CallReducer).unwrap();
695703
let op = ReducerOp {
696704
id: ReducerId(42),
697705
name: "foobar",
@@ -700,7 +708,7 @@ mod test {
700708
timestamp: Timestamp::from_micros_since_unix_epoch(24),
701709
args: &ArgsTuple::nullary(),
702710
};
703-
call_call_reducer(scope, fun, op)
711+
Ok(call_call_reducer(scope, fun, op)?)
704712
})
705713
};
706714

@@ -769,7 +777,11 @@ js error Uncaught Error: foobar
769777
},
770778
})
771779
"#;
772-
let raw_mod = with_module_catch(code, call_describe_module).map_err(|e| e.to_string());
780+
let raw_mod = with_module_catch(code, |scope| {
781+
let describe_module = get_hook(scope, ModuleHookKey::DescribeModule).unwrap();
782+
call_describe_module(scope, describe_module)
783+
})
784+
.map_err(|e| e.to_string());
773785
assert_eq!(raw_mod, Ok(RawModuleDef::V9(<_>::default())));
774786
}
775787
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ impl StringConst {
3636
v8::String::new_from_onebyte_const(scope, &self.0)
3737
.expect("`create_external_onebyte_const` should've asserted `.len() < kMaxLength`")
3838
}
39+
40+
/// Returns the backing string slice.
41+
pub(super) fn as_str(&'static self) -> &'static str {
42+
self.0.as_str()
43+
}
3944
}
4045

4146
/// Converts an identifier to a compile-time ASCII string.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use std::cell::OnceCell;
2+
use std::rc::Rc;
3+
4+
use enum_map::EnumMap;
5+
use v8::{Context, Function, Local, Object, PinScope};
6+
7+
use super::AbiVersion;
8+
use crate::host::v8::de::property;
9+
use crate::host::v8::error::ExcResult;
10+
use crate::host::v8::error::Throwable;
11+
use crate::host::v8::error::TypeError;
12+
use crate::host::v8::from_value::cast;
13+
use crate::host::v8::string::StringConst;
14+
15+
/// Returns the hook function `name` on `hooks_obj`.
16+
pub(super) fn get_hook_function<'scope>(
17+
scope: &mut PinScope<'scope, '_>,
18+
hooks_obj: Local<'_, Object>,
19+
name: &'static StringConst,
20+
) -> ExcResult<Local<'scope, Function>> {
21+
let key = name.string(scope);
22+
let object = property(scope, hooks_obj, key)?;
23+
cast!(scope, object, Function, "module function hook `{}`", name.as_str()).map_err(|e| e.throw(scope))
24+
}
25+
26+
/// Registers all the module function `hooks`
27+
/// and sets the given `AbiVersion` to `abi`.
28+
pub(super) fn set_hook_slots(
29+
scope: &mut PinScope<'_, '_>,
30+
abi: AbiVersion,
31+
hooks: &[(ModuleHookKey, Local<'_, Function>)],
32+
) -> ExcResult<()> {
33+
// Make sure to call `set_slot` first, as it creates the annex
34+
// and `set_embedder_data` is currently buggy.
35+
let ctx = scope.get_current_context();
36+
let hooks_info = HooksInfo::get_or_create(&ctx, abi)
37+
.map_err(|_| TypeError("cannot call `register_hooks` from different versions").throw(scope))?;
38+
for &(hook, func) in hooks {
39+
hooks_info
40+
.register(hook)
41+
.map_err(|_| TypeError("cannot call `register_hooks` multiple times").throw(scope))?;
42+
ctx.set_embedder_data(hook.to_slot_index(), func.into());
43+
}
44+
Ok(())
45+
}
46+
47+
#[derive(enum_map::Enum, Copy, Clone)]
48+
pub(in super::super) enum ModuleHookKey {
49+
DescribeModule,
50+
CallReducer,
51+
}
52+
53+
impl ModuleHookKey {
54+
/// Returns the index for the slot that holds the module function hook.
55+
/// The index is passed to `v8::Context::{get,set}_embedder_data`.
56+
fn to_slot_index(self) -> i32 {
57+
match self {
58+
// high numbers to avoid overlapping with rusty_v8 - can be
59+
// reverted to just 0, 1... once denoland/rusty_v8#1868 merges
60+
ModuleHookKey::DescribeModule => 20,
61+
ModuleHookKey::CallReducer => 21,
62+
}
63+
}
64+
}
65+
66+
/// Holds the `AbiVersion` used by the module
67+
/// and the module hooks registered by the module
68+
/// for that version.
69+
struct HooksInfo {
70+
abi: AbiVersion,
71+
registered: EnumMap<ModuleHookKey, OnceCell<()>>,
72+
}
73+
74+
impl HooksInfo {
75+
/// Returns, and possibly creates, the [`HooksInfo`] stored in `ctx`.
76+
///
77+
/// Returns an error if `abi` doesn't match the abi version in the
78+
/// already existing `HooksInfo`.
79+
fn get_or_create(ctx: &Context, abi: AbiVersion) -> Result<Rc<Self>, ()> {
80+
match ctx.get_slot::<Self>() {
81+
Some(this) if this.abi == abi => Ok(this),
82+
Some(_) => Err(()),
83+
None => {
84+
let this = Rc::new(Self {
85+
abi,
86+
registered: EnumMap::default(),
87+
});
88+
ctx.set_slot(this.clone());
89+
Ok(this)
90+
}
91+
}
92+
}
93+
94+
/// Mark down the given `hook` as registered, returning an error if it already was.
95+
fn register(&self, hook: ModuleHookKey) -> Result<(), ()> {
96+
self.registered[hook].set(())
97+
}
98+
99+
/// Returns the `AbiVersion` for the given `hook`, if any.
100+
fn get(&self, hook: ModuleHookKey) -> Option<AbiVersion> {
101+
self.registered[hook].get().map(|_| self.abi)
102+
}
103+
}
104+
105+
#[derive(Copy, Clone)]
106+
/// The actual callable module hook function and its abi version.
107+
pub(in super::super) struct HookFunction<'scope>(pub AbiVersion, pub Local<'scope, Function>);
108+
109+
/// Returns the hook function previously registered in [`register_hooks`].
110+
pub(in super::super) fn get_hook<'scope>(
111+
scope: &mut PinScope<'scope, '_>,
112+
hook: ModuleHookKey,
113+
) -> Option<HookFunction<'scope>> {
114+
let ctx = scope.get_current_context();
115+
let hooks = ctx.get_slot::<HooksInfo>()?;
116+
117+
let abi_version = hooks.get(hook)?;
118+
119+
let hooks = ctx
120+
.get_embedder_data(scope, hook.to_slot_index())
121+
.expect("if `AbiVersion` is set the hook must be set");
122+
Some(HookFunction(abi_version, hooks.cast()))
123+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use spacetimedb_lib::{RawModuleDef, VersionTuple};
2+
use v8::{callback_scope, Context, FixedArray, Local, Module, PinScope};
3+
4+
use crate::host::v8::de::scratch_buf;
5+
use crate::host::v8::error::{ErrorOrException, ExcResult, ExceptionThrown, Throwable, TypeError};
6+
use crate::host::wasm_common::abi::parse_abi_version;
7+
use crate::host::wasm_common::module_host_actor::{ReducerOp, ReducerResult};
8+
9+
mod hooks;
10+
mod v1;
11+
12+
pub(super) use self::hooks::{get_hook, HookFunction, ModuleHookKey};
13+
14+
/// The return type of a module -> host syscall.
15+
pub(super) type FnRet<'scope> = ExcResult<Local<'scope, v8::Value>>;
16+
17+
/// The version of the ABI that is exposed to V8.
18+
#[derive(Copy, Clone, PartialEq, Eq)]
19+
pub enum AbiVersion {
20+
V1,
21+
}
22+
23+
/// A dependency resolver for the user's module
24+
/// that will resolve `spacetimedb_sys` to a module that exposes the ABI.
25+
pub(super) fn resolve_sys_module<'scope>(
26+
context: Local<'scope, Context>,
27+
spec: Local<'scope, v8::String>,
28+
_attrs: Local<'scope, FixedArray>,
29+
_referrer: Local<'scope, Module>,
30+
) -> Option<Local<'scope, Module>> {
31+
callback_scope!(unsafe scope, context);
32+
resolve_sys_module_inner(scope, spec).ok()
33+
}
34+
35+
fn resolve_sys_module_inner<'scope>(
36+
scope: &mut PinScope<'scope, '_>,
37+
spec: Local<'scope, v8::String>,
38+
) -> ExcResult<Local<'scope, Module>> {
39+
let scratch = &mut scratch_buf::<32>();
40+
let spec = spec.to_rust_cow_lossy(scope, scratch);
41+
42+
let generic_error = || TypeError(format!("Could not find module {spec:?}"));
43+
44+
let (module, ver) = spec
45+
.strip_prefix("spacetime:")
46+
.and_then(|spec| spec.split_once('@'))
47+
.ok_or_else(|| generic_error().throw(scope))?;
48+
49+
let VersionTuple { major, minor } = parse_abi_version(ver)
50+
.ok_or_else(|| TypeError(format!("Invalid version in module spec {spec:?}")).throw(scope))?;
51+
52+
match module {
53+
"sys" => match (major, minor) {
54+
(1, 0) => Ok(v1::sys_v1_0(scope)),
55+
_ => Err(TypeError(format!(
56+
"Could not import {spec:?}, likely because this module was built for a newer version of SpacetimeDB.\n\
57+
It requires sys module v{major}.{minor}, but that version is not supported by the database."
58+
))
59+
.throw(scope)),
60+
},
61+
_ => Err(generic_error().throw(scope)),
62+
}
63+
}
64+
65+
/// Calls the registered `__call_reducer__` function hook.
66+
///
67+
/// This handles any (future) ABI version differences.
68+
pub(super) fn call_call_reducer(
69+
scope: &mut PinScope<'_, '_>,
70+
fun: HookFunction<'_>,
71+
op: ReducerOp<'_>,
72+
) -> ExcResult<ReducerResult> {
73+
let HookFunction(ver, fun) = fun;
74+
match ver {
75+
AbiVersion::V1 => v1::call_call_reducer(scope, fun, op),
76+
}
77+
}
78+
79+
/// Calls the registered `__describe_module__` function hook.
80+
///
81+
/// This handles any (future) ABI version differences.
82+
pub(super) fn call_describe_module<'scope>(
83+
scope: &mut PinScope<'scope, '_>,
84+
fun: HookFunction<'_>,
85+
) -> Result<RawModuleDef, ErrorOrException<ExceptionThrown>> {
86+
let HookFunction(ver, fun) = fun;
87+
match ver {
88+
AbiVersion::V1 => v1::call_describe_module(scope, fun),
89+
}
90+
}

0 commit comments

Comments
 (0)