Skip to content

Commit 94b6c5e

Browse files
committed
Add proper syscall versioning
1 parent 8118dd8 commit 94b6c5e

File tree

6 files changed

+268
-134
lines changed

6 files changed

+268
-134
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: 19 additions & 7 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, ModuleHook,
10+
};
911
use super::module_common::{build_common_module_from_raw, run_describer, ModuleCommon};
1012
use super::module_host::{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;
@@ -322,12 +325,13 @@ fn startup_instance_worker<'scope>(
322325
scope: &mut PinScope<'scope, '_>,
323326
program: Arc<str>,
324327
module_or_mcc: Either<ModuleCommon, ModuleCreationContextLimited>,
325-
) -> anyhow::Result<(Local<'scope, Function>, Either<ModuleCommon, ModuleCommon>)> {
328+
) -> anyhow::Result<(HookFunction<'scope>, Either<ModuleCommon, ModuleCommon>)> {
326329
// Start-up the user's module.
327330
eval_user_module_catch(scope, &program).map_err(DescribeError::Setup)?;
328331

329332
// Find the `__call_reducer__` function.
330-
let call_reducer_fun = catch_exception(scope, |scope| Ok(call_reducer_fun(scope)?)).map_err(|(e, _)| e)?;
333+
let call_reducer_fun =
334+
get_hook(scope, ModuleHook::CallReducer).context("The `spacetimedb/server` module was never imported")?;
331335

332336
// If we don't have a module, make one.
333337
let module_common = match module_or_mcc {
@@ -571,7 +575,7 @@ fn call_reducer<'scope>(
571575
instance_common: &mut InstanceCommon,
572576
replica_ctx: &ReplicaContext,
573577
scope: &mut PinScope<'scope, '_>,
574-
fun: Local<'scope, Function>,
578+
fun: HookFunction<'_>,
575579
tx: Option<MutTxId>,
576580
params: CallReducerParams,
577581
) -> (super::ReducerCallResult, bool) {
@@ -648,14 +652,18 @@ fn extract_description<'scope>(
648652
|a, b, c| log_traceback(replica_ctx, a, b, c),
649653
|| {
650654
catch_exception(scope, |scope| {
651-
let def = call_describe_module(scope)?;
655+
let Some(describe_module) = get_hook(scope, ModuleHook::DescribeModule) else {
656+
return Ok(RawModuleDef::V9(Default::default()));
657+
};
658+
let def = call_describe_module(scope, describe_module)?;
652659
Ok(def)
653660
})
654661
.map_err(|(e, _)| e)
655662
.map_err(Into::into)
656663
},
657664
)
658665
}
666+
659667
#[cfg(test)]
660668
mod test {
661669
use super::to_value::test::with_scope;
@@ -684,7 +692,7 @@ mod test {
684692
fn call_call_reducer_works() {
685693
let call = |code| {
686694
with_module_catch(code, |scope| {
687-
let fun = call_reducer_fun(scope)?;
695+
let fun = get_hook(scope, ModuleHook::CallReducer).unwrap();
688696
let op = ReducerOp {
689697
id: ReducerId(42),
690698
name: "foobar",
@@ -762,7 +770,11 @@ js error Uncaught Error: foobar
762770
},
763771
})
764772
"#;
765-
let raw_mod = with_module_catch(code, call_describe_module).map_err(|e| e.to_string());
773+
let raw_mod = with_module_catch(code, |scope| {
774+
let describe_module = get_hook(scope, ModuleHook::DescribeModule).unwrap();
775+
call_describe_module(scope, describe_module)
776+
})
777+
.map_err(|e| e.to_string());
766778
assert_eq!(raw_mod, Ok(RawModuleDef::V9(<_>::default())));
767779
}
768780
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ 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+
pub(super) fn as_str(&'static self) -> &'static str {
41+
self.0.as_str()
42+
}
3943
}
4044

4145
/// Converts an identifier to a compile-time ASCII string.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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+
pub(super) fn get_hook_function<'s>(
16+
scope: &mut PinScope<'s, '_>,
17+
hooks_obj: Local<'_, Object>,
18+
name: &'static StringConst,
19+
) -> ExcResult<Local<'s, Function>> {
20+
let key = name.string(scope);
21+
let object = property(scope, hooks_obj, key)?;
22+
cast!(scope, object, Function, "module function hook `{}`", name.as_str()).map_err(|e| e.throw(scope))
23+
}
24+
25+
pub(super) fn set_hook_slots(
26+
scope: &mut PinScope<'_, '_>,
27+
abi: AbiVersion,
28+
hooks: &[(ModuleHook, Local<'_, Function>)],
29+
) -> ExcResult<()> {
30+
// Make sure to call `set_slot` first, as it creates the annex
31+
// and `set_embedder_data` is currently buggy.
32+
let ctx = scope.get_current_context();
33+
let hooks_info = HooksInfo::get_or_create(&ctx);
34+
for &(hook, func) in hooks {
35+
hooks_info
36+
.register(hook, abi)
37+
.map_err(|_| TypeError("cannot call register_hooks multiple times").throw(scope))?;
38+
ctx.set_embedder_data(hook.to_slot_index(), func.into());
39+
}
40+
Ok(())
41+
}
42+
43+
#[derive(enum_map::Enum, Copy, Clone)]
44+
pub(in crate::host::v8) enum ModuleHook {
45+
DescribeModule,
46+
CallReducer,
47+
}
48+
49+
impl ModuleHook {
50+
/// Get the `v8::Context::{get,set}_embedder_data` slot that holds this hook.
51+
fn to_slot_index(self) -> i32 {
52+
match self {
53+
ModuleHook::DescribeModule => 20,
54+
ModuleHook::CallReducer => 21,
55+
}
56+
}
57+
}
58+
59+
#[derive(Default)]
60+
struct HooksInfo {
61+
abi: OnceCell<AbiVersion>,
62+
registered: EnumMap<ModuleHook, OnceCell<()>>,
63+
}
64+
65+
impl HooksInfo {
66+
fn get_or_create(ctx: &Context) -> Rc<Self> {
67+
ctx.get_slot().unwrap_or_else(|| {
68+
let this = Rc::<Self>::default();
69+
ctx.set_slot(this.clone());
70+
this
71+
})
72+
}
73+
74+
fn register(&self, hook: ModuleHook, abi: AbiVersion) -> Result<(), ()> {
75+
if *self.abi.get_or_init(|| abi) != abi {
76+
return Err(());
77+
}
78+
self.registered[hook].set(())
79+
}
80+
81+
fn get(&self, hook: ModuleHook) -> Option<AbiVersion> {
82+
self.registered[hook].get().and(self.abi.get().copied())
83+
}
84+
}
85+
86+
#[derive(Copy, Clone)]
87+
pub(in crate::host::v8) struct HookFunction<'s>(pub AbiVersion, pub Local<'s, Function>);
88+
89+
/// Returns the hook function previously registered in [`register_hooks`].
90+
pub(in crate::host::v8) fn get_hook<'scope>(
91+
scope: &mut PinScope<'scope, '_>,
92+
hook: ModuleHook,
93+
) -> Option<HookFunction<'scope>> {
94+
let ctx = scope.get_current_context();
95+
let hooks = ctx.get_slot::<HooksInfo>()?;
96+
97+
let abi_version = hooks.get(hook)?;
98+
99+
let hooks = ctx
100+
.get_embedder_data(scope, hook.to_slot_index())
101+
.expect("if `AbiVersion` is set the hook must be set");
102+
Some(HookFunction(abi_version, hooks.cast()))
103+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
use spacetimedb_lib::RawModuleDef;
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::ExcResult;
6+
use crate::host::v8::error::Throwable;
7+
use crate::host::v8::error::TypeError;
8+
use crate::host::wasm_common::module_host_actor::{ReducerOp, ReducerResult};
9+
10+
mod hooks;
11+
mod v1;
12+
13+
pub(super) use self::hooks::{get_hook, HookFunction, ModuleHook};
14+
15+
/// The return type of a module -> host syscall.
16+
pub(super) type FnRet<'scope> = ExcResult<Local<'scope, v8::Value>>;
17+
18+
/// The version of the ABI that is exposed to V8.
19+
#[derive(Copy, Clone, PartialEq, Eq)]
20+
pub enum AbiVersion {
21+
V1,
22+
}
23+
24+
/// A dependency resolver for the user's module
25+
/// that will resolve `spacetimedb_sys` to a module that exposes the ABI.
26+
pub(super) fn resolve_sys_module<'s>(
27+
context: Local<'s, Context>,
28+
spec: Local<'s, v8::String>,
29+
_attrs: Local<'s, FixedArray>,
30+
_referrer: Local<'s, Module>,
31+
) -> Option<Local<'s, Module>> {
32+
callback_scope!(unsafe scope, context);
33+
resolve_sys_module_inner(scope, spec).ok()
34+
}
35+
36+
fn resolve_sys_module_inner<'s>(
37+
scope: &mut PinScope<'s, '_>,
38+
spec: Local<'s, v8::String>,
39+
) -> ExcResult<Local<'s, Module>> {
40+
let scratch = &mut scratch_buf::<32>();
41+
let spec = spec.to_rust_cow_lossy(scope, scratch);
42+
43+
let generic_error = || TypeError(format!("Could not find module {spec:?}"));
44+
45+
let (module, ver) = spec
46+
.strip_prefix("spacetime:")
47+
.and_then(|spec| spec.split_once('@'))
48+
.ok_or_else(|| generic_error().throw(scope))?;
49+
50+
let (maj, min) = ver
51+
.split_once('.')
52+
.and_then(|(maj, min)| Option::zip(maj.parse::<u32>().ok(), min.parse::<u32>().ok()))
53+
.ok_or_else(|| TypeError(format!("Invalid version in module spec {spec:?}")).throw(scope))?;
54+
55+
match module {
56+
"sys" => match (maj, min) {
57+
(1, 0) => Ok(v1::sys_v1_0(scope)),
58+
_ => Err(TypeError(format!(
59+
"Could not import {spec:?}, likely because this module was built for a newer version of SpacetimeDB.\n\
60+
It requires sys module v{maj}.{min}, but that version is not supported by the database."
61+
))
62+
.throw(scope)),
63+
},
64+
_ => Err(generic_error().throw(scope)),
65+
}
66+
}
67+
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+
pub(super) fn call_describe_module<'scope>(
81+
scope: &mut PinScope<'scope, '_>,
82+
fun: HookFunction<'_>,
83+
) -> ExcResult<RawModuleDef> {
84+
let HookFunction(ver, fun) = fun;
85+
match ver {
86+
AbiVersion::V1 => v1::call_describe_module(scope, fun),
87+
}
88+
}

0 commit comments

Comments
 (0)