Skip to main content

submilli_wasm/
engine.rs

1//! `Engine` — shared, thread-safe compilation/runtime root; holds the epoch counter.
2
3use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
4use std::sync::{Arc, Mutex, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard, Weak};
5
6use crate::canon::{AggKind, CanonicalTypeId, GroupId, ModuleType, TypeRegistry};
7use crate::config::{CollectorKind, Config};
8use crate::module::ModuleLimits;
9use crate::value::{FieldType, Finality, ValType};
10use crate::{Error, Result};
11
12/// A compiled-code and runtime environment, shared across `Store`s and threads.
13///
14/// Cheap to clone (reference-counted handle), `Send + Sync` — mirrors `wasmtime::Engine`.
15#[derive(Clone, Debug)]
16pub struct Engine {
17    inner: Arc<EngineInner>,
18}
19
20/// Default wasm stack budget (bytes) when `Config::max_wasm_stack` is unset,
21/// matching wasmtime's 512 KiB default.
22const DEFAULT_MAX_WASM_STACK: usize = 512 * 1024;
23
24#[allow(clippy::struct_excessive_bools)] // independent engine flags derived from `Config`
25#[derive(Debug)]
26struct EngineInner {
27    epoch: AtomicU64,
28    max_wasm_stack: usize,
29    consume_fuel: bool,
30    epoch_interruption: bool,
31    is_async: bool,
32    /// The resolved collector strategy (mark-sweep or allocate-only null), decided at
33    /// `Engine::new` from `Config::collector`.
34    collector: CollectorKind,
35    /// Engine-wide GC-pressure threshold (bytes); also sizes each store's heap ceiling.
36    gc_memory_threshold: Option<usize>,
37    /// Per-store pre-authorized GC budget (bytes): reservation growth within it skips the limiter,
38    /// and it caps a single growth step (`Config::gc_heap_reservation`).
39    gc_heap_reservation: usize,
40    /// Default validation-time complexity ceilings for `Module::new` — the untrusted tier
41    /// (`Config::max_module_bytes` / `max_expanded_locals`, #32). Trusted modules override them
42    /// per-compile via `Module::new_with_limits`.
43    module_limits: ModuleLimits,
44    /// Total GC bytes committed (reserved) across all of the engine's stores — updated at
45    /// reservation-batch granularity (never per object), so it has no hot-path cost. Drives the
46    /// engine-wide GC-pressure axis (§14).
47    gc_committed: AtomicUsize,
48    /// Per-store GC-request **mailboxes**: each live store registers a flag here (held as a `Weak`,
49    /// the store owns the `Arc`). When `gc_committed` crosses `gc_memory_threshold`, the engine
50    /// posts to every mailbox; each store reads (and clears) *its own* at a back-edge safe point and
51    /// self-collects. A `Store` is `!Sync`, so this is the only way the engine can *request* (never
52    /// force) a collection on another thread — and one store servicing its request leaves the others'
53    /// untouched. Dead mailboxes are pruned on post.
54    gc_requests: Mutex<Vec<Weak<AtomicBool>>>,
55    /// Whether to capture backtraces (`Config::wasm_backtrace`); also gates the per-`Op` offset
56    /// table + `name`-section retention at parse (#29c).
57    wasm_backtrace: bool,
58    /// Whether to retain the module's `.debug_*` DWARF for source-level backtraces (#29c).
59    retain_dwarf: bool,
60    /// Engine-wide canonical type registry (cross-module GC type identity). Locked only by
61    /// `Module::new`/`Drop` to register/release rec groups — runtime compares the baked
62    /// canonical ids without touching it.
63    types: RwLock<TypeRegistry>,
64}
65
66impl Engine {
67    /// Creates a new `Engine` from the given configuration.
68    pub fn new(config: &Config) -> Result<Engine> {
69        Ok(Engine {
70            inner: Arc::new(EngineInner {
71                epoch: AtomicU64::new(0),
72                max_wasm_stack: config
73                    .max_wasm_stack_bytes()
74                    .unwrap_or(DEFAULT_MAX_WASM_STACK),
75                consume_fuel: config.consume_fuel_enabled(),
76                epoch_interruption: config.epoch_interruption_enabled(),
77                is_async: config.async_support_enabled(),
78                collector: config.collector_kind().resolve()?,
79                gc_memory_threshold: config.gc_memory_threshold_bytes(),
80                gc_heap_reservation: config.gc_heap_reservation_bytes(),
81                module_limits: config.module_limits(),
82                gc_committed: AtomicUsize::new(0),
83                gc_requests: Mutex::new(Vec::new()),
84                wasm_backtrace: config.wasm_backtrace_enabled(),
85                retain_dwarf: config.debug_info_enabled()
86                    || (config.wasm_backtrace_enabled() && config.wasm_backtrace_details_enabled()),
87                types: RwLock::new(TypeRegistry::default()),
88            }),
89        })
90    }
91
92    /// Reads the engine type registry, **recovering from lock poisoning** (#33): a panic while
93    /// some other store held this lock must not cascade a panic into every other tenant on the
94    /// engine. Registry operations are short and leave it structurally valid, so the recovered
95    /// guard is safe to use.
96    fn types_read(&self) -> RwLockReadGuard<'_, TypeRegistry> {
97        self.inner
98            .types
99            .read()
100            .unwrap_or_else(PoisonError::into_inner)
101    }
102
103    /// Writes the engine type registry, recovering from lock poisoning — see [`Self::types_read`].
104    fn types_write(&self) -> RwLockWriteGuard<'_, TypeRegistry> {
105        self.inner
106            .types
107            .write()
108            .unwrap_or_else(PoisonError::into_inner)
109    }
110
111    /// Interns a module's rec groups into the engine registry, returning the per-module-type
112    /// canonical ids and the registered group ids (held by the `Module` for release on drop).
113    pub(crate) fn intern_types(
114        &self,
115        types: &[ModuleType],
116    ) -> (Vec<CanonicalTypeId>, Vec<GroupId>) {
117        self.types_write().intern_module(types)
118    }
119
120    /// Releases a module's registered group ids (decrement refcounts; reclaim at zero).
121    pub(crate) fn release_types(&self, group_ids: &[GroupId]) {
122        self.types_write().release(group_ids);
123    }
124
125    /// Adds a registration to the group owning `id` (a type handle was cloned / materialized).
126    pub(crate) fn incref_type(&self, id: CanonicalTypeId) {
127        self.types_write().incref_type(id);
128    }
129
130    /// Removes a registration from the group owning `id` (a type handle was dropped).
131    pub(crate) fn decref_type(&self, id: CanonicalTypeId) {
132        self.types_write().decref_type(id);
133    }
134
135    /// Removes one registration from group `g` (a `RecGroup` / `Module` handle was dropped).
136    pub(crate) fn release_group(&self, g: GroupId) {
137        self.release_types(&[g]);
138    }
139
140    /// The number of live (registered) rec groups — for leak/reclamation tests.
141    pub(crate) fn live_group_count(&self) -> usize {
142        self.types_read().live_group_count()
143    }
144
145    /// Whether canonical type `sub` is a declared subtype of `sup`.
146    pub(crate) fn is_subtype(&self, sub: CanonicalTypeId, sup: CanonicalTypeId) -> bool {
147        self.types_read().is_subtype(sub, sup)
148    }
149
150    /// Whether `other` is the same engine instance (handle identity).
151    pub(crate) fn same(&self, other: &Engine) -> bool {
152        Arc::ptr_eq(&self.inner, &other.inner)
153    }
154
155    /// The declared finality of a canonical type, or `None` if it is not live.
156    pub(crate) fn type_finality(&self, id: CanonicalTypeId) -> Option<Finality> {
157        self.types_read().finality(id)
158    }
159
160    /// The kind (func/struct/array) of a canonical type id, if registered.
161    pub(crate) fn type_kind(&self, id: CanonicalTypeId) -> Option<AggKind> {
162        self.types_read().kind(id)
163    }
164
165    /// Interns a host-built func type and returns its canonical id. (The group is held by the
166    /// engine for its lifetime — host-type reclamation is a later collector concern.)
167    pub(crate) fn intern_func_type(
168        &self,
169        params: &[ValType],
170        results: &[ValType],
171    ) -> CanonicalTypeId {
172        self.types_write().intern_func(params, results).0
173    }
174
175    pub(crate) fn intern_struct_type(
176        &self,
177        finality: Finality,
178        supertype: Option<CanonicalTypeId>,
179        fields: &[FieldType],
180    ) -> CanonicalTypeId {
181        self.types_write()
182            .intern_struct(finality, supertype, fields)
183            .0
184    }
185
186    pub(crate) fn intern_array_type(
187        &self,
188        finality: Finality,
189        supertype: Option<CanonicalTypeId>,
190        field: &FieldType,
191    ) -> CanonicalTypeId {
192        self.types_write()
193            .intern_array(finality, supertype, field)
194            .0
195    }
196
197    /// Interns a host-built rec group (`RecGroupBuilder`), returning the members' canonical ids
198    /// and the group id (with one registration, adopted by the resulting `RecGroup`).
199    pub(crate) fn intern_host_group(
200        &self,
201        members: &[ModuleType],
202        externals: &[CanonicalTypeId],
203    ) -> (Vec<CanonicalTypeId>, GroupId) {
204        self.types_write().intern_host_group(members, externals)
205    }
206
207    /// The engine's canonical type registry lock (used by the two-phase materializers in `canon`,
208    /// which acquire it briefly to clone canonical data, then build handles after releasing it).
209    pub(crate) fn types(&self) -> &RwLock<TypeRegistry> {
210        &self.inner.types
211    }
212
213    /// The materialized (params, results) of a canonical func type; an error if `id` is not a
214    /// live func type.
215    pub(crate) fn func_sig(&self, id: CanonicalTypeId) -> Result<(Vec<ValType>, Vec<ValType>)> {
216        crate::canon::func_sig(self, id).ok_or_else(|| Error::msg("not a live func type"))
217    }
218
219    /// The materialized fields of a canonical struct type; an error if `id` is not a live struct
220    /// type.
221    pub(crate) fn struct_fields(&self, id: CanonicalTypeId) -> Result<Vec<FieldType>> {
222        crate::canon::struct_fields(self, id).ok_or_else(|| Error::msg("not a live struct type"))
223    }
224
225    /// The materialized element of a canonical array type; an error if `id` is not a live array
226    /// type.
227    pub(crate) fn array_field(&self, id: CanonicalTypeId) -> Result<FieldType> {
228        crate::canon::array_field(self, id).ok_or_else(|| Error::msg("not a live array type"))
229    }
230
231    /// Whether epoch-based interruption is enabled (`Config::epoch_interruption`).
232    pub(crate) fn epoch_interruption(&self) -> bool {
233        self.inner.epoch_interruption
234    }
235
236    /// The wasm execution-stack byte budget (`Config::max_wasm_stack`).
237    pub(crate) fn max_wasm_stack(&self) -> usize {
238        self.inner.max_wasm_stack
239    }
240
241    /// Whether fuel metering is enabled (`Config::consume_fuel`).
242    pub(crate) fn consume_fuel(&self) -> bool {
243        self.inner.consume_fuel
244    }
245
246    /// Whether async execution is enabled (`Config::async_support`).
247    pub(crate) fn is_async(&self) -> bool {
248        self.inner.is_async
249    }
250
251    /// Whether backtraces are captured (`Config::wasm_backtrace`); also whether the per-`Op` offset
252    /// table + `name` section are retained at parse.
253    pub(crate) fn wasm_backtrace_enabled(&self) -> bool {
254        self.inner.wasm_backtrace
255    }
256
257    /// Whether the module's `.debug_*` DWARF is retained for source-level backtraces.
258    pub(crate) fn retain_dwarf(&self) -> bool {
259        self.inner.retain_dwarf
260    }
261
262    /// The resolved collector strategy (mark-sweep or allocate-only null).
263    pub(crate) fn collector(&self) -> CollectorKind {
264        self.inner.collector
265    }
266
267    /// Whether the engine runs a tracing collector (i.e. not the allocate-only null collector).
268    pub(crate) fn is_collecting(&self) -> bool {
269        self.inner.collector == CollectorKind::MarkSweep
270    }
271
272    /// The engine-wide GC-pressure threshold in bytes, if configured. Sizes each store's GC-heap
273    /// ceiling and is the high-water mark for the engine-wide GC-pressure axis.
274    pub(crate) fn gc_memory_threshold(&self) -> Option<usize> {
275        self.inner.gc_memory_threshold
276    }
277
278    /// The per-store pre-authorized GC reservation in bytes (`Config::gc_heap_reservation`).
279    pub(crate) fn gc_heap_reservation(&self) -> usize {
280        self.inner.gc_heap_reservation
281    }
282
283    /// The default validation-time ceilings for `Module::new` (the untrusted tier).
284    pub(crate) fn module_limits(&self) -> ModuleLimits {
285        self.inner.module_limits
286    }
287
288    /// Registers a new store's GC-request mailbox, returning the flag the store owns (the engine
289    /// keeps a `Weak`). Posted to under engine-wide GC pressure; read-and-cleared by the store.
290    pub(crate) fn register_gc_request(&self) -> Arc<AtomicBool> {
291        let flag = Arc::new(AtomicBool::new(false));
292        if let Ok(mut mailboxes) = self.inner.gc_requests.lock() {
293            mailboxes.push(Arc::downgrade(&flag));
294        }
295        flag
296    }
297
298    /// Records a reservation growth of `delta` GC bytes against the engine-wide committed total. If
299    /// it crosses `gc_memory_threshold` (when configured), posts a collection request to every live
300    /// store's mailbox (pruning dead ones). Batch-granular, so this is off the hot path.
301    pub(crate) fn add_gc_committed(&self, delta: usize) {
302        let total = self.inner.gc_committed.fetch_add(delta, Ordering::Relaxed) + delta;
303        if self.inner.gc_memory_threshold.is_none_or(|t| total <= t) {
304            return;
305        }
306        if let Ok(mut mailboxes) = self.inner.gc_requests.lock() {
307            mailboxes.retain(|w| match w.upgrade() {
308                Some(flag) => {
309                    flag.store(true, Ordering::Relaxed);
310                    true
311                }
312                None => false, // store dropped — prune its mailbox
313            });
314        }
315    }
316
317    /// Releases `delta` GC bytes from the engine-wide committed total (a store dropped its heap).
318    pub(crate) fn sub_gc_committed(&self, delta: usize) {
319        self.inner.gc_committed.fetch_sub(delta, Ordering::Relaxed);
320    }
321
322    /// Total GC bytes committed (reserved) across the engine's stores (for tests / introspection).
323    pub(crate) fn gc_committed_bytes(&self) -> usize {
324        self.inner.gc_committed.load(Ordering::Relaxed)
325    }
326
327    /// Compiles `bytes` and returns the serialized compiled artifact (as
328    /// [`Module::serialize`]), suitable for `unsafe Module::deserialize`. Validates +
329    /// compiles once here so deserialize can skip both.
330    pub fn precompile_module(&self, bytes: &[u8]) -> Result<Vec<u8>> {
331        crate::module::Module::from_binary(self, bytes)?.serialize()
332    }
333
334    /// Bumps the epoch counter; pairs with `Store::set_epoch_deadline`.
335    pub fn increment_epoch(&self) {
336        self.inner.epoch.fetch_add(1, Ordering::Relaxed);
337    }
338
339    /// Returns a non-owning handle to this engine (e.g. for an epoch ticker thread).
340    pub fn weak(&self) -> EngineWeak {
341        EngineWeak {
342            inner: Arc::downgrade(&self.inner),
343        }
344    }
345
346    pub(crate) fn current_epoch(&self) -> u64 {
347        self.inner.epoch.load(Ordering::Relaxed)
348    }
349}
350
351impl Default for Engine {
352    fn default() -> Engine {
353        Engine::new(&Config::default()).expect("default config is always valid")
354    }
355}
356
357/// A non-owning handle to an [`Engine`], obtained via [`Engine::weak`].
358#[derive(Clone, Debug, Default)]
359pub struct EngineWeak {
360    inner: Weak<EngineInner>,
361}
362
363impl EngineWeak {
364    /// Upgrades to a strong [`Engine`] handle if the engine is still alive.
365    pub fn upgrade(&self) -> Option<Engine> {
366        self.inner.upgrade().map(|inner| Engine { inner })
367    }
368}
369
370#[cfg(test)]
371#[path = "engine_tests.rs"]
372mod tests;