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