From 3c6c8a115ddc4da79011f76550d93d29b7763406 Mon Sep 17 00:00:00 2001 From: Noa Date: Tue, 29 Apr 2025 12:08:51 -0500 Subject: [PATCH] wip switch to sats-based deserialization ModuleInstance impl wip --- Cargo.lock | 1 + Cargo.toml | 1 - crates/core/Cargo.toml | 2 + crates/core/src/host/v8/.gitignore | 1 + crates/core/src/host/v8/de.rs | 2 +- crates/core/src/host/v8/error.rs | 2 +- crates/core/src/host/v8/mod.rs | 317 +++++++++++++++++++++++++- crates/core/src/host/v8/test_code.ts | 25 ++ crates/core/src/host/v8/tsconfig.json | 9 + crates/core/src/host/v8/types.d.ts | 57 +++++ crates/core/src/host/v8/util.rs | 148 ++++++++++++ crates/core/src/host/v8/wrapper.ts | 304 ++++++++++++++++++++++++ 12 files changed, 860 insertions(+), 9 deletions(-) create mode 100644 crates/core/src/host/v8/.gitignore create mode 100644 crates/core/src/host/v8/test_code.ts create mode 100644 crates/core/src/host/v8/tsconfig.json create mode 100644 crates/core/src/host/v8/types.d.ts create mode 100644 crates/core/src/host/v8/util.rs create mode 100644 crates/core/src/host/v8/wrapper.ts diff --git a/Cargo.lock b/Cargo.lock index 705c0f9b24f..cc003fa8612 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5631,6 +5631,7 @@ dependencies = [ "log", "memchr", "nix 0.30.1", + "num-traits", "once_cell", "openssl", "parking_lot 0.12.3", diff --git a/Cargo.toml b/Cargo.toml index e6cbb963f01..6542e93b24b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -264,7 +264,6 @@ termcolor = "1.2.0" thin-vec = "0.2.13" thiserror = "1.0.37" tokio = { version = "1.37", features = ["full"] } -tokio_metrics = { version = "0.4.0" } tokio-postgres = { version = "0.7.8", features = ["with-chrono-0_4"] } tokio-stream = "0.1.17" tokio-tungstenite = { version = "0.27.0", features = ["native-tls"] } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index cfa3251bcd6..dd7f55aa29f 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -118,6 +118,8 @@ jwks.workspace = true async_cache = "0.3.1" faststr = "0.2.23" core_affinity = "0.8" +num-traits = "0.2" +# sourcemap = "9" [target.'cfg(not(target_env = "msvc"))'.dependencies] tikv-jemallocator = {workspace = true} diff --git a/crates/core/src/host/v8/.gitignore b/crates/core/src/host/v8/.gitignore new file mode 100644 index 00000000000..a6c7c2852d0 --- /dev/null +++ b/crates/core/src/host/v8/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/crates/core/src/host/v8/de.rs b/crates/core/src/host/v8/de.rs index 3103cf14095..41c320ae520 100644 --- a/crates/core/src/host/v8/de.rs +++ b/crates/core/src/host/v8/de.rs @@ -92,7 +92,7 @@ impl de::Error for Error<'_> { } /// Returns a scratch buffer to fill when deserializing strings. -fn scratch_buf() -> [MaybeUninit; N] { +pub(super) fn scratch_buf() -> [MaybeUninit; N] { [const { MaybeUninit::uninit() }; N] } diff --git a/crates/core/src/host/v8/error.rs b/crates/core/src/host/v8/error.rs index 1021f9c5f11..d8d30fd3beb 100644 --- a/crates/core/src/host/v8/error.rs +++ b/crates/core/src/host/v8/error.rs @@ -22,7 +22,7 @@ impl IntoJsString for String { /// /// Newtyped for additional type safety and to track JS exceptions in the type system. #[derive(Debug)] -pub(super) struct ExceptionValue<'scope>(Local<'scope, Value>); +pub(super) struct ExceptionValue<'scope>(pub Local<'scope, Value>); /// Error types that can convert into JS exception values. pub(super) trait IntoException<'scope> { diff --git a/crates/core/src/host/v8/mod.rs b/crates/core/src/host/v8/mod.rs index 9cff2587320..7e8824fff62 100644 --- a/crates/core/src/host/v8/mod.rs +++ b/crates/core/src/host/v8/mod.rs @@ -2,17 +2,24 @@ use super::module_common::{build_common_module_from_raw, ModuleCommon}; use super::module_host::{CallReducerParams, DynModule, Module, ModuleInfo, ModuleInstance, ModuleRuntime}; -use super::UpdateDatabaseResult; +use super::{ReducerCallResult, Scheduler, UpdateDatabaseResult}; +use crate::database_logger::{BacktraceProvider, LogLevel, Record}; use crate::host::wasm_common::instrumentation::CallTimes; use crate::host::wasm_common::module_host_actor::{ EnergyStats, ExecuteResult, ExecutionTimings, InstanceCommon, ReducerOp, }; use crate::host::ArgsTuple; -use crate::{host::Scheduler, module_host_context::ModuleCreationContext, replica_context::ReplicaContext}; +use crate::module_host_context::ModuleCreationContext; +use crate::replica_context::ReplicaContext; use anyhow::anyhow; use core::time::Duration; use de::deserialize_js; use error::{catch_exception, exception_already_thrown, ExcResult, Throwable}; +use de::{deserialize_js, scratch_buf}; +use error::{ + catch_exception, exception_already_thrown, ErrorOrException, ExcResult, ExceptionThrown, ExceptionValue, Throwable, + TypeError, +}; use from_value::cast; use key_cache::get_or_create_key_cache; use ser::serialize_to_js; @@ -22,6 +29,12 @@ use spacetimedb_datastore::traits::Program; use spacetimedb_lib::{ConnectionId, Identity, RawModuleDef}; use std::sync::{Arc, LazyLock}; use v8::{Context, ContextOptions, ContextScope, Function, HandleScope, Isolate, Local, Value}; +use util::{ascii_str, module, strings}; +use v8::{ + CallbackScope, Context, ContextOptions, ContextScope, ExternalReference, FixedArray, Function, + FunctionCallbackArguments, FunctionCodeHandling, Global, HandleScope, Isolate, IsolateHandle, Local, MapFnTo, + ModuleStatus, ObjectTemplate, OwnedIsolate, ResolveModuleCallback, ScriptOrigin, Value, +}; mod de; mod error; @@ -29,6 +42,7 @@ mod from_value; mod key_cache; mod ser; mod to_value; +mod util; /// The V8 runtime, for modules written in e.g., JS or TypeScript. #[derive(Default)] @@ -82,16 +96,19 @@ impl V8RuntimeInner { return Err::(anyhow!("v8_todo")); } - let desc = todo!(); + let program = std::str::from_utf8(&mcc.program.bytes)?; + let (snapshot, desc) = compile(program, Arc::new(Logger))?; + // Validate and create a common module rom the raw definition. let common = build_common_module_from_raw(mcc, desc)?; - Ok(JsModule { common }) + Ok(JsModule { common, snapshot }) } } #[derive(Clone)] -struct JsModule { +pub struct JsModule { + snapshot: Arc<[u8]>, common: ModuleCommon, } @@ -105,6 +122,293 @@ impl DynModule for JsModule { } } +struct Logger; +impl Logger { + fn write(&self, level: LogLevel, record: &Record<'_>, _bt: &dyn BacktraceProvider) { + eprintln!( + "{level:?} [{}] [{}:{}] {}", + record.ts, + record.filename.unwrap_or(""), + record.line_number.unwrap_or(0), + record.message, + ) + } +} + +#[repr(usize)] +enum GlobalInternalField { + WrapperModule, + Last, +} + +// fn builtins_snapshot() -> anyhow::Result> { +fn builtins_snapshot() -> anyhow::Result<(OwnedIsolate, Global)> { + let isolate = Isolate::snapshot_creator(Some(extern_refs().into()), None); + let mut isolate = scopeguard::guard(isolate, |isolate| { + // rusty_v8 panics if we don't call this when dropping isolate + isolate.create_blob(FunctionCodeHandling::Clear); + }); + let context = { + let isolate = &mut *isolate; + let handle_scope = &mut HandleScope::new(isolate); + + let global_template = ObjectTemplate::new(handle_scope); + global_template.set_internal_field_count(GlobalInternalField::Last as usize); + let context = Context::new( + handle_scope, + ContextOptions { + global_template: Some(global_template), + ..Default::default() + }, + ); + + let scope = &mut ContextScope::new(handle_scope, context); + scope.set_default_context(context); + assert_eq!(scope.add_context(context), 0); + let global = context.global(scope); + // scope.add_context_data(context, global); + // scope.add_context_ + // scope.get_current_context().set_slot(logger); + catch_exception(scope, |scope| { + let name = ascii_str!("spacetime:wrapper").string(scope).into(); + let module = init_module(scope, name, 0, include_str!("./wrapper.ts"), resolve_internal_module)?; + + // this is hacky + global.set_internal_field(GlobalInternalField::WrapperModule as usize, module.into()); + + Ok(()) + })?; + Global::new(scope, context) + }; + + // let snapshot = scopeguard::ScopeGuard::into_inner(isolate) + // .create_blob(v8::FunctionCodeHandling::Clear) + // .unwrap(); + + // Ok((*snapshot).into()) + + Ok((scopeguard::ScopeGuard::into_inner(isolate), context)) +} + +fn compile(program: &str, logger: Arc) -> anyhow::Result<(Arc<[u8]>, RawModuleDef)> { + // let builtins = builtins_snapshot()?; + // let isolate = v8::Isolate::snapshot_creator_from_existing_snapshot(builtins, Some(&EXTERN_REFS), None); + let (isolate, context) = builtins_snapshot()?; + let mut isolate = scopeguard::guard(isolate, |isolate| { + // rusty_v8 panics if we don't call this when dropping isolate + isolate.create_blob(FunctionCodeHandling::Keep); + }); + isolate.set_slot(logger.clone()); + + let module_def = { + let isolate = &mut *isolate; + let handle_scope = &mut HandleScope::new(isolate); + // let context = v8::Context::from_snapshot(handle_scope, 0, Default::default()).unwrap(); + let context = Local::new(handle_scope, context); + let scope = &mut ContextScope::new(handle_scope, context); + + // scope.set_prepare_stack_trace_callback(prepare_stack_trace); + + // scope.set_default_context(context); + + catch_exception(scope, |scope| { + let name = ascii_str!("spacetime:module").string(scope).into(); + init_module(scope, name, 1, program, resolve_wrapper_module)?; + Ok(()) + })?; + + call_describe_module(scope)? + }; + + let snapshot = scopeguard::ScopeGuard::into_inner(isolate) + .create_blob(FunctionCodeHandling::Keep) + .unwrap(); + // d923b61bd4a4a000589af55b9ac5f046e97c4c756c96427fbc24d1253e7c9c77 + // dbg!(spacetimedb_lib::hash_bytes(&snapshot)); + let snapshot = >::from(&*snapshot); + + Ok((snapshot, module_def)) +} + +fn find_source_map(program: &str) -> Option<&str> { + let sm_ref = "//# sourceMappingURL="; + program.match_indices(sm_ref).find_map(|(i, _)| { + let (before, after) = program.split_at(i); + (before.is_empty() || before.ends_with(['\r', '\n'])) + .then(|| &after.lines().next().unwrap_or(after)[sm_ref.len()..]) + }) +} + +fn init_module<'s>( + scope: &mut HandleScope<'s>, + resource_name: Local<'s, Value>, + script_id: i32, + program: &str, + resolve_module: impl MapFnTo>, +) -> Result, ErrorOrException> { + let source = v8::String::new(scope, program).ok_or_else(exception_already_thrown)?; + let source_map_url = find_source_map(program).map(|r| v8::String::new(scope, r).unwrap().into()); + let origin = ScriptOrigin::new( + scope, + resource_name, + 0, + 0, + false, + script_id, + source_map_url, + false, + false, + true, + None, + ); + let source = &mut v8::script_compiler::Source::new(source, Some(&origin)); + let module = v8::script_compiler::compile_module(scope, source).ok_or_else(exception_already_thrown)?; + + module + .instantiate_module(scope, resolve_module) + .ok_or_else(exception_already_thrown)?; + + module.evaluate(scope).ok_or_else(exception_already_thrown)?; + + if module.get_status() == ModuleStatus::Errored { + let exc = ExceptionValue(Local::new(scope, module.get_exception())); + Err(exc.throw(scope))?; + } + + Ok(module) +} + +fn resolve_internal_module<'s>( + context: Local<'s, Context>, + spec: Local<'s, v8::String>, + _attrs: Local<'s, FixedArray>, + _referrer: Local<'s, v8::Module>, +) -> Option> { + let scope = &mut *unsafe { CallbackScope::new(context) }; + if spec == spacetime_sys_10_0::SPEC_STRING.string(scope) { + Some(spacetime_sys_10_0::make(scope)) + } else { + module_exception(scope, spec).throw(scope); + None + } +} + +strings!(SPACETIME_MODULE = "spacetimedb"); + +fn resolve_wrapper_module<'s>( + context: Local<'s, Context>, + spec: Local<'s, v8::String>, + _attrs: Local<'s, FixedArray>, + _referrer: Local<'s, v8::Module>, +) -> Option> { + let scope = &mut *unsafe { CallbackScope::new(context) }; + if spec == SPACETIME_MODULE.string(scope) { + let module = context + .global(scope) + .get_internal_field(scope, GlobalInternalField::WrapperModule as usize) + .unwrap() + .cast::(); + Some(module) + } else { + module_exception(scope, spec).throw(scope); + None + } +} + +fn module_exception(scope: &mut HandleScope<'_>, spec: Local<'_, v8::String>) -> TypeError { + let mut buf = scratch_buf::<32>(); + let spec = spec.to_rust_cow_lossy(scope, &mut buf); + TypeError(format!("Could not find module {spec:?}")) +} + +module!( + spacetime_sys_10_0 = "spacetime:sys/v10.0", + function(console_log), + symbol(console_level_error = "console.Level.Error"), + symbol(console_level_warn = "console.Level.Warn"), + symbol(console_level_info = "console.Level.Info"), + symbol(console_level_debug = "console.Level.Debug"), + symbol(console_level_trace = "console.Level.Trace"), + symbol(console_level_panic = "console.Level.Panic"), +); + +fn extern_refs() -> Vec { + spacetime_sys_10_0::external_refs() + .chain(Some(ExternalReference { + pointer: std::ptr::null_mut(), + })) + .collect() +} + +fn console_log(scope: &mut HandleScope<'_>, args: FunctionCallbackArguments<'_>) -> ExcResult<()> { + let logger = scope.get_slot::>().unwrap().clone(); + let level = args.get(0); + let level = if level == spacetime_sys_10_0::console_level_error(scope) { + LogLevel::Error + } else if level == spacetime_sys_10_0::console_level_warn(scope) { + LogLevel::Warn + } else if level == spacetime_sys_10_0::console_level_info(scope) { + LogLevel::Info + } else if level == spacetime_sys_10_0::console_level_debug(scope) { + LogLevel::Debug + } else if level == spacetime_sys_10_0::console_level_trace(scope) { + LogLevel::Trace + } else if level == spacetime_sys_10_0::console_level_panic(scope) { + LogLevel::Panic + } else { + return Err(TypeError(ascii_str!("Invalid log level")).throw(scope)); + }; + let msg = args.get(1).cast::(); + let mut buf = scratch_buf::<128>(); + let msg = msg.to_rust_cow_lossy(scope, &mut buf); + let frame: Local<'_, v8::StackFrame> = v8::StackTrace::current_stack_trace(scope, 2) + .ok_or_else(exception_already_thrown)? + .get_frame(scope, 1) + .ok_or_else(exception_already_thrown)?; + let mut buf = scratch_buf::<32>(); + let filename = frame + .get_script_name(scope) + .map(|s| s.to_rust_cow_lossy(scope, &mut buf)); + let record = Record { + // TODO: figure out whether to use walltime now or logical reducer now (env.reducer_start) + ts: chrono::Utc::now(), + target: None, + filename: filename.as_deref(), + line_number: Some(frame.get_line_number() as u32), + message: &msg, + }; + logger.write(level, &record, &()); + Ok(()) +} + +#[test] +fn v8_compile_test() { + let program = include_str!("./test_code.ts"); + let (_snapshot, module) = compile(program, Arc::new(Logger)).unwrap(); + dbg!(module); + // dbg!(module_idx, bytes::Bytes::copy_from_slice(&snapshot)); + // panic!(); +} + +fn _request_interrupt(handle: &IsolateHandle, f: F) -> bool +where + F: FnOnce(&mut Isolate), +{ + unsafe extern "C" fn cb(isolate: &mut Isolate, data: *mut std::ffi::c_void) + where + F: FnOnce(&mut Isolate), + { + let f = unsafe { Box::::from_raw(data.cast()) }; + f(isolate) + } + let data = Box::into_raw(Box::new(f)); + let already_destroyed = handle.request_interrupt(cb::, data.cast()); + if already_destroyed { + drop(unsafe { Box::from_raw(data) }); + } + already_destroyed +} + impl Module for JsModule { type Instance = JsInstance; @@ -123,11 +427,12 @@ impl Module for JsModule { } } -struct JsInstance { +pub struct JsInstance { common: InstanceCommon, replica_ctx: Arc, } +#[allow(unused)] impl ModuleInstance for JsInstance { fn trapped(&self) -> bool { self.common.trapped diff --git a/crates/core/src/host/v8/test_code.ts b/crates/core/src/host/v8/test_code.ts new file mode 100644 index 00000000000..54c22574e94 --- /dev/null +++ b/crates/core/src/host/v8/test_code.ts @@ -0,0 +1,25 @@ +import { registerReducer, registerType, type } from 'spacetimedb'; + +const Foo = registerType( + 'Foo', + type.product({ + bar: type.f32, + baz: type.string, + }) +); + +console.log('hello there', new Error().stack); +try { + function x() { + throw new Error('hello'); + } + x(); +} catch (e) { + // Error.captureStackTrace(e, ) + console.log('woww', e); +} +registerReducer('beepboop', [type.array(type.f32), type.bool, Foo], (x, y, z) => { + // z.bar; +}); +// console.log(registerReducer); +// registerReducer(1, [], () => {}); diff --git a/crates/core/src/host/v8/tsconfig.json b/crates/core/src/host/v8/tsconfig.json new file mode 100644 index 00000000000..a6f87031a61 --- /dev/null +++ b/crates/core/src/host/v8/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ESNext", + "paths": { + "spacetimedb": ["./wrapper"] + }, + "inlineSourceMap": true + } +} diff --git a/crates/core/src/host/v8/types.d.ts b/crates/core/src/host/v8/types.d.ts new file mode 100644 index 00000000000..a9cb7080b19 --- /dev/null +++ b/crates/core/src/host/v8/types.d.ts @@ -0,0 +1,57 @@ +declare module 'spacetime:sys/v10.0' { + export const console_level_error: unique symbol; + export const console_level_warn: unique symbol; + export const console_level_info: unique symbol; + export const console_level_debug: unique symbol; + export const console_level_trace: unique symbol; + export const console_level_panic: unique symbol; + type ConsoleLevel = + | typeof console_level_error + | typeof console_level_warn + | typeof console_level_info + | typeof console_level_debug + | typeof console_level_trace + | typeof console_level_panic; + + export function console_log(level: ConsoleLevel, msg: string): void; + + export function register_reducer(name: string, product_type: ProductType, func: Function): void; + + export function register_type(name: string, type: AlgebraicType): number; + + type Variant = Readonly<{ tag: Tag; value: Value }>; + + export type option = Readonly<{ some: T }> | null; + + export type AlgebraicType = + | Variant<'Ref', number> + | Variant<'Product', ProductType> + | ArrayVariant + | PrimitiveType; + export type ProductType = Readonly<{ + elements: readonly ProductTypeElement[]; + }>; + export type ProductTypeElement = Readonly<{ + name?: option; + algebraic_type: AlgebraicType; + }>; + type ArrayVariant = Readonly<{ tag: 'Array'; value: AlgebraicType }>; + export type Unit = Readonly<{}>; + export type PrimitiveType = + | Variant<'String', Unit> + | Variant<'Bool', Unit> + | Variant<'I8', Unit> + | Variant<'U8', Unit> + | Variant<'I16', Unit> + | Variant<'U16', Unit> + | Variant<'I32', Unit> + | Variant<'U32', Unit> + | Variant<'I64', Unit> + | Variant<'U64', Unit> + | Variant<'I128', Unit> + | Variant<'U128', Unit> + | Variant<'I256', Unit> + | Variant<'U256', Unit> + | Variant<'F32', Unit> + | Variant<'F64', Unit>; +} diff --git a/crates/core/src/host/v8/util.rs b/crates/core/src/host/v8/util.rs new file mode 100644 index 00000000000..ccf1dc2442c --- /dev/null +++ b/crates/core/src/host/v8/util.rs @@ -0,0 +1,148 @@ +use super::error::{ExcResult, IntoJsString}; +use v8::{HandleScope, Local}; + +pub(super) struct StringConst(v8::OneByteConst); + +impl StringConst { + pub(super) const fn new(s: &'static str) -> Self { + Self(v8::String::create_external_onebyte_const(s.as_bytes())) + } + pub(super) fn string<'s>(&'static self, scope: &mut HandleScope<'s, ()>) -> Local<'s, v8::String> { + // unwrap() b/c create_external_onebyte_const asserts new_from_onebyte_const's + // preconditions (str < kMaxLength) + v8::String::new_from_onebyte_const(scope, &self.0).unwrap() + } +} + +impl IntoJsString for Local<'_, v8::String> { + fn into_string<'s>(self, scope: &mut HandleScope<'s>) -> Local<'s, v8::String> { + Local::new(scope, self) + } +} +impl IntoJsString for &'static StringConst { + fn into_string<'s>(self, scope: &mut HandleScope<'s>) -> Local<'s, v8::String> { + self.string(scope) + } +} + +pub(super) fn nicer_callback(f: F) -> v8::FunctionCallback +where + F: Fn(&mut HandleScope<'_>, v8::FunctionCallbackArguments<'_>) -> ExcResult + Copy, + R: ReturnValue, +{ + let cb = move |scope: &mut HandleScope<'_>, args: v8::FunctionCallbackArguments<'_>, rv: v8::ReturnValue<'_>| { + if let Ok(value) = f(scope, args) { + value.set_return_value(rv) + } + }; + v8::MapFnTo::map_fn_to(cb) +} + +pub(super) trait ReturnValue { + fn set_return_value(self, rv: v8::ReturnValue<'_>); +} + +macro_rules! impl_return_value { + ($t:ty, $self:ident, $func:ident($($args:tt)*)) => { + impl ReturnValue for $t { + fn set_return_value($self, mut rv: v8::ReturnValue<'_>) { + rv.$func($($args)*); + } + } + }; + ($t:ty, $func:ident) => { + impl_return_value!($t, self, $func(self)); + }; +} + +impl_return_value!(v8::Local<'_, v8::Value>, set); +impl_return_value!(bool, set_bool); +impl_return_value!(i32, set_int32); +impl_return_value!(u32, set_uint32); +impl_return_value!(f64, set_double); +impl_return_value!((), self, set_undefined()); + +pub(super) fn external_synthetic_steps(f: F) -> v8::ExternalReference +where + for<'a> F: v8::MapFnTo>, +{ + let pointer = f.map_fn_to() as _; + v8::ExternalReference { pointer } +} + +macro_rules! ascii_str { + ($str:expr) => { + const { &$crate::host::v8::util::StringConst::new($str) } + }; +} +pub(super) use ascii_str; + +macro_rules! strings { + ($vis:vis $($name:ident = $val:expr),*$(,)?) => { + $($vis static $name: $crate::host::v8::util::StringConst = $crate::host::v8::util::StringConst::new($val);)* + }; +} +pub(super) use strings; + +macro_rules! module { + ($name:ident = $module_name:expr, $($export_kind:ident($export_name:ident $($export:tt)*)),*$(,)?) => { + mod $name { + pub const SPEC: &str = $module_name; + $crate::host::v8::util::strings!(pub SPEC_STRING = SPEC); + + #[allow(non_snake_case, non_upper_case_globals)] + mod names { + $crate::host::v8::util::strings!(pub(super) $($export_name = stringify!($export_name),)*); + } + + pub fn make<'s>(scope: &mut v8::HandleScope<'s>) -> v8::Local<'s, v8::Module> { + let export_names = [$(names::$export_name.string(scope),)*]; + let spec = SPEC_STRING.string(scope); + v8::Module::create_synthetic_module(scope, spec, &export_names, evaluation_steps) + } + + fn evaluation_steps<'s>(context: v8::Local<'s, v8::Context>, module: v8::Local<'s, v8::Module>) -> Option> { + let scope = &mut *unsafe { v8::CallbackScope::new(context) }; + $({ + let name = names::$export_name.string(scope); + let val = + $crate::host::v8::util::module!(@export scope, name, $export_kind($export_name $($export)*)); + module.set_synthetic_module_export(scope, name, val)?; + })* + Some(v8::undefined(scope).into()) + } + + pub fn external_refs<'s>() -> impl Iterator { + [ + $crate::host::v8::util::external_synthetic_steps(evaluation_steps), + $($crate::host::v8::util::module!(@export_ref $export_kind($export_name $($export)*)),)* + ].into_iter() + } + + $($crate::host::v8::util::module!(@export_rust $export_kind($export_name $($export)*));)* + } + }; + (@export $scope:ident, $name:ident, function($export_name:ident)) => {{ + let func = v8::Function::new_raw($scope, $crate::host::v8::util::nicer_callback(super::$export_name)).unwrap(); + func.set_name($name); + func.into() + }}; + (@export_ref function($export_name:ident)) => { + v8::ExternalReference { function: $crate::host::v8::util::nicer_callback(super::$export_name) } + }; + (@export_rust function($($t:tt)*)) => {}; + (@export $scope:ident, $name:ident, symbol($export_name:ident = $symbol:expr)) => {{ + $export_name($scope).into() + }}; + (@export_ref symbol($($t:tt)*)) => { + #[cfg(any())] () + }; + (@export_rust symbol($export_name:ident = $symbol:expr)) => { + pub fn $export_name<'s>(scope: &mut v8::HandleScope<'s, ()>) -> v8::Local<'s, v8::Symbol> { + $crate::host::v8::util::strings!(STRING = $symbol); + let string = STRING.string(scope); + v8::Symbol::for_api(scope, string) + } + }; +} +pub(super) use module; diff --git a/crates/core/src/host/v8/wrapper.ts b/crates/core/src/host/v8/wrapper.ts new file mode 100644 index 00000000000..b7ab8b2a2ec --- /dev/null +++ b/crates/core/src/host/v8/wrapper.ts @@ -0,0 +1,304 @@ +import { + console_log, + console_level_error, + console_level_warn, + console_level_info, + console_level_debug, + console_level_trace, + console_level_panic, + register_reducer, + register_type, +} from 'spacetime:sys/v10.0'; + +function fmtLog(...data: unknown[]) { + return data.join(' '); +} + +const console = { + __proto__: {}, + + [Symbol.toStringTag]: 'console', + + assert: (condition = false, ...data: any) => { + if (!condition) { + console_log(console_level_error, fmtLog(...data)); + } + }, + clear: () => {}, + debug: (...data: any) => { + console_log(console_level_debug, fmtLog(...data)); + }, + error: (...data: any) => { + console_log(console_level_error, fmtLog(...data)); + }, + info: (...data: any) => { + console_log(console_level_info, fmtLog(...data)); + }, + log: (...data: any) => { + console_log(console_level_info, fmtLog(...data)); + }, + table: (tabularData: unknown, properties: any) => { + console_log(console_level_info, fmtLog(tabularData)); + }, + trace: (...data: any) => { + console_log(console_level_trace, fmtLog(...data)); + }, + warn: (...data: any) => { + console_log(console_level_warn, fmtLog(...data)); + }, + dir: (item: any, options: any) => {}, + dirxml: (...data: any) => {}, + + // Counting + count: (label = 'default') => {}, + countReset: (label = 'default') => {}, + + // Grouping + group: (...data: any) => {}, + groupCollapsed: (...data: any) => {}, + groupEnd: () => {}, + + // Timing + time: (label = 'default') => {}, + timeLog: (label = 'default', ...data: any) => {}, + timeEnd: (label = 'default') => {}, +}; +// @ts-ignore +globalThis.console = console; + +const { freeze } = Object; + +const stringType = Symbol('spacetimedb.type.string'); +const boolType = Symbol('spacetimedb.type.bool'); +const i8Type = Symbol('spacetimedb.type.i8'); +const u8Type = Symbol('spacetimedb.type.u8'); +const i16Type = Symbol('spacetimedb.type.i16'); +const u16Type = Symbol('spacetimedb.type.u16'); +const i32Type = Symbol('spacetimedb.type.i32'); +const u32Type = Symbol('spacetimedb.type.u32'); +const i64Type = Symbol('spacetimedb.type.i64'); +const u64Type = Symbol('spacetimedb.type.u64'); +const i128Type = Symbol('spacetimedb.type.i128'); +const u128Type = Symbol('spacetimedb.type.u128'); +const i256Type = Symbol('spacetimedb.type.i256'); +const u256Type = Symbol('spacetimedb.type.u256'); +const f32Type = Symbol('spacetimedb.type.f32'); +const f64Type = Symbol('spacetimedb.type.f64'); + +export const type = freeze({ + string: stringType, + bool: boolType, + i8: i8Type, + u8: u8Type, + i16: i16Type, + u16: u16Type, + i32: i32Type, + u32: u32Type, + i64: i64Type, + u64: u64Type, + i128: i128Type, + u128: u128Type, + i256: i256Type, + u256: u256Type, + f32: f32Type, + f64: f64Type, + array(elem: Elem) { + return new ArrayType(elem); + }, + product(map: Map) { + return new ProductType(map); + }, +}); + +const toInternalType = Symbol('spacetimedb.toInternalType'); + +class ArrayType { + #inner: Extract; + constructor(inner: Elem) { + this.#inner = freeze({ tag: 'Array', value: convertType(inner) }); + } + get [toInternalType]() { + return this.#inner; + } +} + +type ProductMap = { [s: string]: AlgebraicType }; +class ProductType { + #inner: Extract; + constructor(map: Map) { + const elements = freeze( + Object.entries(map).map(([k, v]) => + freeze({ name: freeze({ some: k }), algebraic_type: convertType(v) }) + ) + ); + this.#inner = freeze({ tag: 'Product', value: freeze({ elements }) }); + } + get [toInternalType]() { + return this.#inner; + } +} + +class TypeRef { + #inner: Extract; + constructor(ref: number) { + this.#inner = freeze({ tag: 'Ref', value: ref }); + } + get [toInternalType]() { + return this.#inner; + } +} + +export const unit = freeze({}); + +const primitives = freeze({ + string: freeze({ tag: 'String', value: unit }), + bool: freeze({ tag: 'Bool', value: unit }), + i8: freeze({ tag: 'I8', value: unit }), + u8: freeze({ tag: 'U8', value: unit }), + i16: freeze({ tag: 'I16', value: unit }), + u16: freeze({ tag: 'U16', value: unit }), + i32: freeze({ tag: 'I32', value: unit }), + u32: freeze({ tag: 'U32', value: unit }), + i64: freeze({ tag: 'I64', value: unit }), + u64: freeze({ tag: 'U64', value: unit }), + i128: freeze({ tag: 'I128', value: unit }), + u128: freeze({ tag: 'U128', value: unit }), + i256: freeze({ tag: 'I256', value: unit }), + u256: freeze({ tag: 'U256', value: unit }), + f32: freeze({ tag: 'F32', value: unit }), + f64: freeze({ tag: 'F64', value: unit }), +}); + +function convertType(ty: AlgebraicType): import('spacetime:sys/v10.0').AlgebraicType { + if (typeof ty === 'symbol') { + switch (ty) { + case type.string: + return primitives.string; + case type.bool: + return primitives.bool; + case type.i8: + return primitives.i8; + case type.u8: + return primitives.u8; + case type.i16: + return primitives.i16; + case type.u16: + return primitives.u16; + case type.i32: + return primitives.i32; + case type.u32: + return primitives.u32; + case type.i64: + return primitives.i64; + case type.u64: + return primitives.u64; + case type.i128: + return primitives.i128; + case type.u128: + return primitives.u128; + case type.i256: + return primitives.i256; + case type.u256: + return primitives.u256; + case type.f32: + return primitives.f32; + case type.f64: + return primitives.f64; + default: + let {}: never = ty; + } + } else if (toInternalType in ty) { + return ty[toInternalType]; + } + throw new TypeError('Expected Spacetime type, got ' + ty); +} + +type PrimitiveType = Extract<(typeof type)[keyof typeof type], symbol>; + +type AlgebraicType = TypeRef | ProductType | ArrayType | PrimitiveType; + +export type I8 = number; +export type U8 = number; +export type I16 = number; +export type U16 = number; +export type I32 = number; +export type U32 = number; +export type I64 = bigint; +export type U64 = bigint; +export type I128 = bigint; +export type U128 = bigint; +export type I256 = bigint; +export type U256 = bigint; + +type PrimitiveTypeToType = T extends typeof stringType + ? string + : T extends typeof boolType + ? boolean + : T extends typeof i8Type + ? I8 + : T extends typeof u8Type + ? U8 + : T extends typeof i16Type + ? I16 + : T extends typeof u16Type + ? U16 + : T extends typeof i32Type + ? I32 + : T extends typeof u32Type + ? U32 + : T extends typeof i64Type + ? I64 + : T extends typeof u64Type + ? U64 + : T extends typeof i128Type + ? I128 + : T extends typeof u128Type + ? U128 + : T extends typeof i256Type + ? I256 + : T extends typeof u256Type + ? U256 + : T extends typeof f32Type + ? number + : T extends typeof f64Type + ? number + : never; + +type AlgebraicTypeToType = [T] extends [TypeRef] + ? AlgebraicTypeToType + : [T] extends [ProductType] + ? { [k in keyof U]: AlgebraicTypeToType } + : [T] extends [ArrayType] + ? AlgebraicTypeToType[] + : [T] extends [PrimitiveType] + ? PrimitiveTypeToType + : never; + +type ArgsToType = { + [i in keyof Args]: AlgebraicTypeToType; +}; + +export function registerReducer( + name: string, + params: Args, + func: (...args: ArgsToType) => void +) { + if (typeof name !== 'string') { + throw new TypeError('First argument to registerReducer must be string'); + } + if (!Array.isArray(params)) { + throw new TypeError('Second argument to registerReducer must be array'); + } + const elements = freeze( + params.map(ty => freeze({ name: null, algebraic_type: convertType(ty) })) + ); + register_reducer(name, freeze({ elements }), func); +} + +export function registerType(name: string, type: Type): TypeRef { + if (typeof name !== 'string') { + throw new TypeError('First argument to registerType must be string'); + } + const ref = register_type(name, convertType(type)); + return new TypeRef(ref); +}