spacetimedb/host/wasmtime/
mod.rs1use 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 #[cfg(feature = "perfmap")]
45 config.profiler(wasmtime::ProfilingStrategy::PerfMap);
46
47 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 [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 const QUANTA_MULTIPLIER: u64 = 1_000;
121}
122
123impl From<ReducerBudget> for WasmtimeFuel {
124 fn from(v: ReducerBudget) -> Self {
125 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#[derive(Clone, Copy)]
186pub struct Mem {
187 pub memory: wasmtime::Memory,
189}
190
191impl Mem {
192 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 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 unsafe { &mut *(v as *mut [u8] as *mut MemView) }
218 }
219 fn from_slice(v: &[u8]) -> &Self {
220 unsafe { &*(v as *const [u8] as *const MemView) }
222 }
223
224 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 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 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 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 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#[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
281trait 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}