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;