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::host::module_host::ModuleRuntime;
11use crate::module_host_context::ModuleCreationContext;
12
13mod wasm_instance_env;
14mod wasmtime_module;
15
16use wasmtime_module::WasmtimeModule;
17
18use self::wasm_instance_env::WasmInstanceEnv;
19
20use super::wasm_common::module_host_actor::InitializationError;
21use super::wasm_common::{abi, module_host_actor::WasmModuleHostActor, ModuleCreationError};
22
23pub struct WasmtimeRuntime {
24 engine: Engine,
25 linker: Box<Linker<WasmInstanceEnv>>,
26}
27
28const EPOCH_TICK_LENGTH: Duration = Duration::from_millis(10);
29
30const EPOCH_TICKS_PER_SECOND: u64 = Duration::from_secs(1).div_duration_f64(EPOCH_TICK_LENGTH) as u64;
31
32impl WasmtimeRuntime {
33 pub fn new(data_dir: Option<&ServerDataDir>) -> Self {
34 let mut config = wasmtime::Config::new();
35 config
36 .cranelift_opt_level(wasmtime::OptLevel::Speed)
37 .consume_fuel(true)
38 .epoch_interruption(true)
39 .wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable);
40
41 #[cfg(feature = "perfmap")]
46 config.profiler(wasmtime::ProfilingStrategy::PerfMap);
47
48 if let Some(data_dir) = data_dir {
50 let _ = Self::set_cache_config(&mut config, data_dir.wasmtime_cache());
51 }
52
53 let engine = Engine::new(&config).unwrap();
54
55 let weak_engine = engine.weak();
56 tokio::spawn(async move {
57 let mut interval = tokio::time::interval(EPOCH_TICK_LENGTH);
58 loop {
59 interval.tick().await;
60 let Some(engine) = weak_engine.upgrade() else { break };
61 engine.increment_epoch();
62 }
63 });
64
65 let mut linker = Box::new(Linker::new(&engine));
66 WasmtimeModule::link_imports(&mut linker).unwrap();
67
68 WasmtimeRuntime { engine, linker }
69 }
70
71 fn set_cache_config(config: &mut wasmtime::Config, cache_dir: WasmtimeCacheDir) -> anyhow::Result<()> {
72 use std::io::Write;
73 let cache_config = toml::toml! {
74 [cache]
76 enabled = true
77 directory = (toml::Value::try_from(cache_dir.0)?)
78 };
79 let tmpfile = tempfile::NamedTempFile::new()?;
80 write!(&tmpfile, "{cache_config}")?;
81 config.cache_config_load(tmpfile.path())?;
82 Ok(())
83 }
84}
85
86impl ModuleRuntime for WasmtimeRuntime {
87 fn make_actor(&self, mcc: ModuleCreationContext) -> anyhow::Result<impl super::module_host::Module> {
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}