Skip to main content

torvyn_engine/
wasmtime_engine.rs

1//! Wasmtime-based implementation of the [`WasmEngine`] trait.
2//!
3//! This module is gated behind the `wasmtime-backend` feature flag (default: on).
4//!
5//! # LLI DEVIATIONS from LLI-04 (adapted per spike findings)
6//! - Wasmtime v42 instead of v29
7//! - `async_support(true)` removed: deprecated no-op in v42
8//! - `post_return_async()` removed: deprecated no-op in v42
9//! - `wasmtime::Error` is distinct from `anyhow::Error` in v42
10
11use async_trait::async_trait;
12use wasmtime::component::{Component, Linker};
13use wasmtime::{Config, Engine, Store, StoreLimitsBuilder};
14
15use torvyn_types::ComponentId;
16
17use crate::config::WasmtimeEngineConfig;
18use crate::error::EngineError;
19use crate::traits::WasmEngine;
20use crate::types::{
21    CompiledComponent, CompiledComponentInner, ComponentInstance, ComponentInstanceInner,
22    HostState, ImportBindings, ImportBindingsInner, WasmtimeInstanceState,
23};
24
25/// Wasmtime-based Wasm engine implementation.
26///
27/// Wraps a `wasmtime::Engine` configured per [`WasmtimeEngineConfig`].
28/// Thread-safe: the inner `wasmtime::Engine` is `Send + Sync` and can
29/// be shared across async tasks.
30///
31/// # COLD PATH — constructed once at host startup.
32///
33/// # Examples
34/// ```no_run
35/// use torvyn_engine::{WasmtimeEngine, WasmtimeEngineConfig};
36///
37/// let config = WasmtimeEngineConfig::default();
38/// let engine = WasmtimeEngine::new(config).expect("engine creation");
39/// ```
40pub struct WasmtimeEngine {
41    /// The underlying Wasmtime engine.
42    engine: Engine,
43
44    /// The configuration used to create this engine.
45    config: WasmtimeEngineConfig,
46}
47
48impl WasmtimeEngine {
49    /// Create a new `WasmtimeEngine` with the given configuration.
50    ///
51    /// # COLD PATH — called once at host startup.
52    ///
53    /// # Errors
54    /// Returns [`EngineError::Internal`] if the Wasmtime `Config` is invalid.
55    pub fn new(config: WasmtimeEngineConfig) -> Result<Self, EngineError> {
56        let problems = config.validate();
57        if !problems.is_empty() {
58            return Err(EngineError::Internal {
59                reason: format!("Invalid engine configuration: {}", problems.join("; ")),
60            });
61        }
62
63        let mut wasmtime_config = Config::new();
64
65        // LLI DEVIATION: async_support(true) is deprecated (no-op) in Wasmtime 42.
66        // Async is always available; no config needed.
67        // wasmtime_config.async_support(true); // removed per spike finding 3.6
68
69        // Fuel for CPU budgeting and cooperative preemption.
70        if config.fuel_enabled {
71            wasmtime_config.consume_fuel(true);
72        }
73
74        // SIMD support.
75        wasmtime_config.wasm_simd(config.simd_enabled);
76
77        // Multi-memory support.
78        wasmtime_config.wasm_multi_memory(config.multi_memory);
79
80        // Component Model support (required).
81        wasmtime_config.wasm_component_model(true);
82
83        // Stack size.
84        wasmtime_config.max_wasm_stack(config.stack_size);
85
86        // Parallel compilation.
87        if let Some(threads) = config.compilation_threads {
88            wasmtime_config.parallel_compilation(threads > 1);
89        }
90
91        // Compilation strategy.
92        match config.strategy {
93            crate::config::CompilationStrategy::Cranelift => {
94                wasmtime_config.strategy(wasmtime::Strategy::Cranelift);
95            }
96            crate::config::CompilationStrategy::Winch => {
97                // LLI DEVIATION: Winch may not be stable for Component Model.
98                // Fall back to Cranelift until verified.
99                wasmtime_config.strategy(wasmtime::Strategy::Cranelift);
100            }
101        }
102
103        let engine = Engine::new(&wasmtime_config).map_err(|e| EngineError::Internal {
104            reason: format!("Failed to create Wasmtime engine: {e}"),
105        })?;
106
107        Ok(Self { engine, config })
108    }
109
110    /// Returns a reference to the underlying Wasmtime engine.
111    ///
112    /// Useful for downstream crates that need to create linkers
113    /// or other engine-dependent objects.
114    #[inline]
115    pub fn inner(&self) -> &Engine {
116        &self.engine
117    }
118
119    /// Returns a reference to the engine configuration.
120    #[inline]
121    pub fn config(&self) -> &WasmtimeEngineConfig {
122        &self.config
123    }
124
125    /// Create a new `Store` configured for a specific component instance.
126    ///
127    /// # COLD PATH — called once per component instantiation.
128    fn create_store(&self, component_id: ComponentId) -> Store<HostState> {
129        let limits = StoreLimitsBuilder::new()
130            .memory_size(self.config.max_memory_bytes)
131            .table_elements(self.config.max_table_elements as usize)
132            .instances(self.config.max_instances as usize)
133            .trap_on_grow_failure(true) // Per spike finding 2.5
134            .build();
135
136        let host_state = HostState {
137            component_id,
138            limits,
139            fuel_budget: self.config.default_fuel,
140        };
141
142        let mut store = Store::new(&self.engine, host_state);
143
144        // Apply resource limiter.
145        store.limiter(|state| &mut state.limits);
146
147        // Set initial fuel if enabled.
148        if self.config.fuel_enabled {
149            store
150                .set_fuel(self.config.default_fuel)
151                .expect("fuel should be configurable when consume_fuel is enabled");
152
153            // Configure async yield interval for cooperative preemption.
154            if self.config.fuel_yield_interval > 0 {
155                store
156                    .fuel_async_yield_interval(Some(self.config.fuel_yield_interval))
157                    .expect("fuel yield interval should be configurable");
158            }
159        }
160
161        store
162    }
163
164    /// Create a new `Linker` for the engine.
165    ///
166    /// # COLD PATH — called during pipeline linking.
167    /// Used by downstream crates (torvyn-linker) and tests.
168    #[allow(dead_code)]
169    pub(crate) fn create_linker(&self) -> Linker<HostState> {
170        Linker::new(&self.engine)
171    }
172
173    /// Wrap a `Linker` into `ImportBindings`.
174    ///
175    /// # COLD PATH.
176    /// Used by downstream crates (torvyn-linker) and tests.
177    #[allow(dead_code)]
178    pub(crate) fn import_bindings_from_linker(linker: Linker<HostState>) -> ImportBindings {
179        ImportBindings {
180            inner: ImportBindingsInner::Wasmtime(linker),
181        }
182    }
183}
184
185#[async_trait]
186impl WasmEngine for WasmtimeEngine {
187    fn compile_component(&self, bytes: &[u8]) -> Result<CompiledComponent, EngineError> {
188        let component =
189            Component::new(&self.engine, bytes).map_err(|e| EngineError::CompilationFailed {
190                reason: e.to_string(),
191                source_hint: None,
192            })?;
193
194        Ok(CompiledComponent {
195            inner: CompiledComponentInner::Wasmtime(component),
196        })
197    }
198
199    fn serialize_component(&self, compiled: &CompiledComponent) -> Result<Vec<u8>, EngineError> {
200        match &compiled.inner {
201            CompiledComponentInner::Wasmtime(component) => {
202                component.serialize().map_err(|e| EngineError::Internal {
203                    reason: format!("Serialization failed: {e}"),
204                })
205            }
206            _ => Err(EngineError::Internal {
207                reason: "Cannot serialize non-Wasmtime component".into(),
208            }),
209        }
210    }
211
212    unsafe fn deserialize_component(
213        &self,
214        bytes: &[u8],
215    ) -> Result<Option<CompiledComponent>, EngineError> {
216        // SAFETY: Caller guarantees bytes are from serialize_component
217        // with matching engine config. Wasmtime validates the format
218        // header before loading native code.
219        match unsafe { Component::deserialize(&self.engine, bytes) } {
220            Ok(component) => Ok(Some(CompiledComponent {
221                inner: CompiledComponentInner::Wasmtime(component),
222            })),
223            Err(e) => {
224                let msg = e.to_string();
225                if msg.contains("incompatible") || msg.contains("version") {
226                    Ok(None)
227                } else {
228                    Err(EngineError::DeserializationFailed { reason: msg })
229                }
230            }
231        }
232    }
233
234    async fn instantiate(
235        &self,
236        compiled: &CompiledComponent,
237        imports: ImportBindings,
238        component_id: ComponentId,
239    ) -> Result<ComponentInstance, EngineError> {
240        let component = match &compiled.inner {
241            CompiledComponentInner::Wasmtime(c) => c,
242            _ => {
243                return Err(EngineError::Internal {
244                    reason: "Cannot instantiate non-Wasmtime component".into(),
245                });
246            }
247        };
248
249        let linker = match imports.inner {
250            ImportBindingsInner::Wasmtime(l) => l,
251            _ => {
252                return Err(EngineError::Internal {
253                    reason: "Cannot use non-Wasmtime import bindings".into(),
254                });
255            }
256        };
257
258        let mut store = self.create_store(component_id);
259
260        // Instantiate the component asynchronously.
261        let instance = linker
262            .instantiate_async(&mut store, component)
263            .await
264            .map_err(|e| EngineError::InstantiationFailed {
265                component_id,
266                reason: e.to_string(),
267            })?;
268
269        // Pre-resolve exported function handles for hot-path invocation.
270        let func_process = instance.get_func(&mut store, "process");
271        let func_pull = instance.get_func(&mut store, "pull");
272        let func_push = instance.get_func(&mut store, "push");
273        let func_init = instance.get_func(&mut store, "init");
274        let func_teardown = instance.get_func(&mut store, "teardown");
275
276        let has_processor = func_process.is_some();
277        let has_source = func_pull.is_some();
278        let has_sink = func_push.is_some();
279        let has_lifecycle = func_init.is_some();
280
281        let state = WasmtimeInstanceState {
282            store,
283            instance,
284            func_process,
285            func_pull,
286            func_push,
287            func_init,
288            func_teardown,
289        };
290
291        Ok(ComponentInstance {
292            component_id,
293            inner: ComponentInstanceInner::Wasmtime(state),
294            has_lifecycle,
295            has_processor,
296            has_source,
297            has_sink,
298        })
299    }
300
301    /// # WARM PATH — called before each invocation.
302    fn set_fuel(&self, instance: &mut ComponentInstance, fuel: u64) -> Result<(), EngineError> {
303        match &mut instance.inner {
304            ComponentInstanceInner::Wasmtime(state) => {
305                state
306                    .store
307                    .set_fuel(fuel)
308                    .map_err(|e| EngineError::Internal {
309                        reason: format!("Failed to set fuel: {e}"),
310                    })
311            }
312            _ => Err(EngineError::Internal {
313                reason: "set_fuel called on non-Wasmtime instance".into(),
314            }),
315        }
316    }
317
318    /// # HOT PATH — called after each invocation.
319    fn fuel_remaining(&self, instance: &ComponentInstance) -> Option<u64> {
320        match &instance.inner {
321            ComponentInstanceInner::Wasmtime(state) => state.store.get_fuel().ok(),
322            _ => None,
323        }
324    }
325
326    /// # WARM PATH
327    fn memory_usage(&self, instance: &ComponentInstance) -> usize {
328        match &instance.inner {
329            ComponentInstanceInner::Wasmtime(_state) => {
330                // LLI DEVIATION: There is no single API to get total memory
331                // usage of a component instance. Component instances may
332                // contain multiple core module instances. For Phase 0, return
333                // 0 and rely on StoreLimits for enforcement.
334                0
335            }
336            _ => 0,
337        }
338    }
339}
340
341// ---------------------------------------------------------------------------
342// Tests
343// ---------------------------------------------------------------------------
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn test_wasmtime_engine_creation() {
351        let config = WasmtimeEngineConfig::default();
352        let engine = WasmtimeEngine::new(config);
353        assert!(engine.is_ok());
354    }
355
356    #[test]
357    fn test_wasmtime_engine_invalid_config() {
358        let config = WasmtimeEngineConfig {
359            fuel_enabled: true,
360            default_fuel: 0, // Invalid
361            ..WasmtimeEngineConfig::default()
362        };
363        let engine = WasmtimeEngine::new(config);
364        assert!(engine.is_err());
365    }
366
367    #[test]
368    fn test_compile_invalid_bytes() {
369        let config = WasmtimeEngineConfig::default();
370        let engine = WasmtimeEngine::new(config).unwrap();
371        let result = engine.compile_component(b"not a wasm component");
372        assert!(result.is_err());
373        match result.unwrap_err() {
374            EngineError::CompilationFailed { .. } => {}
375            other => panic!("expected CompilationFailed, got: {other}"),
376        }
377    }
378
379    #[test]
380    fn test_compile_empty_bytes() {
381        let config = WasmtimeEngineConfig::default();
382        let engine = WasmtimeEngine::new(config).unwrap();
383        let result = engine.compile_component(b"");
384        assert!(result.is_err());
385    }
386
387    #[test]
388    fn test_compile_minimal_component_from_wat() {
389        let config = WasmtimeEngineConfig::default();
390        let engine = WasmtimeEngine::new(config).unwrap();
391
392        // A minimal valid component using WAT text format.
393        // Wasmtime can compile WAT directly via Component::new.
394        let wat = "(component)";
395        // compile_component takes bytes, WAT may not work through that path.
396        // Use the engine directly for WAT.
397        let _result = engine.compile_component(wat.as_bytes());
398        let component = Component::new(engine.inner(), wat);
399        assert!(component.is_ok(), "engine should compile minimal WAT");
400    }
401
402    #[test]
403    fn test_engine_config_accessors() {
404        let config = WasmtimeEngineConfig::default();
405        let engine = WasmtimeEngine::new(config).unwrap();
406        assert!(engine.config().fuel_enabled);
407    }
408
409    #[test]
410    fn test_serialize_deserialize_roundtrip() {
411        let config = WasmtimeEngineConfig::default();
412        let engine = WasmtimeEngine::new(config).unwrap();
413
414        // Compile a minimal component.
415        let component = Component::new(engine.inner(), "(component)").expect("compile WAT");
416        let compiled = CompiledComponent {
417            inner: CompiledComponentInner::Wasmtime(component),
418        };
419
420        // Serialize.
421        let bytes = engine
422            .serialize_component(&compiled)
423            .expect("serialize should work");
424        assert!(!bytes.is_empty());
425
426        // Deserialize.
427        // SAFETY: bytes were just produced by serialize_component with same engine.
428        let deserialized =
429            unsafe { engine.deserialize_component(&bytes) }.expect("deserialize should work");
430        assert!(deserialized.is_some());
431    }
432
433    #[tokio::test]
434    async fn test_instantiate_minimal_component() {
435        let config = WasmtimeEngineConfig::default();
436        let engine = WasmtimeEngine::new(config).unwrap();
437
438        let component = Component::new(engine.inner(), "(component)").expect("compile WAT");
439        let compiled = CompiledComponent {
440            inner: CompiledComponentInner::Wasmtime(component),
441        };
442
443        let linker = engine.create_linker();
444        let imports = WasmtimeEngine::import_bindings_from_linker(linker);
445        let component_id = ComponentId::new(1);
446
447        let instance = engine.instantiate(&compiled, imports, component_id).await;
448        assert!(instance.is_ok());
449
450        let inst = instance.unwrap();
451        assert_eq!(inst.component_id(), component_id);
452        // Minimal component has no exports.
453        assert!(!inst.has_processor());
454        assert!(!inst.has_source());
455        assert!(!inst.has_sink());
456        assert!(!inst.has_lifecycle());
457    }
458
459    #[tokio::test]
460    async fn test_fuel_set_and_read() {
461        let config = WasmtimeEngineConfig::default();
462        let engine = WasmtimeEngine::new(config).unwrap();
463
464        let component = Component::new(engine.inner(), "(component)").expect("compile WAT");
465        let compiled = CompiledComponent {
466            inner: CompiledComponentInner::Wasmtime(component),
467        };
468
469        let linker = engine.create_linker();
470        let imports = WasmtimeEngine::import_bindings_from_linker(linker);
471        let mut instance = engine
472            .instantiate(&compiled, imports, ComponentId::new(1))
473            .await
474            .unwrap();
475
476        // Default fuel should be set.
477        let remaining = engine.fuel_remaining(&instance);
478        assert!(remaining.is_some());
479
480        // Set new fuel.
481        engine.set_fuel(&mut instance, 500).unwrap();
482        assert_eq!(engine.fuel_remaining(&instance), Some(500));
483    }
484
485    #[test]
486    fn test_memory_usage_returns_zero_for_now() {
487        let config = WasmtimeEngineConfig::default();
488        let engine = WasmtimeEngine::new(config).unwrap();
489
490        // Without an instance, we can't test this directly.
491        // This is tested via the instantiate path above.
492        let _ = engine;
493    }
494}