From b1030f6a1c23c23303ecc0b6d6a456b30a21bfb8 Mon Sep 17 00:00:00 2001 From: Mazdak Farrokhzad Date: Fri, 3 Oct 2025 12:04:48 +0200 Subject: [PATCH 1/4] v8 -> 140.2, s/HandleScope/PinScope/g --- Cargo.lock | 363 ++++++++++++++++++++++++-- Cargo.toml | 2 +- crates/core/src/host/v8/de.rs | 66 ++--- crates/core/src/host/v8/error.rs | 38 +-- crates/core/src/host/v8/from_value.rs | 17 +- crates/core/src/host/v8/key_cache.rs | 14 +- crates/core/src/host/v8/mod.rs | 35 +-- crates/core/src/host/v8/ser.rs | 40 +-- crates/core/src/host/v8/syscall.rs | 58 ++-- crates/core/src/host/v8/to_value.rs | 16 +- 10 files changed, 478 insertions(+), 171 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a5ac90b2aa4..62f9b944849 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -484,9 +484,9 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.71.1" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ "bitflags 2.9.0", "cexpr", @@ -730,6 +730,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "calendrical_calculations" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c5d386a9f2c8b97e1a036420bcf937db4e5c9df33eb0232025008ced6104c0" +dependencies = [ + "core_maths", + "displaydoc", +] + [[package]] name = "camino" version = "1.1.9" @@ -1099,6 +1109,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + [[package]] name = "cpp_demangle" version = "0.4.4" @@ -1610,6 +1629,38 @@ dependencies = [ "subtle", ] +[[package]] +name = "diplomat" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced081520ee8cf2b8c5b64a1a901eccd7030ece670dac274afe64607d6499b71" +dependencies = [ + "diplomat_core", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "diplomat-runtime" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "098f9520ec5c190943b083bca3ea4cc4e67dc5f85a37062e528ecf1d25f04eb4" + +[[package]] +name = "diplomat_core" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad619d9fdee0e731bb6f8f7d797b6ecfdc2395e363f554d2f6377155955171eb" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "smallvec", + "strck", + "syn 2.0.101", +] + [[package]] name = "directories-next" version = "2.0.0" @@ -2668,6 +2719,29 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_calendar" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f6c40ba6481ed7ddd358437af0f000eb9f661345845977d9d1b38e606374e1f" +dependencies = [ + "calendrical_calculations", + "displaydoc", + "icu_calendar_data", + "icu_locale", + "icu_locale_core", + "icu_provider 2.0.0", + "tinystr 0.8.1", + "writeable 0.6.1", + "zerovec 0.11.4", +] + +[[package]] +name = "icu_calendar_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219c8639ab936713a87b571eed2bc2615aa9137e8af6eb221446ee5644acc18" + [[package]] name = "icu_collections" version = "1.5.0" @@ -2675,11 +2749,59 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ "displaydoc", - "yoke", + "yoke 0.7.5", + "zerofrom", + "zerovec 0.10.4", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke 0.8.0", "zerofrom", - "zerovec", + "zerovec 0.11.4", +] + +[[package]] +name = "icu_locale" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ae5921528335e91da1b6c695dbf1ec37df5ac13faa3f91e5640be93aa2fbefd" +dependencies = [ + "displaydoc", + "icu_collections 2.0.0", + "icu_locale_core", + "icu_locale_data", + "icu_provider 2.0.0", + "potential_utf", + "tinystr 0.8.1", + "zerovec 0.11.4", ] +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap 0.8.0", + "tinystr 0.8.1", + "writeable 0.6.1", + "zerovec 0.11.4", +] + +[[package]] +name = "icu_locale_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fdef0c124749d06a743c69e938350816554eb63ac979166590e2b4ee4252765" + [[package]] name = "icu_locid" version = "1.5.0" @@ -2687,10 +2809,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", + "litemap 0.7.5", + "tinystr 0.7.6", + "writeable 0.5.5", + "zerovec 0.10.4", ] [[package]] @@ -2702,9 +2824,9 @@ dependencies = [ "displaydoc", "icu_locid", "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", + "icu_provider 1.5.0", + "tinystr 0.7.6", + "zerovec 0.10.4", ] [[package]] @@ -2720,15 +2842,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" dependencies = [ "displaydoc", - "icu_collections", + "icu_collections 1.5.0", "icu_normalizer_data", "icu_properties", - "icu_provider", + "icu_provider 1.5.0", "smallvec", "utf16_iter", "utf8_iter", "write16", - "zerovec", + "zerovec 0.10.4", ] [[package]] @@ -2744,12 +2866,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" dependencies = [ "displaydoc", - "icu_collections", + "icu_collections 1.5.0", "icu_locid_transform", "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", + "icu_provider 1.5.0", + "tinystr 0.7.6", + "zerovec 0.10.4", ] [[package]] @@ -2768,11 +2890,28 @@ dependencies = [ "icu_locid", "icu_provider_macros", "stable_deref_trait", - "tinystr", - "writeable", - "yoke", + "tinystr 0.7.6", + "writeable 0.5.5", + "yoke 0.7.5", "zerofrom", - "zerovec", + "zerovec 0.10.4", +] + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr 0.8.1", + "writeable 0.6.1", + "yoke 0.8.0", + "zerofrom", + "zerotrie", + "zerovec 0.11.4", ] [[package]] @@ -3004,6 +3143,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "ixdtf" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ef2d119044a672ceb96e59608dffe8677f29dc6ec48ed693a4b9ac84751e9b" +dependencies = [ + "displaydoc", +] + [[package]] name = "jemalloc_pprof" version = "0.8.1" @@ -3021,6 +3169,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "jiff-tzdb" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" + [[package]] name = "jni" version = "0.21.1" @@ -3220,6 +3374,12 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + [[package]] name = "lock_api" version = "0.4.12" @@ -4103,6 +4263,16 @@ dependencies = [ "postgres-protocol", ] +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "serde", + "zerovec 0.11.4", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -5022,6 +5192,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "ry_temporal_capi" +version = "0.0.11-ry.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdfddefd45ee4814bd83d94b7196c95ef6af159f4a0035a6c67bd59edcff14ee" +dependencies = [ + "diplomat", + "diplomat-runtime", + "icu_calendar", + "icu_locale", + "num-traits", + "temporal_rs", + "writeable 0.6.1", +] + [[package]] name = "ryu" version = "1.0.20" @@ -6635,6 +6820,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" +[[package]] +name = "strck" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42316e70da376f3d113a68d138a60d8a9883c604fe97942721ec2068dab13a9f" +dependencies = [ + "unicode-ident", +] + [[package]] name = "stringprep" version = "0.1.5" @@ -6905,6 +7099,25 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "temporal_rs" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7807e330b12e288b847a3e2a2b0dcd41ca764d0f90f9e8940f02c6ddd68cd2d7" +dependencies = [ + "combine", + "core_maths", + "icu_calendar", + "icu_locale", + "ixdtf", + "jiff-tzdb", + "num-traits", + "timezone_provider", + "tinystr 0.8.1", + "tzif", + "writeable 0.6.1", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -7079,6 +7292,17 @@ dependencies = [ "time-core", ] +[[package]] +name = "timezone_provider" +version = "0.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f357f8e2cddee6a7b56b69fbb4cab30a7e82914c80ee7f9a5eb799ee3de3f24d" +dependencies = [ + "tinystr 0.8.1", + "zerotrie", + "zerovec 0.11.4", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -7086,7 +7310,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", - "zerovec", + "zerovec 0.10.4", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec 0.11.4", ] [[package]] @@ -7525,6 +7759,15 @@ dependencies = [ "spacetimedb 1.3.0", ] +[[package]] +name = "tzif" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e762ac355f0c204d09ae644b3d59423d5ddfc5603997d60c8c56f24e429a9d" +dependencies = [ + "combine", +] + [[package]] name = "unarray" version = "0.1.4" @@ -7665,17 +7908,18 @@ dependencies = [ [[package]] name = "v8" -version = "137.2.1" +version = "140.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca393e2032ddba2a57169e15cac5d0a81cdb3d872a8886f4468bc0f486098d2" +checksum = "8827809a2884fb68530d678a8ef15b1ed1344bbf844879194d68c140c6f844f9" dependencies = [ - "bindgen 0.71.1", + "bindgen 0.72.1", "bitflags 2.9.0", "fslock", "gzip-header", "home", "miniz_oxide", "paste", + "ry_temporal_capi", "which 6.0.3", ] @@ -8692,6 +8936,12 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + [[package]] name = "wyz" version = "0.5.1" @@ -8755,7 +9005,19 @@ checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", - "yoke-derive", + "yoke-derive 0.7.5", + "zerofrom", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive 0.8.0", "zerofrom", ] @@ -8771,6 +9033,18 @@ dependencies = [ "synstructure 0.13.2", ] +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "synstructure 0.13.2", +] + [[package]] name = "zerocopy" version = "0.8.25" @@ -8832,15 +9106,35 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", +] + [[package]] name = "zerovec" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ - "yoke", + "yoke 0.7.5", + "zerofrom", + "zerovec-derive 0.10.3", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke 0.8.0", "zerofrom", - "zerovec-derive", + "zerovec-derive 0.11.1", ] [[package]] @@ -8854,6 +9148,17 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "zip" version = "2.6.1" diff --git a/Cargo.toml b/Cargo.toml index f84eff4df7a..446a3e35c9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -292,7 +292,7 @@ unicode-normalization = "0.1.23" url = "2.3.1" urlencoding = "2.1.2" uuid = { version = "1.2.1", features = ["v4"] } -v8 = "137.2" +v8 = "140.2" walkdir = "2.2.5" wasmbin = "0.6" webbrowser = "1.0.2" diff --git a/crates/core/src/host/v8/de.rs b/crates/core/src/host/v8/de.rs index 7828d5bdf65..419e0289ea3 100644 --- a/crates/core/src/host/v8/de.rs +++ b/crates/core/src/host/v8/de.rs @@ -11,11 +11,11 @@ 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 v8::{Array, HandleScope, Local, Name, Object, Uint8Array, Value}; +use v8::{Array, Local, Name, Object, PinScope, 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>>( - scope: &mut HandleScope<'de>, + scope: &mut PinScope<'de, '_>, val: Local<'_, Value>, seed: T, ) -> ExcResult { @@ -27,21 +27,25 @@ pub(super) fn deserialize_js_seed<'de, T: DeserializeSeed<'de>>( /// Deserializes a `T` from `val` in `scope`. pub(super) fn deserialize_js<'de, T: de::Deserialize<'de>>( - scope: &mut HandleScope<'de>, + scope: &mut PinScope<'de, '_>, val: Local<'_, Value>, ) -> ExcResult { deserialize_js_seed(scope, val, PhantomData) } /// Deserializes from V8 values. -struct Deserializer<'this, 'scope> { - common: DeserializerCommon<'this, 'scope>, +struct Deserializer<'this, 'scope, 'isolate> { + common: DeserializerCommon<'this, 'scope, 'isolate>, input: Local<'scope, Value>, } -impl<'this, 'scope> Deserializer<'this, 'scope> { +impl<'this, 'scope, 'isolate> Deserializer<'this, 'scope, 'isolate> { /// Creates a new deserializer from `input` in `scope`. - fn new(scope: &'this mut HandleScope<'scope>, input: Local<'_, Value>, key_cache: &'this mut KeyCache) -> Self { + fn new( + scope: &'this mut PinScope<'scope, 'isolate>, + input: Local<'_, Value>, + key_cache: &'this mut KeyCache, + ) -> Self { let input = Local::new(scope, input); let common = DeserializerCommon { scope, key_cache }; Deserializer { input, common } @@ -51,15 +55,15 @@ impl<'this, 'scope> Deserializer<'this, 'scope> { /// Things shared between various [`Deserializer`]s. /// /// The lifetime `'scope` is that of the scope of values deserialized. -struct DeserializerCommon<'this, 'scope> { +struct DeserializerCommon<'this, 'scope, 'isolate> { /// The scope of values to deserialize. - scope: &'this mut HandleScope<'scope>, + scope: &'this mut PinScope<'scope, 'isolate>, /// A cache for frequently used strings. key_cache: &'this mut KeyCache, } -impl<'scope> DeserializerCommon<'_, 'scope> { - fn reborrow(&mut self) -> DeserializerCommon<'_, 'scope> { +impl<'scope, 'isolate> DeserializerCommon<'_, 'scope, 'isolate> { + fn reborrow(&mut self) -> DeserializerCommon<'_, 'scope, 'isolate> { DeserializerCommon { scope: self.scope, key_cache: self.key_cache, @@ -76,7 +80,7 @@ enum Error<'scope> { } impl<'scope> Throwable<'scope> for Error<'scope> { - fn throw(self, scope: &mut HandleScope<'scope>) -> ExceptionThrown { + fn throw(self, scope: &PinScope<'scope, '_>) -> ExceptionThrown { match self { Self::Unthrown(exception) => exception.throw(scope), Self::Thrown(thrown) => thrown, @@ -118,7 +122,7 @@ macro_rules! deserialize_primitive { }; } -impl<'de, 'this, 'scope: 'de> de::Deserializer<'de> for Deserializer<'this, 'scope> { +impl<'de, 'this, 'scope: 'de> de::Deserializer<'de> for Deserializer<'this, 'scope, '_> { type Error = Error<'scope>; // Deserialization of primitive types defers to `FromValue`. @@ -156,7 +160,7 @@ impl<'de, 'this, 'scope: 'de> de::Deserializer<'de> for Deserializer<'this, 'sco } fn deserialize_sum>(self, visitor: V) -> Result { - let scope = &mut *self.common.scope; + let scope = &*self.common.scope; let sum_name = visitor.sum_name().unwrap_or(""); // We expect a canonical representation of a sum value in JS to be @@ -187,7 +191,7 @@ impl<'de, 'this, 'scope: 'de> de::Deserializer<'de> for Deserializer<'this, 'sco fn deserialize_str>(self, visitor: V) -> Result { let val = cast!(self.common.scope, self.input, v8::String, "`string`")?; let mut buf = scratch_buf::<64>(); - match val.to_rust_cow_lossy(self.common.scope, &mut buf) { + match val.to_rust_cow_lossy(&mut *self.common.scope, &mut buf) { Cow::Borrowed(s) => visitor.visit(s), Cow::Owned(string) => visitor.visit_owned(string), } @@ -212,8 +216,8 @@ impl<'de, 'this, 'scope: 'de> de::Deserializer<'de> for Deserializer<'this, 'sco /// Provides access to the field names and values in a JS object /// under the assumption that it's a product. -struct ProductAccess<'this, 'scope> { - common: DeserializerCommon<'this, 'scope>, +struct ProductAccess<'this, 'scope, 'isolate> { + common: DeserializerCommon<'this, 'scope, 'isolate>, /// The input object being deserialized. object: Local<'scope, Object>, /// A field's value, to deserialize next in [`NamedProductAccess::get_field_value_seed`]. @@ -223,7 +227,7 @@ struct ProductAccess<'this, 'scope> { } // Creates an interned [`v8::String`]. -pub(super) fn v8_interned_string<'scope>(scope: &mut HandleScope<'scope>, field: &str) -> Local<'scope, v8::String> { +pub(super) fn v8_interned_string<'scope>(scope: &PinScope<'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 @@ -232,7 +236,7 @@ pub(super) fn v8_interned_string<'scope>(scope: &mut HandleScope<'scope>, field: /// Normalizes `field` into an interned `v8::String`. pub(super) fn intern_field_name<'scope>( - scope: &mut HandleScope<'scope>, + scope: &PinScope<'scope, '_>, field: Option<&str>, index: usize, ) -> Local<'scope, Name> { @@ -243,11 +247,11 @@ pub(super) fn intern_field_name<'scope>( v8_interned_string(scope, &field).into() } -impl<'de, 'scope: 'de> de::NamedProductAccess<'de> for ProductAccess<'_, 'scope> { +impl<'de, 'scope: 'de> de::NamedProductAccess<'de> for ProductAccess<'_, 'scope, '_> { type Error = Error<'scope>; fn get_field_ident>(&mut self, visitor: V) -> Result, Self::Error> { - let scope = &mut *self.common.scope; + let scope = &*self.common.scope; let mut field_names = visitor.field_names(); while let Some(field) = field_names.nth(self.index) { // Get and advance the current index. @@ -296,17 +300,17 @@ impl<'de, 'scope: 'de> de::NamedProductAccess<'de> for ProductAccess<'_, 'scope> /// Used in `Deserializer::deserialize_sum` to translate a `tag` property of a JS object /// to a variant and to provide a deserializer for its value/payload. -struct SumAccess<'this, 'scope> { - common: DeserializerCommon<'this, 'scope>, +struct SumAccess<'this, 'scope, 'isolate> { + common: DeserializerCommon<'this, 'scope, 'isolate>, /// The tag of the sum value. tag: Local<'scope, v8::String>, /// The value of the sum value. value: Local<'scope, Value>, } -impl<'de, 'this, 'scope: 'de> de::SumAccess<'de> for SumAccess<'this, 'scope> { +impl<'de, 'this, 'scope: 'de, 'isolate> de::SumAccess<'de> for SumAccess<'this, 'scope, 'isolate> { type Error = Error<'scope>; - type Variant = Deserializer<'this, 'scope>; + type Variant = Deserializer<'this, 'scope, 'isolate>; fn variant>(self, visitor: V) -> Result<(V::Output, Self::Variant), Self::Error> { // Read the `tag` property in JS. @@ -328,7 +332,7 @@ impl<'de, 'this, 'scope: 'de> de::SumAccess<'de> for SumAccess<'this, 'scope> { } } -impl<'de, 'this, 'scope: 'de> de::VariantAccess<'de> for Deserializer<'this, 'scope> { +impl<'de, 'this, 'scope: 'de> de::VariantAccess<'de> for Deserializer<'this, 'scope, '_> { type Error = Error<'scope>; fn deserialize_seed>(self, seed: T) -> Result { @@ -338,18 +342,18 @@ impl<'de, 'this, 'scope: 'de> de::VariantAccess<'de> for Deserializer<'this, 'sc /// Used by an `ArrayVisitor` to deserialize every element of a JS array /// to a SATS array. -struct ArrayAccess<'this, 'scope, T> { - common: DeserializerCommon<'this, 'scope>, +struct ArrayAccess<'this, 'scope, 'isolate, T> { + common: DeserializerCommon<'this, 'scope, 'isolate>, arr: Local<'scope, Array>, seeds: RepeatN, index: u32, } -impl<'de, 'this, 'scope, T> ArrayAccess<'this, 'scope, T> +impl<'de, 'this, 'scope, 'isolate, T> ArrayAccess<'this, 'scope, 'isolate, T> where T: DeserializeSeed<'de> + Clone, { - fn new(arr: Local<'scope, Array>, common: DeserializerCommon<'this, 'scope>, seed: T) -> Self { + fn new(arr: Local<'scope, Array>, common: DeserializerCommon<'this, 'scope, 'isolate>, seed: T) -> Self { Self { arr, common, @@ -359,7 +363,7 @@ where } } -impl<'de, 'scope: 'de, T: DeserializeSeed<'de> + Clone> de::ArrayAccess<'de> for ArrayAccess<'_, 'scope, T> { +impl<'de, 'scope: 'de, T: DeserializeSeed<'de> + Clone> de::ArrayAccess<'de> for ArrayAccess<'_, 'scope, '_, T> { type Element = T::Output; type Error = Error<'scope>; diff --git a/crates/core/src/host/v8/error.rs b/crates/core/src/host/v8/error.rs index 1be9437b55f..950c84b3c58 100644 --- a/crates/core/src/host/v8/error.rs +++ b/crates/core/src/host/v8/error.rs @@ -5,7 +5,7 @@ use crate::database_logger::{BacktraceFrame, BacktraceProvider, ModuleBacktrace} use super::serialize_to_js; use core::fmt; use spacetimedb_sats::Serialize; -use v8::{Exception, HandleScope, Local, StackFrame, StackTrace, TryCatch, Value}; +use v8::{tc_scope, Exception, HandleScope, Local, PinScope, PinnedRef, StackFrame, StackTrace, TryCatch, Value}; /// The result of trying to convert a [`Value`] in scope `'scope` to some type `T`. pub(super) type ValueResult<'scope, T> = Result>; @@ -13,11 +13,11 @@ pub(super) type ValueResult<'scope, T> = Result>; /// Types that can convert into a JS string type. pub(super) trait IntoJsString { /// Converts `self` into a JS string. - fn into_string<'scope>(self, scope: &mut HandleScope<'scope>) -> Local<'scope, v8::String>; + fn into_string<'scope>(self, scope: &PinScope<'scope, '_>) -> Local<'scope, v8::String>; } impl IntoJsString for String { - fn into_string<'scope>(self, scope: &mut HandleScope<'scope>) -> Local<'scope, v8::String> { + fn into_string<'scope>(self, scope: &PinScope<'scope, '_>) -> Local<'scope, v8::String> { v8::String::new(scope, &self).unwrap() } } @@ -31,11 +31,11 @@ pub(super) struct ExceptionValue<'scope>(Local<'scope, Value>); /// Error types that can convert into JS exception values. pub(super) trait IntoException<'scope> { /// Converts `self` into a JS exception value. - fn into_exception(self, scope: &mut HandleScope<'scope>) -> ExceptionValue<'scope>; + fn into_exception(self, scope: &PinScope<'scope, '_>) -> ExceptionValue<'scope>; } impl<'scope> IntoException<'scope> for ExceptionValue<'scope> { - fn into_exception(self, _: &mut HandleScope<'scope>) -> ExceptionValue<'scope> { + fn into_exception(self, _: &PinScope<'scope, '_>) -> ExceptionValue<'scope> { self } } @@ -45,7 +45,7 @@ impl<'scope> IntoException<'scope> for ExceptionValue<'scope> { pub struct TypeError(pub M); impl<'scope, M: IntoJsString> IntoException<'scope> for TypeError { - fn into_exception(self, scope: &mut HandleScope<'scope>) -> ExceptionValue<'scope> { + fn into_exception(self, scope: &PinScope<'scope, '_>) -> ExceptionValue<'scope> { let msg = self.0.into_string(scope); ExceptionValue(Exception::type_error(scope, msg)) } @@ -56,7 +56,7 @@ impl<'scope, M: IntoJsString> IntoException<'scope> for TypeError { pub struct RangeError(pub M); impl<'scope, M: IntoJsString> IntoException<'scope> for RangeError { - fn into_exception(self, scope: &mut HandleScope<'scope>) -> ExceptionValue<'scope> { + fn into_exception(self, scope: &PinScope<'scope, '_>) -> ExceptionValue<'scope> { let msg = self.0.into_string(scope); ExceptionValue(Exception::range_error(scope, msg)) } @@ -71,7 +71,7 @@ pub(super) struct TerminationError { impl TerminationError { /// Convert `anyhow::Error` to a termination error. pub(super) fn from_error<'scope>( - scope: &mut HandleScope<'scope>, + scope: &PinScope<'scope, '_>, error: &anyhow::Error, ) -> ExcResult> { let __terminated__ = format!("{error}"); @@ -90,7 +90,7 @@ pub(super) struct CodeError { impl CodeError { /// Create a code error from a code. pub(super) fn from_code<'scope>( - scope: &mut HandleScope<'scope>, + scope: &PinScope<'scope, '_>, __code_error__: u16, ) -> ExcResult> { let error = Self { __code_error__ }; @@ -108,7 +108,7 @@ pub(super) struct BufferTooSmall { impl BufferTooSmall { /// Create a code error from a code. pub(super) fn from_requirement<'scope>( - scope: &mut HandleScope<'scope>, + scope: &PinScope<'scope, '_>, __buffer_too_small__: u32, ) -> ExcResult> { let error = Self { __buffer_too_small__ }; @@ -138,11 +138,11 @@ pub(super) trait Throwable<'scope> { /// /// If an exception has already been thrown, /// [`ExceptionThrown`] can be returned directly. - fn throw(self, scope: &mut HandleScope<'scope>) -> ExceptionThrown; + fn throw(self, scope: &PinScope<'scope, '_>) -> ExceptionThrown; } impl<'scope, T: IntoException<'scope>> Throwable<'scope> for T { - fn throw(self, scope: &mut HandleScope<'scope>) -> ExceptionThrown { + fn throw(self, scope: &PinScope<'scope, '_>) -> ExceptionThrown { let ExceptionValue(exception) = self.into_exception(scope); scope.throw_exception(exception); exception_already_thrown() @@ -202,7 +202,7 @@ pub(super) struct JsStackTrace { impl JsStackTrace { /// Converts a V8 [`StackTrace`] into one independent of `'scope`. - pub(super) fn from_trace<'scope>(scope: &mut HandleScope<'scope>, trace: Local<'scope, StackTrace>) -> Self { + pub(super) fn from_trace<'scope>(scope: &PinScope<'scope, '_>, trace: Local<'scope, StackTrace>) -> Self { let frames = (0..trace.get_frame_count()) .map(|index| { let frame = trace.get_frame(scope, index).unwrap(); @@ -213,7 +213,7 @@ impl JsStackTrace { } /// Construct a backtrace from `scope`. - pub(super) fn from_current_stack_trace(scope: &mut HandleScope<'_>) -> ExcResult { + pub(super) fn from_current_stack_trace(scope: &PinScope<'_, '_>) -> ExcResult { let trace = StackTrace::current_stack_trace(scope, 1024).ok_or_else(exception_already_thrown)?; Ok(Self::from_trace(scope, trace)) } @@ -263,7 +263,7 @@ pub(super) struct JsStackTraceFrame { impl JsStackTraceFrame { /// Converts a V8 [`StackFrame`] into one independent of `'scope`. - fn from_frame<'scope>(scope: &mut HandleScope<'scope>, frame: Local<'scope, StackFrame>) -> Self { + fn from_frame<'scope>(scope: &PinScope<'scope, '_>, frame: Local<'scope, StackFrame>) -> Self { let script_name = frame .get_script_name_or_source_url(scope) .map(|s| s.to_rust_string_lossy(scope)); @@ -329,7 +329,7 @@ impl fmt::Display for JsStackTraceFrame { impl JsError { /// Turns a caught JS exception in `scope` into a [`JSError`]. - fn from_caught(scope: &mut TryCatch<'_, HandleScope<'_>>) -> Self { + fn from_caught(scope: &PinnedRef<'_, TryCatch<'_, '_, HandleScope<'_>>>) -> Self { match scope.message() { Some(message) => Self { trace: message @@ -358,10 +358,10 @@ pub(super) fn log_traceback(func_type: &str, func: &str, e: &anyhow::Error) { /// 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>, + scope: &mut PinScope<'scope, '_>, + body: impl FnOnce(&mut PinScope<'scope, '_>) -> Result>, ) -> Result> { - let scope = &mut TryCatch::new(scope); + tc_scope!(scope, scope); body(scope).map_err(|e| match e { ErrorOrException::Err(e) => ErrorOrException::Err(e), ErrorOrException::Exception(_) => ErrorOrException::Exception(JsError::from_caught(scope)), diff --git a/crates/core/src/host/v8/from_value.rs b/crates/core/src/host/v8/from_value.rs index e3322eff0e9..114345434ee 100644 --- a/crates/core/src/host/v8/from_value.rs +++ b/crates/core/src/host/v8/from_value.rs @@ -5,22 +5,19 @@ use crate::host::v8::error::ExceptionValue; use super::error::{IntoException as _, TypeError, ValueResult}; use bytemuck::{AnyBitPattern, NoUninit}; use spacetimedb_sats::{i256, u256}; -use v8::{BigInt, Boolean, HandleScope, Int32, Local, Number, Uint32, Value}; +use v8::{BigInt, Boolean, Int32, Local, Number, PinScope, Uint32, Value}; /// Types that a v8 [`Value`] can be converted into. pub(super) trait FromValue: Sized { /// Converts `val` in `scope` to `Self` if possible. - fn from_value<'scope>(val: Local<'_, Value>, scope: &mut HandleScope<'scope>) -> ValueResult<'scope, Self>; + fn from_value<'scope>(val: Local<'_, Value>, scope: &PinScope<'scope, '_>) -> ValueResult<'scope, Self>; } /// Provides a [`FromValue`] implementation. macro_rules! impl_from_value { ($ty:ty, ($val:ident, $scope:ident) => $logic:expr) => { impl FromValue for $ty { - fn from_value<'scope>( - $val: Local<'_, Value>, - $scope: &mut HandleScope<'scope>, - ) -> ValueResult<'scope, Self> { + fn from_value<'scope>($val: Local<'_, Value>, $scope: &PinScope<'scope, '_>) -> ValueResult<'scope, Self> { $logic } } @@ -29,7 +26,7 @@ macro_rules! impl_from_value { /// Tries to cast `Value` into `T` or raises a JS exception as a returned `Err` value. pub(super) fn try_cast<'scope_a, 'scope_b, T>( - scope: &mut HandleScope<'scope_a>, + scope: &PinScope<'scope_a, '_>, val: Local<'scope_b, Value>, on_err: impl FnOnce(&str) -> String, ) -> ValueResult<'scope_a, Local<'scope_b, T>> @@ -50,13 +47,13 @@ pub(super) use cast; /// Returns a JS exception value indicating that a value overflowed /// when converting to the type `rust_ty`. -fn value_overflowed<'scope>(rust_ty: &str, scope: &mut HandleScope<'scope>) -> ExceptionValue<'scope> { +fn value_overflowed<'scope>(rust_ty: &str, scope: &PinScope<'scope, '_>) -> ExceptionValue<'scope> { TypeError(format!("Value overflowed `{rust_ty}`")).into_exception(scope) } /// Returns a JS exception value indicating that a value underflowed /// when converting to the type `rust_ty`. -fn value_underflowed<'scope>(rust_ty: &str, scope: &mut HandleScope<'scope>) -> ExceptionValue<'scope> { +fn value_underflowed<'scope>(rust_ty: &str, scope: &PinScope<'scope, '_>) -> ExceptionValue<'scope> { TypeError(format!("Value underflowed `{rust_ty}`")).into_exception(scope) } @@ -120,7 +117,7 @@ int64_from_value!(i64, i64_value); /// - `bigint` is the integer to convert. fn bigint_to_bytes<'scope, const N: usize, const W: usize, const UNSIGNED: bool>( rust_ty: &str, - scope: &mut HandleScope<'scope>, + scope: &PinScope<'scope, '_>, bigint: &BigInt, ) -> ValueResult<'scope, (bool, [u8; N])> where diff --git a/crates/core/src/host/v8/key_cache.rs b/crates/core/src/host/v8/key_cache.rs index bd0356d8eca..27c76d7f60e 100644 --- a/crates/core/src/host/v8/key_cache.rs +++ b/crates/core/src/host/v8/key_cache.rs @@ -1,12 +1,12 @@ use super::de::v8_interned_string; use core::cell::RefCell; use std::rc::Rc; -use v8::{Global, HandleScope, Local}; +use v8::{Global, Local, PinScope}; /// 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> { +pub(super) fn get_or_create_key_cache(scope: &PinScope<'_, '_>) -> Rc> { let context = scope.get_current_context(); context.get_slot::>().unwrap_or_else(|| { let cache = Rc::default(); @@ -30,29 +30,29 @@ pub(super) struct KeyCache { impl KeyCache { /// Returns the `tag` property name. - pub(super) fn tag<'scope>(&mut self, scope: &mut HandleScope<'scope>) -> Local<'scope, v8::String> { + pub(super) fn tag<'scope>(&mut self, scope: &PinScope<'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> { + pub(super) fn value<'scope>(&mut self, scope: &PinScope<'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> { + pub(super) fn describe_module<'scope>(&mut self, scope: &PinScope<'scope, '_>) -> Local<'scope, v8::String> { Self::get_or_create_key(scope, &mut self.describe_module, "__describe_module__") } /// Returns the `__call_reducer__` property name. - pub(super) fn call_reducer<'scope>(&mut self, scope: &mut HandleScope<'scope>) -> Local<'scope, v8::String> { + pub(super) fn call_reducer<'scope>(&mut self, scope: &PinScope<'scope, '_>) -> Local<'scope, v8::String> { Self::get_or_create_key(scope, &mut self.call_reducer, "__call_reducer__") } /// 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>, + scope: &PinScope<'scope, '_>, slot: &mut Option>, string: &str, ) -> Local<'scope, v8::String> { diff --git a/crates/core/src/host/v8/mod.rs b/crates/core/src/host/v8/mod.rs index 8582f18bde5..f4befd6a9a0 100644 --- a/crates/core/src/host/v8/mod.rs +++ b/crates/core/src/host/v8/mod.rs @@ -33,8 +33,7 @@ use std::sync::{Arc, LazyLock}; use std::time::Instant; use syscall::register_host_funs; use v8::{ - Context, ContextOptions, ContextScope, Function, HandleScope, Isolate, IsolateHandle, Local, Object, OwnedIsolate, - Value, + scope, Context, ContextScope, Function, Isolate, IsolateHandle, Local, Object, OwnedIsolate, PinScope, Value, }; mod de; @@ -398,7 +397,7 @@ fn with_script( callback_every: u64, callback: InterruptCallback, budget: ReducerBudget, - logic: impl for<'scope> FnOnce(&mut HandleScope<'scope>, Local<'scope, Value>) -> R, + logic: impl for<'scope> FnOnce(&mut PinScope<'scope, '_>, Local<'scope, Value>) -> R, ) -> (OwnedIsolate, R) { with_scope(isolate, callback_every, callback, budget, |scope| { let code = v8::String::new(scope, code).unwrap(); @@ -416,19 +415,21 @@ pub(crate) fn with_scope( callback_every: u64, callback: InterruptCallback, budget: ReducerBudget, - logic: impl FnOnce(&mut HandleScope<'_>) -> R, + logic: impl FnOnce(&mut PinScope<'_, '_>) -> R, ) -> (OwnedIsolate, R) { isolate.set_capture_stack_trace_for_uncaught_exceptions(true, 1024); let isolate_handle = isolate.thread_safe_handle(); - let mut scope_1 = HandleScope::new(&mut isolate); - let context = Context::new(&mut scope_1, ContextOptions::default()); - let mut scope_2 = ContextScope::new(&mut scope_1, context); + + let with_isolate = |isolate: &mut OwnedIsolate| -> R { + scope!(let scope, isolate); + let context = Context::new(scope, Default::default()); + let scope = &mut ContextScope::new(scope, context); + logic(scope) + }; let timeout_thread_cancel_flag = run_reducer_timeout(callback_every, callback, budget, isolate_handle); - let ret = logic(&mut scope_2); - drop(scope_2); - drop(scope_1); + let ret = with_isolate(&mut isolate); // Cancel the execution timeout in `run_reducer_timeout`. timeout_thread_cancel_flag.store(true, Ordering::Relaxed); @@ -497,12 +498,12 @@ fn duration_to_budget(_duration: Duration) -> ReducerBudget { } /// Returns the global object. -fn global<'scope>(scope: &mut HandleScope<'scope>) -> Local<'scope, Object> { +fn global<'scope>(scope: &PinScope<'scope, '_>) -> Local<'scope, Object> { scope.get_current_context().global(scope) } /// Returns the global property `key`. -fn get_global_property<'scope>(scope: &mut HandleScope<'scope>, key: Local<'scope, v8::String>) -> FnRet<'scope> { +fn get_global_property<'scope>(scope: &PinScope<'scope, '_>, key: Local<'scope, v8::String>) -> FnRet<'scope> { global(scope) .get(scope, key.into()) .ok_or_else(exception_already_thrown) @@ -510,7 +511,7 @@ fn get_global_property<'scope>(scope: &mut HandleScope<'scope>, key: Local<'scop /// Calls free function `fun` with `args`. fn call_free_fun<'scope>( - scope: &mut HandleScope<'scope>, + scope: &PinScope<'scope, '_>, fun: Local<'scope, Function>, args: &[Local<'scope, Value>], ) -> FnRet<'scope> { @@ -519,7 +520,7 @@ fn call_free_fun<'scope>( } // Calls the `__call_reducer__` function on the global proxy object using `op`. -fn call_call_reducer_from_op(scope: &mut HandleScope<'_>, op: ReducerOp<'_>) -> anyhow::Result>> { +fn call_call_reducer_from_op(scope: &mut PinScope<'_, '_>, op: ReducerOp<'_>) -> anyhow::Result>> { call_call_reducer( scope, op.id.into(), @@ -532,7 +533,7 @@ fn call_call_reducer_from_op(scope: &mut HandleScope<'_>, op: ReducerOp<'_>) -> // Calls the `__call_reducer__` function on the global proxy object. fn call_call_reducer( - scope: &mut HandleScope<'_>, + scope: &mut PinScope<'_, '_>, reducer_id: u32, sender: &Identity, conn_id: &ConnectionId, @@ -586,7 +587,7 @@ fn extract_description(program: &str) -> Result { } // Calls the `__describe_module__` function on the global proxy object to extract a [`RawModuleDef`]. -fn call_describe_module(scope: &mut HandleScope<'_>) -> anyhow::Result { +fn call_describe_module(scope: &mut PinScope<'_, '_>) -> anyhow::Result { // 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); @@ -615,7 +616,7 @@ mod test { fn with_script( code: &str, - logic: impl for<'scope> FnOnce(&mut HandleScope<'scope>, Local<'scope, Value>) -> R, + logic: impl for<'scope> FnOnce(&mut PinScope<'scope, '_>, Local<'scope, Value>) -> R, ) -> R { with_scope(|scope| { let code = v8::String::new(scope, code).unwrap(); diff --git a/crates/core/src/host/v8/ser.rs b/crates/core/src/host/v8/ser.rs index c9ed1887f68..2fe6979a1e3 100644 --- a/crates/core/src/host/v8/ser.rs +++ b/crates/core/src/host/v8/ser.rs @@ -11,10 +11,10 @@ use spacetimedb_sats::{ ser::{self, Serialize}, u256, }; -use v8::{Array, ArrayBuffer, HandleScope, IntegrityLevel, Local, Object, Uint8Array, Value}; +use v8::{Array, ArrayBuffer, IntegrityLevel, Local, Object, PinScope, Uint8Array, Value}; /// Serializes `value` into a V8 into `scope`. -pub(super) fn serialize_to_js<'scope>(scope: &mut HandleScope<'scope>, value: &impl Serialize) -> FnRet<'scope> { +pub(super) fn serialize_to_js<'scope>(scope: &PinScope<'scope, '_>, value: &impl Serialize) -> FnRet<'scope> { let key_cache = get_or_create_key_cache(scope); let key_cache = &mut *key_cache.borrow_mut(); value @@ -23,20 +23,20 @@ pub(super) fn serialize_to_js<'scope>(scope: &mut HandleScope<'scope>, value: &i } /// Deserializes to V8 values. -struct Serializer<'this, 'scope> { +struct Serializer<'this, 'scope, 'isolate> { /// The scope to serialize values into. - scope: &'this mut HandleScope<'scope>, + scope: &'this PinScope<'scope, 'isolate>, /// A cache for frequently used strings. key_cache: &'this mut KeyCache, } -impl<'this, 'scope> Serializer<'this, 'scope> { +impl<'this, 'scope, 'isolate> Serializer<'this, 'scope, 'isolate> { /// Creates a new serializer into `scope`. - pub fn new(scope: &'this mut HandleScope<'scope>, key_cache: &'this mut KeyCache) -> Self { + pub fn new(scope: &'this PinScope<'scope, 'isolate>, key_cache: &'this mut KeyCache) -> Self { Self { scope, key_cache } } - fn reborrow(&mut self) -> Serializer<'_, 'scope> { + fn reborrow(&mut self) -> Serializer<'_, 'scope, 'isolate> { Serializer { scope: self.scope, key_cache: self.key_cache, @@ -56,7 +56,7 @@ enum Error { } impl<'scope> Throwable<'scope> for Error { - fn throw(self, scope: &mut HandleScope<'scope>) -> ExceptionThrown { + fn throw(self, scope: &PinScope<'scope, '_>) -> ExceptionThrown { match self { Self::StringTooLarge(len) => { RangeError(format!("`{len}` bytes is too large to be a JS string")).throw(scope) @@ -90,20 +90,20 @@ macro_rules! serialize_primitive { /// However, the values of existing properties may be modified, /// which can be useful if the module wants to modify a property /// and then send the object back. -fn seal_object(scope: &mut HandleScope<'_>, object: &Object) -> ExcResult<()> { +fn seal_object(scope: &PinScope<'_, '_>, object: &Object) -> ExcResult<()> { let _ = object .set_integrity_level(scope, IntegrityLevel::Sealed) .ok_or_else(exception_already_thrown)?; Ok(()) } -impl<'this, 'scope> ser::Serializer for Serializer<'this, 'scope> { +impl<'this, 'scope, 'isolate> ser::Serializer for Serializer<'this, 'scope, 'isolate> { type Ok = Local<'scope, Value>; type Error = Error; - type SerializeArray = SerializeArray<'this, 'scope>; + type SerializeArray = SerializeArray<'this, 'scope, 'isolate>; type SerializeSeqProduct = Self::SerializeNamedProduct; - type SerializeNamedProduct = SerializeNamedProduct<'this, 'scope>; + type SerializeNamedProduct = SerializeNamedProduct<'this, 'scope, 'isolate>; // Serialization of primitive types defers to `ToValue`. serialize_primitive!(serialize_bool, bool); @@ -184,13 +184,13 @@ impl<'this, 'scope> ser::Serializer for Serializer<'this, 'scope> { } /// Serializes array elements and finalizes the JS array. -struct SerializeArray<'this, 'scope> { - inner: Serializer<'this, 'scope>, +struct SerializeArray<'this, 'scope, 'isolate> { + inner: Serializer<'this, 'scope, 'isolate>, array: Local<'scope, Array>, next_index: u32, } -impl<'scope> ser::SerializeArray for SerializeArray<'_, 'scope> { +impl<'scope> ser::SerializeArray for SerializeArray<'_, 'scope, '_> { type Ok = Local<'scope, Value>; type Error = Error; @@ -214,13 +214,13 @@ impl<'scope> ser::SerializeArray for SerializeArray<'_, 'scope> { } /// Serializes into JS objects where field names are turned into property names. -struct SerializeNamedProduct<'this, 'scope> { - inner: Serializer<'this, 'scope>, +struct SerializeNamedProduct<'this, 'scope, 'isolate> { + inner: Serializer<'this, 'scope, 'isolate>, object: Local<'scope, Object>, next_index: usize, } -impl<'scope> ser::SerializeSeqProduct for SerializeNamedProduct<'_, 'scope> { +impl<'scope> ser::SerializeSeqProduct for SerializeNamedProduct<'_, 'scope, '_> { type Ok = Local<'scope, Value>; type Error = Error; @@ -233,7 +233,7 @@ impl<'scope> ser::SerializeSeqProduct for SerializeNamedProduct<'_, 'scope> { } } -impl<'scope> ser::SerializeNamedProduct for SerializeNamedProduct<'_, 'scope> { +impl<'scope> ser::SerializeNamedProduct for SerializeNamedProduct<'_, 'scope, '_> { type Ok = Local<'scope, Value>; type Error = Error; @@ -246,7 +246,7 @@ impl<'scope> ser::SerializeNamedProduct for SerializeNamedProduct<'_, 'scope> { let value = elem.serialize(self.inner.reborrow())?; // Figure out the object property to use. - let scope = &mut *self.inner.scope; + let scope = self.inner.scope; let index = self.next_index; self.next_index += 1; let key = intern_field_name(scope, field_name, index).into(); diff --git a/crates/core/src/host/v8/syscall.rs b/crates/core/src/host/v8/syscall.rs index cf053045067..80aff12aba7 100644 --- a/crates/core/src/host/v8/syscall.rs +++ b/crates/core/src/host/v8/syscall.rs @@ -13,10 +13,10 @@ use crate::host::AbiCall; use spacetimedb_lib::Identity; use spacetimedb_primitives::{errno, ColId, IndexId, TableId}; use spacetimedb_sats::Serialize; -use v8::{Function, FunctionCallbackArguments, HandleScope, Local, Value}; +use v8::{Function, FunctionCallbackArguments, Local, PinScope, Value}; /// Registers all module -> host syscalls. -pub(super) fn register_host_funs(scope: &mut HandleScope<'_>) -> ExcResult<()> { +pub(super) fn register_host_funs(scope: &mut PinScope<'_, '_>) -> ExcResult<()> { macro_rules! register { ($wrapper:ident, $abi_call:expr, $fun:ident) => { register_host_fun(scope, stringify!($fun), |s, a| $wrapper($abi_call, s, a, $fun))?; @@ -72,9 +72,9 @@ pub(super) type FnRet<'scope> = ExcResult>; /// Registers a module -> host syscall in `scope` /// where the function has `name` and `body` fn register_host_fun( - scope: &mut HandleScope<'_>, + scope: &mut PinScope<'_, '_>, name: &str, - body: impl Copy + for<'scope> Fn(&mut HandleScope<'scope>, FunctionCallbackArguments<'scope>) -> FnRet<'scope>, + body: impl Copy + for<'scope> Fn(&mut PinScope<'scope, '_>, FunctionCallbackArguments<'scope>) -> FnRet<'scope>, ) -> ExcResult<()> { let name = v8_interned_string(scope, name).into(); let fun = Function::new(scope, adapt_fun(body)) @@ -93,8 +93,8 @@ struct TerminationFlag; /// Adapts `fun`, which returns a [`Value`] to one that works on [`v8::ReturnValue`]. fn adapt_fun( - fun: impl Copy + for<'scope> Fn(&mut HandleScope<'scope>, FunctionCallbackArguments<'scope>) -> FnRet<'scope>, -) -> impl Copy + for<'scope> Fn(&mut HandleScope<'scope>, FunctionCallbackArguments<'scope>, v8::ReturnValue) { + fun: impl Copy + for<'scope> Fn(&mut PinScope<'scope, '_>, FunctionCallbackArguments<'scope>) -> FnRet<'scope>, +) -> impl Copy + for<'scope> Fn(&mut PinScope<'scope, '_>, FunctionCallbackArguments<'scope>, v8::ReturnValue) { move |scope, args, mut rv| { // If the flag was set in `handle_nodes_error`, // we need to block all module -> host ABI calls. @@ -126,9 +126,9 @@ type SysCallResult = Result; /// Handles [`SysCallError`] if it occurs by throwing exceptions into JS. fn with_sys_result<'scope, O: Serialize>( abi_call: AbiCall, - scope: &mut HandleScope<'scope>, + scope: &mut PinScope<'scope, '_>, args: FunctionCallbackArguments<'scope>, - run: impl FnOnce(&mut HandleScope<'scope>, FunctionCallbackArguments<'scope>) -> SysCallResult, + run: impl FnOnce(&mut PinScope<'scope, '_>, FunctionCallbackArguments<'scope>) -> SysCallResult, ) -> FnRet<'scope> { match with_span(abi_call, scope, args, run) { Ok(ret) => serialize_to_js(scope, &ret), @@ -138,7 +138,7 @@ fn with_sys_result<'scope, O: Serialize>( } /// Turns a [`NodesError`] into a thrown exception. -fn throw_nodes_error(abi_call: AbiCall, scope: &mut HandleScope<'_>, error: NodesError) -> ExceptionThrown { +fn throw_nodes_error(abi_call: AbiCall, scope: &mut PinScope<'_, '_>, error: NodesError) -> ExceptionThrown { let res = match err_to_errno_and_log::(abi_call, error) { Ok(code) => CodeError::from_code(scope, code), Err(err) => { @@ -157,7 +157,7 @@ fn throw_nodes_error(abi_call: AbiCall, scope: &mut HandleScope<'_>, error: Node /// Collapses `res` where the `Ok(x)` where `x` is throwable. fn collapse_exc_thrown<'scope>( - scope: &mut HandleScope<'scope>, + scope: &PinScope<'scope, '_>, res: ExcResult>, ) -> ExceptionThrown { let (Ok(thrown) | Err(thrown)) = res.map(|ev| ev.throw(scope)); @@ -167,9 +167,9 @@ fn collapse_exc_thrown<'scope>( /// Tracks the span of `body` under the label `abi_call`. fn with_span<'scope, R>( abi_call: AbiCall, - scope: &mut HandleScope<'scope>, + scope: &mut PinScope<'scope, '_>, args: FunctionCallbackArguments<'scope>, - body: impl FnOnce(&mut HandleScope<'scope>, FunctionCallbackArguments<'scope>) -> R, + body: impl FnOnce(&mut PinScope<'scope, '_>, FunctionCallbackArguments<'scope>) -> R, ) -> R { // Start the span. let span_start = span::CallSpanStart::new(abi_call); @@ -215,7 +215,7 @@ fn with_span<'scope, R>( /// /// Throws a `TypeError` if: /// - `name` is not `string`. -fn table_id_from_name(scope: &mut HandleScope<'_>, args: FunctionCallbackArguments<'_>) -> SysCallResult { +fn table_id_from_name(scope: &mut PinScope<'_, '_>, args: FunctionCallbackArguments<'_>) -> SysCallResult { let name: &str = deserialize_js(scope, args.get(0))?; Ok(env_on_isolate(scope).instance_env.table_id_from_name(name)?) } @@ -251,7 +251,7 @@ fn table_id_from_name(scope: &mut HandleScope<'_>, args: FunctionCallbackArgumen /// /// Throws a `TypeError`: /// - if `name` is not `string`. -fn index_id_from_name(scope: &mut HandleScope<'_>, args: FunctionCallbackArguments<'_>) -> SysCallResult { +fn index_id_from_name(scope: &mut PinScope<'_, '_>, args: FunctionCallbackArguments<'_>) -> SysCallResult { let name: &str = deserialize_js(scope, args.get(0))?; Ok(env_on_isolate(scope).instance_env.index_id_from_name(name)?) } @@ -288,7 +288,7 @@ fn index_id_from_name(scope: &mut HandleScope<'_>, args: FunctionCallbackArgumen /// /// Throws a `TypeError` if: /// - `table_id` is not a `u32`. -fn datastore_table_row_count(scope: &mut HandleScope<'_>, args: FunctionCallbackArguments<'_>) -> SysCallResult { +fn datastore_table_row_count(scope: &mut PinScope<'_, '_>, args: FunctionCallbackArguments<'_>) -> SysCallResult { let table_id: TableId = deserialize_js(scope, args.get(0))?; Ok(env_on_isolate(scope).instance_env.datastore_table_row_count(table_id)?) } @@ -327,7 +327,7 @@ fn datastore_table_row_count(scope: &mut HandleScope<'_>, args: FunctionCallback /// /// Throws a `TypeError`: /// - if `table_id` is not a `u32`. -fn datastore_table_scan_bsatn(scope: &mut HandleScope<'_>, args: FunctionCallbackArguments<'_>) -> SysCallResult { +fn datastore_table_scan_bsatn(scope: &mut PinScope<'_, '_>, args: FunctionCallbackArguments<'_>) -> SysCallResult { let table_id: TableId = deserialize_js(scope, args.get(0))?; let env = env_on_isolate(scope); @@ -426,7 +426,7 @@ fn datastore_table_scan_bsatn(scope: &mut HandleScope<'_>, args: FunctionCallbac /// - `prefix`, `rstart`, and `rend` are not arrays of `u8`s. /// - `prefix_elems` is not a `u16`. fn datastore_index_scan_range_bsatn( - scope: &mut HandleScope<'_>, + scope: &mut PinScope<'_, '_>, args: FunctionCallbackArguments<'_>, ) -> SysCallResult { let index_id: IndexId = deserialize_js(scope, args.get(0))?; @@ -456,7 +456,7 @@ fn datastore_index_scan_range_bsatn( } /// Throws `{ __code_error__: NO_SUCH_ITER }`. -fn no_such_iter(scope: &mut HandleScope<'_>) -> ExceptionThrown { +fn no_such_iter(scope: &PinScope<'_, '_>) -> ExceptionThrown { let res = CodeError::from_code(scope, errno::NO_SUCH_ITER.get()); collapse_exc_thrown(scope, res) } @@ -511,7 +511,7 @@ fn no_such_iter(scope: &mut HandleScope<'_>) -> ExceptionThrown { /// Throws a `TypeError` if: /// - `iter` and `buffer_max_len` are not `u32`s. fn row_iter_bsatn_advance<'scope>( - scope: &mut HandleScope<'scope>, + scope: &mut PinScope<'scope, '_>, args: FunctionCallbackArguments<'scope>, ) -> SysCallResult<(bool, Vec)> { let row_iter_idx: u32 = deserialize_js(scope, args.get(0))?; @@ -578,7 +578,7 @@ fn row_iter_bsatn_advance<'scope>( /// Throws a `TypeError` if: /// - `iter` is not a `u32`. fn row_iter_bsatn_close<'scope>( - scope: &mut HandleScope<'scope>, + scope: &mut PinScope<'scope, '_>, args: FunctionCallbackArguments<'scope>, ) -> FnRet<'scope> { let row_iter_idx: u32 = deserialize_js(scope, args.get(0))?; @@ -654,7 +654,7 @@ fn row_iter_bsatn_close<'scope>( /// Throws a `TypeError` if: /// - `table_id` is not a `u32`. /// - `row` is not an array of `u8`s. -fn datastore_insert_bsatn(scope: &mut HandleScope<'_>, args: FunctionCallbackArguments<'_>) -> SysCallResult> { +fn datastore_insert_bsatn(scope: &mut PinScope<'_, '_>, args: FunctionCallbackArguments<'_>) -> SysCallResult> { let table_id: TableId = deserialize_js(scope, args.get(0))?; let mut row: Vec = deserialize_js(scope, args.get(1))?; @@ -737,7 +737,7 @@ fn datastore_insert_bsatn(scope: &mut HandleScope<'_>, args: FunctionCallbackArg /// Throws a `TypeError` if: /// - `table_id` is not a `u32`. /// - `row` is not an array of `u8`s. -fn datastore_update_bsatn(scope: &mut HandleScope<'_>, args: FunctionCallbackArguments<'_>) -> SysCallResult> { +fn datastore_update_bsatn(scope: &mut PinScope<'_, '_>, args: FunctionCallbackArguments<'_>) -> SysCallResult> { let table_id: TableId = deserialize_js(scope, args.get(0))?; let index_id: IndexId = deserialize_js(scope, args.get(1))?; let mut row: Vec = deserialize_js(scope, args.get(2))?; @@ -805,7 +805,7 @@ fn datastore_update_bsatn(scope: &mut HandleScope<'_>, args: FunctionCallbackArg /// - `prefix`, `rstart`, and `rend` are not arrays of `u8`s. /// - `prefix_elems` is not a `u16`. fn datastore_delete_by_index_scan_range_bsatn( - scope: &mut HandleScope<'_>, + scope: &mut PinScope<'_, '_>, args: FunctionCallbackArguments<'_>, ) -> SysCallResult { let index_id: IndexId = deserialize_js(scope, args.get(0))?; @@ -872,7 +872,7 @@ fn datastore_delete_by_index_scan_range_bsatn( /// - `table_id` is not a `u32`. /// - `relation` is not an array of `u8`s. fn datastore_delete_all_by_eq_bsatn( - scope: &mut HandleScope<'_>, + scope: &mut PinScope<'_, '_>, args: FunctionCallbackArguments<'_>, ) -> SysCallResult { let table_id: TableId = deserialize_js(scope, args.get(0))?; @@ -888,7 +888,7 @@ fn datastore_delete_all_by_eq_bsatn( /// volatile_nonatomic_schedule_immediate(reducer_name: string, args: u8[]) -> undefined /// ``` fn volatile_nonatomic_schedule_immediate<'scope>( - scope: &mut HandleScope<'scope>, + scope: &mut PinScope<'scope, '_>, args: FunctionCallbackArguments<'scope>, ) -> FnRet<'scope> { let name: String = deserialize_js(scope, args.get(0))?; @@ -921,7 +921,7 @@ fn volatile_nonatomic_schedule_immediate<'scope>( /// # Returns /// /// Returns nothing. -fn console_log<'scope>(scope: &mut HandleScope<'scope>, args: FunctionCallbackArguments<'scope>) -> FnRet<'scope> { +fn console_log<'scope>(scope: &mut PinScope<'scope, '_>, args: FunctionCallbackArguments<'scope>) -> FnRet<'scope> { let level: u32 = deserialize_js(scope, args.get(0))?; let msg = args.get(1).cast::(); @@ -989,7 +989,7 @@ fn console_log<'scope>(scope: &mut HandleScope<'scope>, args: FunctionCallbackAr /// Throws a `TypeError` if: /// - `name` is not a `string`. fn console_timer_start<'scope>( - scope: &mut HandleScope<'scope>, + scope: &mut PinScope<'scope, '_>, args: FunctionCallbackArguments<'scope>, ) -> FnRet<'scope> { let name = args.get(0).cast::(); @@ -1029,7 +1029,7 @@ fn console_timer_start<'scope>( /// Throws a `TypeError` if: /// - `span_id` is not a `u32`. fn console_timer_end<'scope>( - scope: &mut HandleScope<'scope>, + scope: &mut PinScope<'scope, '_>, args: FunctionCallbackArguments<'scope>, ) -> FnRet<'scope> { let span_id: u32 = deserialize_js(scope, args.get(0))?; @@ -1060,6 +1060,6 @@ fn console_timer_end<'scope>( /// # Returns /// /// Returns the module identity. -fn identity<'scope>(scope: &mut HandleScope<'scope>, _: FunctionCallbackArguments<'scope>) -> SysCallResult { +fn identity<'scope>(scope: &mut PinScope<'scope, '_>, _: FunctionCallbackArguments<'scope>) -> SysCallResult { Ok(*env_on_isolate(scope).instance_env.database_identity()) } diff --git a/crates/core/src/host/v8/to_value.rs b/crates/core/src/host/v8/to_value.rs index e97dd491eba..9fecfa5a090 100644 --- a/crates/core/src/host/v8/to_value.rs +++ b/crates/core/src/host/v8/to_value.rs @@ -2,20 +2,20 @@ use bytemuck::{NoUninit, Pod}; use spacetimedb_sats::{i256, u256}; -use v8::{BigInt, Boolean, HandleScope, Integer, Local, Number, Value}; +use v8::{BigInt, Boolean, Integer, Local, Number, PinScope, Value}; /// Types that can be converted to a v8-stack-allocated [`Value`]. /// The conversion can be done without the possibility for error. pub(super) trait ToValue { /// Converts `self` within `scope` (a sort of stack management in V8) to a [`Value`]. - fn to_value<'scope>(&self, scope: &mut HandleScope<'scope>) -> Local<'scope, Value>; + fn to_value<'scope>(&self, scope: &PinScope<'scope, '_>) -> Local<'scope, Value>; } /// Provides a [`ToValue`] implementation. macro_rules! impl_to_value { ($ty:ty, ($val:ident, $scope:ident) => $logic:expr) => { impl ToValue for $ty { - fn to_value<'scope>(&self, $scope: &mut HandleScope<'scope>) -> Local<'scope, Value> { + fn to_value<'scope>(&self, $scope: &PinScope<'scope, '_>) -> Local<'scope, Value> { let $val = *self; $logic.into() } @@ -48,7 +48,7 @@ impl_to_value!(u64, (val, scope) => BigInt::new_from_u64(scope, val)); /// /// The `sign` is passed along to the `BigInt`. fn le_bytes_to_bigint<'scope, const N: usize, const W: usize>( - scope: &mut HandleScope<'scope>, + scope: &PinScope<'scope, '_>, sign: bool, le_bytes: [u8; N], ) -> Local<'scope, BigInt> @@ -73,7 +73,7 @@ pub(super) const WORD_MIN: u64 = i64::MIN as u64; /// `i64::MIN` becomes `-1 * WORD_MIN * (2^64)^0 = -1 * WORD_MIN` /// `i128::MIN` becomes `-1 * (0 * (2^64)^0 + WORD_MIN * (2^64)^1) = -1 * WORD_MIN * 2^64` /// `i256::MIN` becomes `-1 * (0 * (2^64)^0 + 0 * (2^64)^1 + WORD_MIN * (2^64)^2) = -1 * WORD_MIN * (2^128)` -fn signed_min_bigint<'scope, const WORDS: usize>(scope: &mut HandleScope<'scope>) -> Local<'scope, BigInt> { +fn signed_min_bigint<'scope, const WORDS: usize>(scope: &PinScope<'scope, '_>) -> Local<'scope, BigInt> { let words = &mut [0u64; WORDS]; if let [.., last] = words.as_mut_slice() { *last = WORD_MIN; @@ -110,14 +110,14 @@ pub(in super::super) mod test { use core::fmt::Debug; use proptest::prelude::*; use spacetimedb_sats::proptest::{any_i256, any_u256}; - use v8::{Context, ContextScope, HandleScope, Isolate}; + use v8::{scope, Context, ContextScope, Isolate}; /// Sets up V8 and runs `logic` with a [`HandleScope`]. - pub(in super::super) fn with_scope(logic: impl FnOnce(&mut HandleScope<'_>) -> R) -> R { + pub(in super::super) fn with_scope(logic: impl FnOnce(&mut PinScope<'_, '_>) -> R) -> R { V8Runtime::init_for_test(); let isolate = &mut Isolate::new(<_>::default()); isolate.set_capture_stack_trace_for_uncaught_exceptions(true, 1024); - let scope = &mut HandleScope::new(isolate); + scope!(let scope, isolate); let context = Context::new(scope, Default::default()); let scope = &mut ContextScope::new(scope, context); From 689cea4074ae5a5d5f0a40df23b4ad4eda1325ee Mon Sep 17 00:00:00 2001 From: Mazdak Farrokhzad Date: Fri, 26 Sep 2025 15:06:20 +0200 Subject: [PATCH 2/4] v8: use fast static strings for known strings --- crates/core/src/host/v8/de.rs | 25 +++------ crates/core/src/host/v8/key_cache.rs | 67 ------------------------- crates/core/src/host/v8/mod.rs | 16 +++--- crates/core/src/host/v8/ser.rs | 35 ++++--------- crates/core/src/host/v8/string_const.rs | 33 ++++++++++++ crates/core/src/host/v8/syscall.rs | 20 +++++--- 6 files changed, 68 insertions(+), 128 deletions(-) delete mode 100644 crates/core/src/host/v8/key_cache.rs create mode 100644 crates/core/src/host/v8/string_const.rs diff --git a/crates/core/src/host/v8/de.rs b/crates/core/src/host/v8/de.rs index 419e0289ea3..3faf6099bfb 100644 --- a/crates/core/src/host/v8/de.rs +++ b/crates/core/src/host/v8/de.rs @@ -2,7 +2,7 @@ use super::error::{exception_already_thrown, ExcResult, ExceptionThrown, ExceptionValue, Throwable, TypeError}; use super::from_value::{cast, FromValue}; -use super::key_cache::{get_or_create_key_cache, KeyCache}; +use super::string_const::{TAG, VALUE}; use core::fmt; use core::iter::{repeat_n, RepeatN}; use core::marker::PhantomData; @@ -19,9 +19,7 @@ pub(super) fn deserialize_js_seed<'de, T: DeserializeSeed<'de>>( val: Local<'_, Value>, seed: T, ) -> ExcResult { - let key_cache = get_or_create_key_cache(scope); - let key_cache = &mut *key_cache.borrow_mut(); - let de = Deserializer::new(scope, val, key_cache); + let de = Deserializer::new(scope, val); seed.deserialize(de).map_err(|e| e.throw(scope)) } @@ -41,13 +39,9 @@ struct Deserializer<'this, 'scope, 'isolate> { impl<'this, 'scope, 'isolate> Deserializer<'this, 'scope, 'isolate> { /// Creates a new deserializer from `input` in `scope`. - fn new( - scope: &'this mut PinScope<'scope, 'isolate>, - input: Local<'_, Value>, - key_cache: &'this mut KeyCache, - ) -> Self { + fn new(scope: &'this mut PinScope<'scope, 'isolate>, input: Local<'_, Value>) -> Self { let input = Local::new(scope, input); - let common = DeserializerCommon { scope, key_cache }; + let common = DeserializerCommon { scope }; Deserializer { input, common } } } @@ -58,16 +52,11 @@ impl<'this, 'scope, 'isolate> Deserializer<'this, 'scope, 'isolate> { struct DeserializerCommon<'this, 'scope, 'isolate> { /// The scope of values to deserialize. scope: &'this mut PinScope<'scope, 'isolate>, - /// A cache for frequently used strings. - key_cache: &'this mut KeyCache, } impl<'scope, 'isolate> DeserializerCommon<'_, 'scope, 'isolate> { fn reborrow(&mut self) -> DeserializerCommon<'_, 'scope, 'isolate> { - DeserializerCommon { - scope: self.scope, - key_cache: self.key_cache, - } + DeserializerCommon { scope: self.scope } } } @@ -165,7 +154,7 @@ impl<'de, 'this, 'scope: 'de> de::Deserializer<'de> for Deserializer<'this, 'sco // We expect a canonical representation of a sum value in JS to be // `{ tag: "foo", value: a_value_for_foo }`. - let tag_field = self.common.key_cache.tag(scope); + let tag_field = TAG.string(scope); let object = cast!(scope, self.input, Object, "object for sum type `{}`", sum_name)?; // Extract the `tag` field. It needs to contain a string. @@ -175,7 +164,7 @@ impl<'de, 'this, 'scope: 'de> de::Deserializer<'de> for Deserializer<'this, 'sco let tag = cast!(scope, tag, v8::String, "string for sum tag of `{}`", sum_name)?; // Extract the `value` field. - let value_field = self.common.key_cache.value(scope); + let value_field = VALUE.string(scope); let value = object .get(scope, value_field.into()) .ok_or_else(exception_already_thrown)?; diff --git a/crates/core/src/host/v8/key_cache.rs b/crates/core/src/host/v8/key_cache.rs deleted file mode 100644 index 27c76d7f60e..00000000000 --- a/crates/core/src/host/v8/key_cache.rs +++ /dev/null @@ -1,67 +0,0 @@ -use super::de::v8_interned_string; -use core::cell::RefCell; -use std::rc::Rc; -use v8::{Global, Local, PinScope}; - -/// 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: &PinScope<'_, '_>) -> Rc> { - let context = scope.get_current_context(); - context.get_slot::>().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>, - /// The `value` property for sum values in JS. - value: Option>, - /// The `__describe_module__` property on the global proxy object. - describe_module: Option>, - /// The `__call_reducer__` property on the global proxy object. - call_reducer: Option>, -} - -impl KeyCache { - /// Returns the `tag` property name. - pub(super) fn tag<'scope>(&mut self, scope: &PinScope<'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: &PinScope<'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: &PinScope<'scope, '_>) -> Local<'scope, v8::String> { - Self::get_or_create_key(scope, &mut self.describe_module, "__describe_module__") - } - - /// Returns the `__call_reducer__` property name. - pub(super) fn call_reducer<'scope>(&mut self, scope: &PinScope<'scope, '_>) -> Local<'scope, v8::String> { - Self::get_or_create_key(scope, &mut self.call_reducer, "__call_reducer__") - } - - /// Returns an interned string corresponding to `string` - /// and memoizes the creation on the v8 side. - fn get_or_create_key<'scope>( - scope: &PinScope<'scope, '_>, - slot: &mut Option>, - 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 - } - } -} diff --git a/crates/core/src/host/v8/mod.rs b/crates/core/src/host/v8/mod.rs index f4befd6a9a0..ba9591cd836 100644 --- a/crates/core/src/host/v8/mod.rs +++ b/crates/core/src/host/v8/mod.rs @@ -10,8 +10,8 @@ use crate::host::wasm_common::module_host_actor::{ }; use crate::host::wasm_common::{RowIters, TimingSpanSet}; use crate::host::wasmtime::{epoch_ticker, ticks_in_duration, EPOCH_TICKS_PER_SECOND}; -use crate::host::ArgsTuple; -use crate::{host::Scheduler, module_host_context::ModuleCreationContext, replica_context::ReplicaContext}; +use crate::host::{ArgsTuple, Scheduler}; +use crate::{module_host_context::ModuleCreationContext, replica_context::ReplicaContext}; use core::ffi::c_void; use core::sync::atomic::{AtomicBool, Ordering}; use core::time::Duration; @@ -22,7 +22,6 @@ use error::{ TerminationError, Throwable, }; use from_value::cast; -use key_cache::get_or_create_key_cache; use ser::serialize_to_js; use spacetimedb_client_api_messages::energy::ReducerBudget; use spacetimedb_datastore::locking_tx_datastore::MutTxId; @@ -31,6 +30,7 @@ use spacetimedb_lib::{ConnectionId, Identity, RawModuleDef, Timestamp}; use spacetimedb_schema::auto_migrate::MigrationPolicy; use std::sync::{Arc, LazyLock}; use std::time::Instant; +use string_const::str_from_ident; use syscall::register_host_funs; use v8::{ scope, Context, ContextScope, Function, Isolate, IsolateHandle, Local, Object, OwnedIsolate, PinScope, Value, @@ -39,8 +39,8 @@ use v8::{ mod de; mod error; mod from_value; -mod key_cache; mod ser; +mod string_const; mod syscall; mod to_value; @@ -540,9 +540,7 @@ fn call_call_reducer( timestamp: i64, reducer_args: &ArgsTuple, ) -> anyhow::Result>> { - // Get a cached version of the `__call_reducer__` property. - let key_cache = get_or_create_key_cache(scope); - let call_reducer_key = key_cache.borrow_mut().call_reducer(scope); + let call_reducer_key = str_from_ident!(__call_reducer__).string(scope); catch_exception(scope, |scope| { // Serialize the arguments. @@ -588,9 +586,7 @@ fn extract_description(program: &str) -> Result { // Calls the `__describe_module__` function on the global proxy object to extract a [`RawModuleDef`]. fn call_describe_module(scope: &mut PinScope<'_, '_>) -> anyhow::Result { - // 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); + let describe_module_key = str_from_ident!(__describe_module__).string(scope); catch_exception(scope, |scope| { // Get the function on the global proxy object and convert to a function. diff --git a/crates/core/src/host/v8/ser.rs b/crates/core/src/host/v8/ser.rs index 2fe6979a1e3..dbbbb442e7d 100644 --- a/crates/core/src/host/v8/ser.rs +++ b/crates/core/src/host/v8/ser.rs @@ -2,7 +2,7 @@ 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::string_const::{TAG, VALUE}; use super::syscall::FnRet; use super::to_value::ToValue; use derive_more::From; @@ -15,32 +15,20 @@ use v8::{Array, ArrayBuffer, IntegrityLevel, Local, Object, PinScope, Uint8Array /// Serializes `value` into a V8 into `scope`. pub(super) fn serialize_to_js<'scope>(scope: &PinScope<'scope, '_>, value: &impl Serialize) -> FnRet<'scope> { - let key_cache = get_or_create_key_cache(scope); - let key_cache = &mut *key_cache.borrow_mut(); - value - .serialize(Serializer::new(scope, key_cache)) - .map_err(|e| e.throw(scope)) + value.serialize(Serializer::new(scope)).map_err(|e| e.throw(scope)) } /// Deserializes to V8 values. +#[derive(Copy, Clone)] struct Serializer<'this, 'scope, 'isolate> { /// The scope to serialize values into. scope: &'this PinScope<'scope, 'isolate>, - /// A cache for frequently used strings. - key_cache: &'this mut KeyCache, } impl<'this, 'scope, 'isolate> Serializer<'this, 'scope, 'isolate> { /// Creates a new serializer into `scope`. - pub fn new(scope: &'this PinScope<'scope, 'isolate>, key_cache: &'this mut KeyCache) -> Self { - Self { scope, key_cache } - } - - fn reborrow(&mut self) -> Serializer<'_, 'scope, 'isolate> { - Serializer { - scope: self.scope, - key_cache: self.key_cache, - } + pub fn new(scope: &'this PinScope<'scope, 'isolate>) -> Self { + Self { scope } } } @@ -158,22 +146,19 @@ impl<'this, 'scope, 'isolate> ser::Serializer for Serializer<'this, 'scope, 'iso } fn serialize_variant( - mut self, + self, tag: u8, var_name: Option<&str>, value: &T, ) -> Result { // Serialize the payload. - let value_value: Local<'scope, Value> = value.serialize(self.reborrow())?; + let value_value: Local<'scope, Value> = value.serialize(self)?; // Figure out the tag. let tag_value: Local<'scope, Value> = intern_field_name(self.scope, var_name, tag as usize).into(); let values = [tag_value, value_value]; // The property keys are always `"tag"` an `"value"`. - let names = [ - self.key_cache.tag(self.scope).into(), - self.key_cache.value(self.scope).into(), - ]; + let names = [TAG.string(self.scope).into(), VALUE.string(self.scope).into()]; // Stitch together the object. let prototype = v8::null(self.scope).into(); @@ -196,7 +181,7 @@ impl<'scope> ser::SerializeArray for SerializeArray<'_, 'scope, '_> { fn serialize_element(&mut self, elem: &T) -> Result<(), Self::Error> { // Serialize the current `elem`ent. - let value = elem.serialize(self.inner.reborrow())?; + let value = elem.serialize(self.inner)?; // Set the value to the array slot at `index`. let index = self.next_index; @@ -243,7 +228,7 @@ impl<'scope> ser::SerializeNamedProduct for SerializeNamedProduct<'_, 'scope, '_ elem: &T, ) -> Result<(), Self::Error> { // Serialize the field value. - let value = elem.serialize(self.inner.reborrow())?; + let value = elem.serialize(self.inner)?; // Figure out the object property to use. let scope = self.inner.scope; diff --git a/crates/core/src/host/v8/string_const.rs b/crates/core/src/host/v8/string_const.rs new file mode 100644 index 00000000000..8dc5ac6ee3f --- /dev/null +++ b/crates/core/src/host/v8/string_const.rs @@ -0,0 +1,33 @@ +use v8::{Local, OneByteConst, PinScope, String}; + +/// A string known at compile time to be ASCII. +pub(super) struct StringConst(OneByteConst); + +impl StringConst { + /// Returns a new string that is known to be ASCII and static. + pub(super) const fn new(string: &'static str) -> Self { + Self(String::create_external_onebyte_const(string.as_bytes())) + } + + /// Converts the string to a V8 string. + pub(super) fn string<'scope>(&'static self, scope: &PinScope<'scope, '_>) -> Local<'scope, String> { + String::new_from_onebyte_const(scope, &self.0) + .expect("`create_external_onebyte_const` should've asserted `.len() < kMaxLength`") + } +} + +/// Converts an identifier to a compile-time ASCII string. +#[macro_export] +macro_rules! str_from_ident { + ($ident:ident) => {{ + const STR: &$crate::host::v8::string_const::StringConst = + &$crate::host::v8::string_const::StringConst::new(stringify!($ident)); + STR + }}; +} +pub(super) use str_from_ident; + +/// The `tag` property of a sum object in JS. +pub(super) const TAG: &StringConst = str_from_ident!(tag); +/// The `value` property of a sum object in JS. +pub(super) const VALUE: &StringConst = str_from_ident!(value); diff --git a/crates/core/src/host/v8/syscall.rs b/crates/core/src/host/v8/syscall.rs index 80aff12aba7..84f90c414d9 100644 --- a/crates/core/src/host/v8/syscall.rs +++ b/crates/core/src/host/v8/syscall.rs @@ -1,12 +1,14 @@ -use super::de::{deserialize_js, scratch_buf, v8_interned_string}; -use super::error::ExcResult; +use super::de::{deserialize_js, scratch_buf}; +use super::error::{ExcResult, ExceptionThrown}; use super::ser::serialize_to_js; -use super::{env_on_isolate, exception_already_thrown}; +use super::string_const::{str_from_ident, StringConst}; +use super::{ + env_on_isolate, exception_already_thrown, global, BufferTooSmall, CodeError, JsInstanceEnv, JsStackTrace, + TerminationError, Throwable, +}; use crate::database_logger::{LogLevel, Record}; use crate::error::NodesError; use crate::host::instance_env::InstanceEnv; -use crate::host::v8::error::ExceptionThrown; -use crate::host::v8::{global, BufferTooSmall, CodeError, JsInstanceEnv, JsStackTrace, TerminationError, Throwable}; use crate::host::wasm_common::instrumentation::span; use crate::host::wasm_common::{err_to_errno_and_log, RowIterIdx, TimingSpan, TimingSpanIdx}; use crate::host::AbiCall; @@ -19,7 +21,9 @@ use v8::{Function, FunctionCallbackArguments, Local, PinScope, Value}; pub(super) fn register_host_funs(scope: &mut PinScope<'_, '_>) -> ExcResult<()> { macro_rules! register { ($wrapper:ident, $abi_call:expr, $fun:ident) => { - register_host_fun(scope, stringify!($fun), |s, a| $wrapper($abi_call, s, a, $fun))?; + register_host_fun(scope, str_from_ident!($fun), |s, a| { + $wrapper($abi_call, s, a, $fun) + })?; }; } @@ -73,10 +77,10 @@ pub(super) type FnRet<'scope> = ExcResult>; /// where the function has `name` and `body` fn register_host_fun( scope: &mut PinScope<'_, '_>, - name: &str, + name: &'static StringConst, body: impl Copy + for<'scope> Fn(&mut PinScope<'scope, '_>, FunctionCallbackArguments<'scope>) -> FnRet<'scope>, ) -> ExcResult<()> { - let name = v8_interned_string(scope, name).into(); + let name = name.string(scope).into(); let fun = Function::new(scope, adapt_fun(body)) .ok_or_else(exception_already_thrown)? .into(); From ef6fcf8afcec35c61cf850dd271779d41354ee3a Mon Sep 17 00:00:00 2001 From: Mazdak Farrokhzad Date: Fri, 3 Oct 2025 09:26:26 +0200 Subject: [PATCH 3/4] v8: expose in client-api & cli & standalone cli/publish: expose --js-path to publish js module --- crates/cli/src/subcommands/publish.rs | 31 ++++++++++++++++++++---- crates/client-api/Cargo.toml | 3 +++ crates/client-api/src/routes/database.rs | 17 ++++++++++++- crates/core/src/host/v8/mod.rs | 4 --- crates/core/src/messages/control_db.rs | 5 +++- crates/standalone/Cargo.toml | 1 + 6 files changed, 50 insertions(+), 11 deletions(-) diff --git a/crates/cli/src/subcommands/publish.rs b/crates/cli/src/subcommands/publish.rs index 4524f8c8081..bfaa96b17c6 100644 --- a/crates/cli/src/subcommands/publish.rs +++ b/crates/cli/src/subcommands/publish.rs @@ -46,8 +46,19 @@ pub fn cli() -> clap::Command { .short('b') .conflicts_with("project_path") .conflicts_with("build_options") + .conflicts_with("js_file") .help("The system path (absolute or relative) to the compiled wasm binary we should publish, instead of building the project."), ) + .arg( + Arg::new("js_file") + .value_parser(clap::value_parser!(PathBuf)) + .long("js-path") + .short('j') + .conflicts_with("project_path") + .conflicts_with("build_options") + .conflicts_with("wasm_file") + .help("UNSTABLE: The system path (absolute or relative) to the javascript file we should publish, instead of building the project."), + ) .arg( Arg::new("num_replicas") .value_parser(clap::value_parser!(u8)) @@ -90,6 +101,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E let force = args.get_flag("force"); let anon_identity = args.get_flag("anon_identity"); let wasm_file = args.get_one::("wasm_file"); + let js_file = args.get_one::("js_file"); let database_host = config.get_host_url(server)?; let build_options = args.get_one::("build_options").unwrap(); let num_replicas = args.get_one::("num_replicas"); @@ -108,13 +120,19 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E )); } - let path_to_wasm = if let Some(path) = wasm_file { - println!("Skipping build. Instead we are publishing {}", path.display()); - path.clone() + // Decide program file path and read program. + // Optionally build the program. + let (path_to_program, host_type) = if let Some(path) = wasm_file { + println!("(WASM) Skipping build. Instead we are publishing {}", path.display()); + (path.clone(), "Wasm") + } else if let Some(path) = js_file { + println!("(JS) Skipping build. Instead we are publishing {}", path.display()); + (path.clone(), "Js") } else { - build::exec_with_argstring(config.clone(), path_to_project, build_options).await? + let path = build::exec_with_argstring(config.clone(), path_to_project, build_options).await?; + (path, "Wasm") }; - let program_bytes = fs::read(path_to_wasm)?; + let program_bytes = fs::read(path_to_program)?; let server_address = { let url = Url::parse(&database_host)?; @@ -191,6 +209,9 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E builder = add_auth_header_opt(builder, &auth_header); + // Set the host type. + builder = builder.query(&[("host_type", host_type)]); + let res = builder.body(program_bytes).send().await?; if res.status() == StatusCode::UNAUTHORIZED && !anon_identity { // If we're not in the `anon_identity` case, then we have already forced the user to log in above (using `get_auth_header`), so this should be safe to unwrap. diff --git a/crates/client-api/Cargo.toml b/crates/client-api/Cargo.toml index 13c6bb816da..be32c90e9c3 100644 --- a/crates/client-api/Cargo.toml +++ b/crates/client-api/Cargo.toml @@ -64,3 +64,6 @@ toml.workspace = true [lints] workspace = true + +[features] +unstable = [] diff --git a/crates/client-api/src/routes/database.rs b/crates/client-api/src/routes/database.rs index ccab05d1280..ea056f57b2d 100644 --- a/crates/client-api/src/routes/database.rs +++ b/crates/client-api/src/routes/database.rs @@ -500,6 +500,8 @@ pub struct PublishDatabaseQueryParams { token: Option, #[serde(default)] policy: MigrationPolicy, + #[serde(default)] + host_type: HostType, } use spacetimedb_client_api_messages::http::SqlStmtResult; @@ -537,10 +539,23 @@ pub async fn publish( num_replicas, token, policy, + host_type, }): Query, Extension(auth): Extension, body: Bytes, ) -> axum::response::Result> { + // Feature gate V8 modules. + // The host must've been compiled with the `unstable` feature. + // TODO(v8): ungate this when V8 is ready to ship. + #[cfg(not(feature = "unstable"))] + if host_type == HostType::Js { + return Err(( + StatusCode::BAD_REQUEST, + "JS host type requires a host with unstable features", + ) + .into()); + } + // You should not be able to publish to a database that you do not own // so, unless you are the owner, this will fail. @@ -645,7 +660,7 @@ pub async fn publish( database_identity, program_bytes: body.into(), num_replicas, - host_type: HostType::Wasm, + host_type, }, policy, ) diff --git a/crates/core/src/host/v8/mod.rs b/crates/core/src/host/v8/mod.rs index ba9591cd836..e5f7656e4a2 100644 --- a/crates/core/src/host/v8/mod.rs +++ b/crates/core/src/host/v8/mod.rs @@ -97,10 +97,6 @@ impl ModuleRuntime for V8RuntimeInner { mcc.program.hash, ); - if true { - return Err::(anyhow::anyhow!("v8_todo")); - } - // TODO(v8): determine min required ABI by module and check that it's supported? // TODO(v8): validate function signatures like in WASM? Is that possible with V8? diff --git a/crates/core/src/messages/control_db.rs b/crates/core/src/messages/control_db.rs index 8299875e339..8f92f350324 100644 --- a/crates/core/src/messages/control_db.rs +++ b/crates/core/src/messages/control_db.rs @@ -75,9 +75,12 @@ pub struct NodeStatus { /// SEE: pub state: String, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[derive( + Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize, serde::Deserialize, +)] #[repr(i32)] pub enum HostType { + #[default] Wasm = 0, Js = 1, } diff --git a/crates/standalone/Cargo.toml b/crates/standalone/Cargo.toml index e719721721d..17b8d547062 100644 --- a/crates/standalone/Cargo.toml +++ b/crates/standalone/Cargo.toml @@ -19,6 +19,7 @@ required-features = [] # Features required to build this target (N/A for lib) [features] # Perfmaps for profiling modules perfmap = ["spacetimedb-core/perfmap"] +unstable = ["spacetimedb-client-api/unstable"] [dependencies] spacetimedb-client-api-messages.workspace = true From a9e52b415bab26e7f4af9d121bbcde95de191af8 Mon Sep 17 00:00:00 2001 From: Mazdak Farrokhzad Date: Thu, 2 Oct 2025 18:34:22 +0200 Subject: [PATCH 4/4] regen cli-reference.md --- docs/docs/cli-reference.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/docs/cli-reference.md b/docs/docs/cli-reference.md index 66be4ef3aef..11045a959f1 100644 --- a/docs/docs/cli-reference.md +++ b/docs/docs/cli-reference.md @@ -89,6 +89,7 @@ Run `spacetime help publish` for more detailed information. Default value: `.` * `-b`, `--bin-path ` — The system path (absolute or relative) to the compiled wasm binary we should publish, instead of building the project. +* `-j`, `--js-path ` — UNSTABLE: The system path (absolute or relative) to the javascript file we should publish, instead of building the project. * `--break-clients` — Allow breaking changes when publishing to an existing database identity. This will break existing clients. * `--anonymous` — Perform this action with an anonymous identity * `-s`, `--server ` — The nickname, domain name or URL of the server to host the database.