spacetimedb/host/wasmtime/
mod.rs

1use std::borrow::Cow;
2use std::time::Duration;
3
4use anyhow::Context;
5use spacetimedb_paths::server::{ServerDataDir, WasmtimeCacheDir};
6use wasmtime::{Engine, Linker, Module, StoreContext, StoreContextMut};
7
8use crate::energy::{EnergyQuanta, ReducerBudget};
9use crate::error::NodesError;
10use crate::module_host_context::ModuleCreationContext;
11
12mod wasm_instance_env;
13mod wasmtime_module;
14
15use wasmtime_module::WasmtimeModule;
16
17use self::wasm_instance_env::WasmInstanceEnv;
18
19use super::wasm_common::module_host_actor::InitializationError;
20use super::wasm_common::{abi, module_host_actor::WasmModuleHostActor, ModuleCreationError};
21
22pub struct WasmtimeRuntime {
23    engine: Engine,
24    linker: Box<Linker<WasmInstanceEnv>>,
25}
26
27const EPOCH_TICK_LENGTH: Duration = Duration::from_millis(10);
28
29const EPOCH_TICKS_PER_SECOND: u64 = Duration::from_secs(1).div_duration_f64(EPOCH_TICK_LENGTH) as u64;
30
31impl WasmtimeRuntime {
32    pub fn new(data_dir: Option<&ServerDataDir>) -> Self {
33        let mut config = wasmtime::Config::new();
34        config
35            .cranelift_opt_level(wasmtime::OptLevel::Speed)
36            .consume_fuel(true)
37            .epoch_interruption(true)
38            .wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable);
39
40        // Offer a compile-time flag for enabling perfmap generation,
41        // so `perf` can display JITted symbol names.
42        // Ideally we would be able to configure this at runtime via a flag to `spacetime start`,
43        // but this is good enough for now.
44        #[cfg(feature = "perfmap")]
45        config.profiler(wasmtime::ProfilingStrategy::PerfMap);
46
47        // ignore errors for this - if we're not able to set up caching, that's fine, it's just an optimization
48        if let Some(data_dir) = data_dir {
49            let _ = Self::set_cache_config(&mut config, data_dir.wasmtime_cache());
50        }
51
52        let engine = Engine::new(&config).unwrap();
53
54        let weak_engine = engine.weak();
55        tokio::spawn(async move {
56            let mut interval = tokio::time::interval(EPOCH_TICK_LENGTH);
57            loop {
58                interval.tick().await;
59                let Some(engine) = weak_engine.upgrade() else { break };
60                engine.increment_epoch();
61            }
62        });
63
64        let mut linker = Box::new(Linker::new(&engine));
65        WasmtimeModule::link_imports(&mut linker).unwrap();
66
67        WasmtimeRuntime { engine, linker }
68    }
69
70    fn set_cache_config(config: &mut wasmtime::Config, cache_dir: WasmtimeCacheDir) -> anyhow::Result<()> {
71        use std::io::Write;
72        let cache_config = toml::toml! {
73            // see <https://docs.wasmtime.dev/cli-cache.html> for options here
74            [cache]
75            enabled = true
76            directory = (toml::Value::try_from(cache_dir.0)?)
77        };
78        let tmpfile = tempfile::NamedTempFile::new()?;
79        write!(&tmpfile, "{}", cache_config)?;
80        config.cache_config_load(tmpfile.path())?;
81        Ok(())
82    }
83
84    pub fn make_actor(
85        &self,
86        mcc: ModuleCreationContext,
87    ) -> Result<impl super::module_host::Module, ModuleCreationError> {
88        let module = Module::new(&self.engine, &mcc.program.bytes).map_err(ModuleCreationError::WasmCompileError)?;
89
90        let func_imports = module
91            .imports()
92            .filter(|imp| matches!(imp.ty(), wasmtime::ExternType::Func(_)));
93        let abi = abi::determine_spacetime_abi(func_imports, |imp| imp.module())?;
94
95        abi::verify_supported(WasmtimeModule::IMPLEMENTED_ABI, abi)?;
96
97        let module = self
98            .linker
99            .instantiate_pre(&module)
100            .map_err(InitializationError::Instantiation)?;
101
102        let module = WasmtimeModule::new(module);
103
104        WasmModuleHostActor::new(mcc, module).map_err(Into::into)
105    }
106}
107
108#[derive(Debug, derive_more::From)]
109pub enum WasmError {
110    Db(NodesError),
111    BufferTooSmall,
112    Wasm(anyhow::Error),
113}
114
115#[derive(Copy, Clone)]
116struct WasmtimeFuel(u64);
117
118impl WasmtimeFuel {
119    /// 1000 energy quanta == 1 wasmtime fuel unit
120    const QUANTA_MULTIPLIER: u64 = 1_000;
121}
122
123impl From<ReducerBudget> for WasmtimeFuel {
124    fn from(v: ReducerBudget) -> Self {
125        // ReducerBudget being u64 is load-bearing here - if it was u128 and v was ReducerBudget::MAX,
126        // truncating this result would mean that with set_store_fuel(budget.into()), get_store_fuel()
127        // would be wildly different than the original `budget`, and the energy usage for the reducer
128        // would be u64::MAX even if it did nothing. ask how I know.
129        WasmtimeFuel(v.get() / Self::QUANTA_MULTIPLIER)
130    }
131}
132
133impl From<WasmtimeFuel> for ReducerBudget {
134    fn from(v: WasmtimeFuel) -> Self {
135        ReducerBudget::new(v.0 * WasmtimeFuel::QUANTA_MULTIPLIER)
136    }
137}
138
139impl From<WasmtimeFuel> for EnergyQuanta {
140    fn from(fuel: WasmtimeFuel) -> Self {
141        EnergyQuanta::new(u128::from(fuel.0) * u128::from(WasmtimeFuel::QUANTA_MULTIPLIER))
142    }
143}
144
145pub trait WasmPointee {
146    type Pointer;
147    fn write_to(self, mem: &mut MemView, ptr: Self::Pointer) -> Result<(), MemError>;
148    fn read_from(mem: &mut MemView, ptr: Self::Pointer) -> Result<Self, MemError>
149    where
150        Self: Sized;
151}
152macro_rules! impl_pointee {
153    ($($t:ty),*) => {
154        $(impl WasmPointee for $t {
155            type Pointer = u32;
156            fn write_to(self, mem: &mut MemView, ptr: Self::Pointer) -> Result<(), MemError> {
157                let bytes = self.to_le_bytes();
158                mem.deref_slice_mut(ptr, bytes.len() as u32)?.copy_from_slice(&bytes);
159                Ok(())
160            }
161            fn read_from(mem: &mut MemView, ptr: Self::Pointer) -> Result<Self, MemError> {
162                Ok(Self::from_le_bytes(*mem.deref_array(ptr)?))
163            }
164        })*
165    };
166}
167impl_pointee!(u8, u16, u32, u64);
168impl_pointee!(super::wasm_common::RowIterIdx);
169
170impl WasmPointee for spacetimedb_lib::Identity {
171    type Pointer = u32;
172    fn write_to(self, mem: &mut MemView, ptr: Self::Pointer) -> Result<(), MemError> {
173        let bytes = self.to_byte_array();
174        mem.deref_slice_mut(ptr, bytes.len() as u32)?.copy_from_slice(&bytes);
175        Ok(())
176    }
177    fn read_from(mem: &mut MemView, ptr: Self::Pointer) -> Result<Self, MemError> {
178        Ok(Self::from_byte_array(*mem.deref_array(ptr)?))
179    }
180}
181
182type WasmPtr<T> = <T as WasmPointee>::Pointer;
183
184/// Wraps access to WASM linear memory with some additional functionality.
185#[derive(Clone, Copy)]
186pub struct Mem {
187    /// The underlying WASM `memory` instance.
188    pub memory: wasmtime::Memory,
189}
190
191impl Mem {
192    /// Constructs an instance of `Mem` from an exports map.
193    pub fn extract(exports: &wasmtime::Instance, store: impl wasmtime::AsContextMut) -> anyhow::Result<Self> {
194        Ok(Self {
195            memory: exports.get_memory(store, "memory").context("no memory export")?,
196        })
197    }
198
199    /// Creates and returns a view into the actual memory `store`.
200    /// This view allows for reads and writes.
201    pub fn view_and_store_mut<'a, T>(&self, store: impl Into<StoreContextMut<'a, T>>) -> (&'a mut MemView, &'a mut T) {
202        let (mem, store_data) = self.memory.data_and_store_mut(store);
203        (MemView::from_slice_mut(mem), store_data)
204    }
205
206    fn view<'a, T: 'a>(&self, store: impl Into<StoreContext<'a, T>>) -> &'a MemView {
207        MemView::from_slice(self.memory.data(store))
208    }
209}
210
211#[repr(transparent)]
212pub struct MemView([u8]);
213
214impl MemView {
215    fn from_slice_mut(v: &mut [u8]) -> &mut Self {
216        // SAFETY: MemView is repr(transparent) over [u8]
217        unsafe { &mut *(v as *mut [u8] as *mut MemView) }
218    }
219    fn from_slice(v: &[u8]) -> &Self {
220        // SAFETY: MemView is repr(transparent) over [u8]
221        unsafe { &*(v as *const [u8] as *const MemView) }
222    }
223
224    /// Get a byte slice of wasm memory given a pointer and a length.
225    pub fn deref_slice(&self, offset: WasmPtr<u8>, len: u32) -> Result<&[u8], MemError> {
226        if offset == 0 {
227            return Err(MemError::Null);
228        }
229        self.0
230            .get(offset as usize..)
231            .and_then(|s| s.get(..len as usize))
232            .ok_or(MemError::OutOfBounds)
233    }
234
235    /// Get a utf8 slice of wasm memory given a pointer and a length.
236    fn deref_str(&self, offset: WasmPtr<u8>, len: u32) -> Result<&str, MemError> {
237        let b = self.deref_slice(offset, len)?;
238        std::str::from_utf8(b).map_err(MemError::Utf8)
239    }
240
241    /// Lossily get a utf8 slice of wasm memory given a pointer and a length, converting any
242    /// non-utf8 bytes to `U+FFFD REPLACEMENT CHARACTER`.
243    fn deref_str_lossy(&self, offset: WasmPtr<u8>, len: u32) -> Result<Cow<str>, MemError> {
244        self.deref_slice(offset, len).map(String::from_utf8_lossy)
245    }
246
247    /// Get a mutable byte slice of wasm memory given a pointer and a length;
248    fn deref_slice_mut(&mut self, offset: WasmPtr<u8>, len: u32) -> Result<&mut [u8], MemError> {
249        if offset == 0 {
250            return Err(MemError::Null);
251        }
252        self.0
253            .get_mut(offset as usize..)
254            .and_then(|s| s.get_mut(..len as usize))
255            .ok_or(MemError::OutOfBounds)
256    }
257
258    /// Get a byte array of wasm memory the size of `N`.
259    fn deref_array<const N: usize>(&self, offset: WasmPtr<u8>) -> Result<&[u8; N], MemError> {
260        Ok(self.deref_slice(offset, N as u32)?.try_into().unwrap())
261    }
262}
263
264/// An error that can result from operations on [`MemView`].
265#[derive(thiserror::Error, Debug)]
266pub enum MemError {
267    #[error("out of bounds pointer passed to a spacetime function")]
268    OutOfBounds,
269    #[error("null pointer passed to a spacetime function")]
270    Null,
271    #[error("invalid utf8 passed to a spacetime function")]
272    Utf8(#[from] std::str::Utf8Error),
273}
274
275impl From<MemError> for WasmError {
276    fn from(err: MemError) -> Self {
277        WasmError::Wasm(err.into())
278    }
279}
280
281/// Extension trait to gracefully handle null `WasmPtr`s, e.g.
282/// `mem.deref_slice(ptr, len).check_nullptr()? == Option<&[u8]>`.
283trait NullableMemOp<T> {
284    fn check_nullptr(self) -> Result<Option<T>, MemError>;
285}
286impl<T> NullableMemOp<T> for Result<T, MemError> {
287    fn check_nullptr(self) -> Result<Option<T>, MemError> {
288        match self {
289            Ok(x) => Ok(Some(x)),
290            Err(MemError::Null) => Ok(None),
291            Err(e) => Err(e),
292        }
293    }
294}