Skip to content

Commit 96a1ad4

Browse files
authored
[V8] Define trait ToValue for primitive types (#2946)
1 parent cbba2b2 commit 96a1ad4

File tree

2 files changed

+104
-1
lines changed

2 files changed

+104
-1
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use super::module_host::CallReducerParams;
12
use crate::{
23
host::{
34
module_common::{build_common_module_from_raw, ModuleCommon},
@@ -11,7 +12,7 @@ use anyhow::anyhow;
1112
use spacetimedb_datastore::locking_tx_datastore::MutTxId;
1213
use std::sync::{Arc, LazyLock};
1314

14-
use super::module_host::CallReducerParams;
15+
mod to_value;
1516

1617
/// The V8 runtime, for modules written in e.g., JS or TypeScript.
1718
#[derive(Default)]

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

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#![allow(dead_code)]
2+
3+
use bytemuck::{NoUninit, Pod};
4+
use spacetimedb_sats::{i256, u256};
5+
use v8::{BigInt, Boolean, HandleScope, Integer, Local, Number, Value};
6+
7+
/// Types that can be converted to a v8-stack-allocated [`Value`].
8+
/// The conversion can be done without the possibility for error.
9+
pub(super) trait ToValue {
10+
/// Convert `self` within `scope` (a sort of stack management in V8) to a [`Value`].
11+
fn to_value<'s>(&self, scope: &mut HandleScope<'s>) -> Local<'s, Value>;
12+
}
13+
14+
/// Provides a [`ToValue`] implementation.
15+
macro_rules! impl_to_value {
16+
($ty:ty, ($val:ident, $scope:ident) => $logic:expr) => {
17+
impl ToValue for $ty {
18+
fn to_value<'s>(&self, $scope: &mut HandleScope<'s>) -> Local<'s, Value> {
19+
let $val = *self;
20+
$logic.into()
21+
}
22+
}
23+
};
24+
}
25+
26+
// Floats are the most direct conversion.
27+
impl_to_value!(f32, (val, scope) => (val as f64).to_value(scope));
28+
impl_to_value!(f64, (val, scope) => Number::new(scope, val));
29+
30+
// Booleans have dedicated conversions.
31+
impl_to_value!(bool, (val, scope) => Boolean::new(scope, val));
32+
33+
// Sub-32-bit integers get widened to 32-bit first.
34+
impl_to_value!(i8, (val, scope) => (val as i32).to_value(scope));
35+
impl_to_value!(u8, (val, scope) => (val as u32).to_value(scope));
36+
impl_to_value!(i16, (val, scope) => (val as i32).to_value(scope));
37+
impl_to_value!(u16, (val, scope) => (val as u32).to_value(scope));
38+
39+
// 32-bit integers have dedicated conversions.
40+
impl_to_value!(i32, (val, scope) => Integer::new(scope, val));
41+
impl_to_value!(u32, (val, scope) => Integer::new_from_unsigned(scope, val));
42+
43+
// 64-bit integers have dedicated conversions.
44+
impl_to_value!(i64, (val, scope) => BigInt::new_from_i64(scope, val));
45+
impl_to_value!(u64, (val, scope) => BigInt::new_from_u64(scope, val));
46+
47+
/// Converts the little-endian bytes of a number to a V8 [`BigInt`].
48+
///
49+
/// The `sign` is passed along to the `BigInt`.
50+
fn le_bytes_to_bigint<'s, const N: usize, const W: usize>(
51+
scope: &mut HandleScope<'s>,
52+
sign: bool,
53+
le_bytes: [u8; N],
54+
) -> Local<'s, BigInt>
55+
where
56+
[u8; N]: NoUninit,
57+
[u64; W]: Pod,
58+
{
59+
let words = bytemuck::must_cast::<_, [u64; W]>(le_bytes).map(u64::from_le);
60+
BigInt::new_from_words(scope, sign, &words).unwrap()
61+
}
62+
63+
// Unsigned 128-bit and 256-bit integers have dedicated conversions.
64+
// They are convered to a list of words before becoming `BigInt`s.
65+
impl_to_value!(u128, (val, scope) => le_bytes_to_bigint::<16, 2>(scope, false, val.to_le_bytes()));
66+
impl_to_value!(u256, (val, scope) => le_bytes_to_bigint::<32, 4>(scope, false, val.to_le_bytes()));
67+
68+
/// Returns `iN::MIN` for `N = 8 * WORDS` as a V8 [`BigInt`].
69+
///
70+
/// Examples:
71+
/// `i64::MIN` becomes `-1 * WORD_MIN * (2^64)^0 = -1 * WORD_MIN`
72+
/// `i128::MIN` becomes `-1 * (0 * (2^64)^0 + WORD_MIN * (2^64)^1) = -1 * WORD_MIN * 2^64`
73+
/// `i256::MIN` becomes `-1 * (0 * (2^64)^0 + 0 * (2^64)^1 + WORD_MIN * (2^64)^2) = -1 * WORD_MIN * (2^128)`
74+
fn signed_min_bigint<'s, const WORDS: usize>(scope: &mut HandleScope<'s>) -> Local<'s, BigInt> {
75+
const WORD_MIN: u64 = i64::MIN as u64;
76+
let words = &mut [0u64; WORDS];
77+
if let [.., last] = words.as_mut_slice() {
78+
*last = WORD_MIN;
79+
}
80+
v8::BigInt::new_from_words(scope, true, words).unwrap()
81+
}
82+
83+
// Signed 128-bit and 256-bit integers have dedicated conversions.
84+
//
85+
// For the negative number case, the magnitude is computed and the sign is passed along.
86+
// A special case is the minimum number.
87+
impl_to_value!(i128, (val, scope) => {
88+
let sign = val.is_negative();
89+
let magnitude = if sign { val.checked_neg() } else { Some(val) };
90+
match magnitude {
91+
Some(magnitude) => le_bytes_to_bigint::<16, 2>(scope, sign, magnitude.to_le_bytes()),
92+
None => signed_min_bigint::<2>(scope),
93+
}
94+
});
95+
impl_to_value!(i256, (val, scope) => {
96+
let sign = val.is_negative();
97+
let magnitude = if sign { val.checked_neg() } else { Some(val) };
98+
match magnitude {
99+
Some(magnitude) => le_bytes_to_bigint::<32, 4>(scope, sign, magnitude.to_le_bytes()),
100+
None => signed_min_bigint::<4>(scope),
101+
}
102+
});

0 commit comments

Comments
 (0)