kobako 0.9.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +32 -0
  4. data/Cargo.lock +3 -1
  5. data/README.md +47 -19
  6. data/data/kobako.wasm +0 -0
  7. data/ext/kobako/Cargo.toml +12 -2
  8. data/ext/kobako/src/runtime/ambient.rs +1 -1
  9. data/ext/kobako/src/runtime/cache.rs +170 -6
  10. data/ext/kobako/src/runtime/capture.rs +1 -1
  11. data/ext/kobako/src/runtime/config.rs +3 -4
  12. data/ext/kobako/src/runtime/dispatch.rs +8 -8
  13. data/ext/kobako/src/runtime/exports.rs +32 -21
  14. data/ext/kobako/src/runtime/instance_pre.rs +97 -0
  15. data/ext/kobako/src/runtime/invocation.rs +36 -93
  16. data/ext/kobako/src/runtime/trap.rs +5 -5
  17. data/ext/kobako/src/runtime.rs +389 -403
  18. data/ext/kobako/src/snapshot.rs +2 -2
  19. data/lib/kobako/capture.rb +5 -7
  20. data/lib/kobako/catalog/handles.rb +28 -39
  21. data/lib/kobako/catalog/namespaces.rb +31 -20
  22. data/lib/kobako/catalog/snippets.rb +18 -16
  23. data/lib/kobako/codec/decoder.rb +5 -1
  24. data/lib/kobako/codec/utils.rb +6 -9
  25. data/lib/kobako/errors.rb +40 -36
  26. data/lib/kobako/handle.rb +2 -3
  27. data/lib/kobako/namespace.rb +17 -6
  28. data/lib/kobako/outcome.rb +12 -14
  29. data/lib/kobako/pool.rb +176 -0
  30. data/lib/kobako/sandbox.rb +68 -88
  31. data/lib/kobako/sandbox_options.rb +5 -9
  32. data/lib/kobako/snapshot.rb +2 -4
  33. data/lib/kobako/snippet/binary.rb +1 -3
  34. data/lib/kobako/snippet/source.rb +1 -2
  35. data/lib/kobako/snippet.rb +1 -2
  36. data/lib/kobako/transport/dispatcher.rb +39 -38
  37. data/lib/kobako/transport/request.rb +1 -1
  38. data/lib/kobako/transport/run.rb +23 -28
  39. data/lib/kobako/transport/yielder.rb +11 -17
  40. data/lib/kobako/transport.rb +2 -3
  41. data/lib/kobako/usage.rb +10 -13
  42. data/lib/kobako/version.rb +1 -1
  43. data/lib/kobako.rb +1 -0
  44. data/release-please-config.json +16 -1
  45. data/sig/kobako/catalog/handles.rbs +0 -2
  46. data/sig/kobako/errors.rbs +3 -0
  47. data/sig/kobako/namespace.rbs +2 -0
  48. data/sig/kobako/pool.rbs +44 -0
  49. data/sig/kobako/sandbox.rbs +2 -2
  50. data/sig/kobako/transport/dispatcher.rbs +2 -0
  51. metadata +4 -1
@@ -1,41 +1,48 @@
1
- //! Cached wasmtime export handles for the host-driven ABI surface.
1
+ //! Per-invocation wasmtime export handles for the host-driven ABI
2
+ //! surface.
2
3
  //!
3
- //! `Runtime::from_path` resolves the three docs/wire-codec.md ABI exports
4
- //! the run path drives (`__kobako_eval` / `__kobako_run` /
5
- //! `__kobako_take_outcome`) once at construction and stores their typed
6
- //! handles here, so each `#eval` / `#run` calls a cached handle rather than
7
- //! re-resolving the export by name. Distinct from `super::cache` (the
8
- //! process-wide Engine / Module cache): this caches *which guest function
9
- //! to call*, per `Runtime`.
4
+ //! `Runtime::instantiate` resolves the ABI exports the run path drives
5
+ //! (`__kobako_eval` / `__kobako_run` / `__kobako_take_outcome` /
6
+ //! `__kobako_alloc`) plus the `memory` export against each fresh
7
+ //! per-invocation instance and bundles their
8
+ //! typed handles here, so the invocation body passes one struct around
9
+ //! rather than re-resolving exports by name at every step. Distinct
10
+ //! from `super::cache` (the process-wide Engine / Module cache): this
11
+ //! carries *which guest function to call*, per invocation.
10
12
  //!
11
- //! `__kobako_alloc` is deliberately absentonly `super::dispatch` calls
12
- //! it, and it does so through `Caller::get_export` on the wasmtime side.
13
+ //! `super::dispatch` does not reach this struct a host import runs
14
+ //! against a `Caller`, so the dispatch path resolves `__kobako_alloc`
15
+ //! and `memory` through `Caller::get_export` instead.
13
16
 
14
- use wasmtime::{AsContextMut, Instance as WtInstance, TypedFunc};
17
+ use wasmtime::{AsContextMut, Instance as WtInstance, Memory, TypedFunc};
15
18
 
16
- use super::invocation::StoreCell;
17
-
18
- /// The cached host-driven export handles. Each is `Option` because test
19
+ /// The resolved host-driven export handles. Each is `Option` because test
19
20
  /// fixtures (a minimal "ping" module) need not provide them; real
20
21
  /// `kobako.wasm` always does, and the run-path methods raise a Ruby
21
- /// `Kobako::TrapError` (via `require_export`) when a handle is `None`.
22
+ /// `Kobako::TrapError` (via `require_export` / `require_memory`) when a
23
+ /// handle is `None`.
24
+ ///
25
+ /// The handles are indices into the owning Store, not borrows of the
26
+ /// `Instance` — they stay valid for the Store's lifetime, which is why
27
+ /// no `Instance` field is kept.
22
28
  pub(crate) struct Exports {
23
29
  pub(crate) eval: Option<TypedFunc<(), ()>>,
24
30
  pub(crate) run: Option<TypedFunc<(i32, i32), ()>>,
25
31
  pub(crate) take_outcome: Option<TypedFunc<(), u64>>,
32
+ pub(crate) alloc: Option<TypedFunc<u32, u32>>,
33
+ pub(crate) memory: Option<Memory>,
26
34
  }
27
35
 
28
36
  impl Exports {
29
- /// Best-effort lookup of the three host-driven exports against a
30
- /// freshly instantiated module. Missing exports are not an error here
37
+ /// Best-effort lookup of the host-driven exports against a freshly
38
+ /// instantiated module. Missing exports are not an error here
31
39
  /// (the test fixture is a bare module); the host enforces presence at
32
40
  /// invocation time. Only the SPEC ABI shapes are accepted —
33
41
  /// `__kobako_eval` is `() -> ()`, `__kobako_run` is
34
- /// `(env_ptr, env_len) -> ()`, `__kobako_take_outcome` is `() -> u64`
42
+ /// `(env_ptr, env_len) -> ()`, `__kobako_take_outcome` is `() -> u64`,
43
+ /// `__kobako_alloc` is `(len) -> ptr`
35
44
  /// (docs/wire-codec.md § ABI Signatures).
36
- pub(crate) fn resolve(instance: &WtInstance, store: &StoreCell) -> Self {
37
- let mut store_ref = store.borrow_mut();
38
- let mut ctx = store_ref.as_context_mut();
45
+ pub(crate) fn resolve(instance: &WtInstance, mut ctx: impl AsContextMut) -> Self {
39
46
  Self {
40
47
  eval: instance
41
48
  .get_typed_func::<(), ()>(&mut ctx, "__kobako_eval")
@@ -46,6 +53,10 @@ impl Exports {
46
53
  take_outcome: instance
47
54
  .get_typed_func::<(), u64>(&mut ctx, "__kobako_take_outcome")
48
55
  .ok(),
56
+ alloc: instance
57
+ .get_typed_func::<u32, u32>(&mut ctx, "__kobako_alloc")
58
+ .ok(),
59
+ memory: instance.get_memory(&mut ctx, "memory"),
49
60
  }
50
61
  }
51
62
  }
@@ -0,0 +1,97 @@
1
+ //! Per-path cache of pre-instantiated wasmtime artifacts.
2
+ //!
3
+ //! The `Linker` wiring (the WASI preview1 import set plus the
4
+ //! `__kobako_dispatch` host import) and its type-check against the
5
+ //! compiled Module are identical for every `Kobako::Runtime` on the
6
+ //! same Guest Binary — both host closures read all their state from
7
+ //! the `Invocation` inside the calling Store, never from the Runtime.
8
+ //! Caching the resolved `InstancePre` per path leaves only the
9
+ //! `instantiate` call itself on the `Runtime.from_path` hot path.
10
+ //!
11
+ //! Concurrency: see `super::cache` — under Ruby's GVL the Mutex serves
12
+ //! `Sync` bounds rather than real contention.
13
+
14
+ use std::collections::HashMap;
15
+ use std::path::{Path, PathBuf};
16
+ use std::sync::{Mutex, OnceLock};
17
+
18
+ use magnus::{Error as MagnusError, Ruby};
19
+ use wasmtime::{Caller, InstancePre, Linker};
20
+ use wasmtime_wasi::p1;
21
+
22
+ use super::cache::{cached_module, shared_engine};
23
+ use super::invocation::Invocation;
24
+ use super::{dispatch, setup_err, trap};
25
+
26
+ static INSTANCE_PRE_CACHE: OnceLock<Mutex<HashMap<PathBuf, InstancePre<Invocation>>>> =
27
+ OnceLock::new();
28
+
29
+ /// Look up `path` in the per-path `InstancePre` cache, wiring the
30
+ /// Linker and resolving the Module's imports on a miss. Compilation
31
+ /// faults surface through `cached_module`; import-resolution faults
32
+ /// raise `Kobako::SetupError`.
33
+ pub(crate) fn cached_instance_pre(path: &Path) -> Result<InstancePre<Invocation>, MagnusError> {
34
+ let cache = INSTANCE_PRE_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
35
+
36
+ if let Some(pre) = cache
37
+ .lock()
38
+ .expect("instance_pre cache mutex poisoned")
39
+ .get(path)
40
+ .cloned()
41
+ {
42
+ return Ok(pre);
43
+ }
44
+
45
+ let module = cached_module(path)?;
46
+ let linker = build_linker()?;
47
+ let ruby = Ruby::get().expect("Ruby thread");
48
+ let pre = linker
49
+ .instantiate_pre(&module)
50
+ .map_err(|e| trap::instantiate_err(&ruby, e))?;
51
+ cache
52
+ .lock()
53
+ .expect("instance_pre cache mutex poisoned")
54
+ .insert(path.to_path_buf(), pre.clone());
55
+ Ok(pre)
56
+ }
57
+
58
+ /// Build the host-import `Linker` every Guest Binary instantiates
59
+ /// against.
60
+ fn build_linker() -> Result<Linker<Invocation>, MagnusError> {
61
+ let ruby = Ruby::get().expect("Ruby thread");
62
+ let mut linker: Linker<Invocation> = Linker::new(shared_engine()?);
63
+
64
+ // Wire the wasmtime-wasi preview1 WASI imports. Routes guest fd 1/2
65
+ // to the MemoryOutputPipes set up before each run via
66
+ // `Runtime::eval`. The closure pulls a `&mut WasiP1Ctx` out of
67
+ // Invocation; the panic semantics live inside `Invocation::wasi_mut`
68
+ // so the wiring stays honest about its precondition.
69
+ p1::add_to_linker_sync(&mut linker, |state: &mut Invocation| state.wasi_mut())
70
+ .map_err(|e| setup_err(&ruby, format!("failed to set up the WASI runtime: {}", e)))?;
71
+
72
+ // `__kobako_dispatch` host import. Signature per docs/wire-codec.md
73
+ // § ABI Signatures:
74
+ // (req_ptr: i32, req_len: i32) -> i64
75
+ // Decodes the Request bytes, dispatches via the Ruby-side
76
+ // dispatch Proc (bound per-Sandbox through `Runtime#on_dispatch=`),
77
+ // allocates a guest buffer through `__kobako_alloc`, writes
78
+ // the Response bytes there, and returns the packed
79
+ // `(ptr<<32)|len`. The dispatcher returns 0 on any wire-layer
80
+ // fault (including no Proc bound); see `dispatch::handle`.
81
+ linker
82
+ .func_wrap(
83
+ "env",
84
+ "__kobako_dispatch",
85
+ |mut caller: Caller<'_, Invocation>, req_ptr: i32, req_len: i32| -> i64 {
86
+ dispatch::handle(&mut caller, req_ptr, req_len)
87
+ },
88
+ )
89
+ .map_err(|e| {
90
+ setup_err(
91
+ &ruby,
92
+ format!("failed to set up the host callback bridge: {}", e),
93
+ )
94
+ })?;
95
+
96
+ Ok(linker)
97
+ }
@@ -2,29 +2,27 @@
2
2
  //! [SPEC.md Single-Invocation Slot] (one `Invocation` per OS thread
3
3
  //! for the lifetime of one `Runtime::eval` / `Runtime::run` call).
4
4
  //!
5
- //! Owned by `StoreCell` (a `RefCell` shim wrapping `wasmtime::Store`)
6
- //! and threaded through every host import — the `__kobako_dispatch`
7
- //! dispatcher reads the bound dispatch Proc, while the run-path methods
8
- //! on `crate::runtime::Runtime` install fresh WASI context + pipes
9
- //! before every invocation (docs/behavior.md B-03 / B-04).
5
+ //! Owned as the data of each per-invocation `wasmtime::Store`
6
+ //! and threaded through every host import —
7
+ //! the `__kobako_dispatch` dispatcher reads the bound dispatch Proc,
8
+ //! while the run-path methods on `crate::runtime::Runtime` install the
9
+ //! invocation's WASI context + pipes at Store creation.
10
10
  //!
11
11
  //! The slot also carries the per-invocation wall-clock deadline
12
- //! (docs/behavior.md B-01, E-19) and the per-invocation linear-memory
13
- //! delta cap `MemoryLimiter` (docs/behavior.md B-01, E-20). Both are
12
+ //! and the per-invocation linear-memory
13
+ //! delta cap `MemoryLimiter`. Both are
14
14
  //! read from the wasmtime `epoch_deadline_callback` / `ResourceLimiter`
15
- //! callbacks installed in `crate::runtime::Runtime::from_path`. The
15
+ //! callbacks installed in `crate::runtime::Runtime::new_store`. The
16
16
  //! memory cap measures only the `memory.grow` delta past the linear-
17
- //! memory size captured at invocation entry — the mruby image's
18
- //! initial allocation and prior invocations' watermark are outside the
19
- //! budget.
17
+ //! memory size captured at invocation entry — the image's initial
18
+ //! allocation is outside the budget.
20
19
  //!
21
20
  //! [SPEC.md Single-Invocation Slot]: ../../../../SPEC.md
22
21
 
23
- use std::cell::{Ref, RefCell, RefMut};
24
22
  use std::time::{Duration, Instant};
25
23
 
26
24
  use magnus::{value::Opaque, Value};
27
- use wasmtime::{ResourceLimiter, Store as WtStore};
25
+ use wasmtime::ResourceLimiter;
28
26
  use wasmtime_wasi::p1::WasiP1Ctx;
29
27
  use wasmtime_wasi::p2::pipe::MemoryOutputPipe;
30
28
 
@@ -53,7 +51,7 @@ impl Invocation {
53
51
  /// Build a fresh per-Store host state. `memory_limit` carries the
54
52
  /// `Sandbox#memory_limit` cap in bytes (or `None` to disable the cap);
55
53
  /// it is read from the wasmtime `ResourceLimiter` callback every
56
- /// time the guest grows linear memory (docs/behavior.md B-01, E-20).
54
+ /// time the guest grows linear memory.
57
55
  pub(super) fn new(memory_limit: Option<usize>) -> Self {
58
56
  Self {
59
57
  wasi: None,
@@ -70,7 +68,7 @@ impl Invocation {
70
68
  /// Install a freshly-built WASI context plus the matching stdout/stderr
71
69
  /// pipe clones. Called from `crate::runtime::Runtime::eval` /
72
70
  /// `crate::runtime::Runtime::run` at the top of every guest
73
- /// invocation (docs/behavior.md B-03 / B-04).
71
+ /// invocation.
74
72
  pub(super) fn install_wasi(
75
73
  &mut self,
76
74
  wasi: WasiP1Ctx,
@@ -128,20 +126,20 @@ impl Invocation {
128
126
 
129
127
  /// Replace the per-run wall-clock deadline. `Some(at)` makes the
130
128
  /// epoch-deadline callback trap once `Instant::now() >= at`; `None`
131
- /// disables the cap. Called at the top of every `#run` (docs/behavior.md B-01).
129
+ /// disables the cap. Called at the top of every `#run`.
132
130
  pub(super) fn set_deadline(&mut self, deadline: Option<Instant>) {
133
131
  self.deadline = deadline;
134
132
  }
135
133
 
136
134
  /// Return the current per-run deadline. Read from the epoch-deadline
137
- /// callback installed by `crate::runtime::Runtime::from_path`.
135
+ /// callback installed by `crate::runtime::Runtime::new_store`.
138
136
  pub(super) fn deadline(&self) -> Option<Instant> {
139
137
  self.deadline
140
138
  }
141
139
 
142
140
  /// Mutable handle to the embedded `MemoryLimiter`. Required by
143
141
  /// the wasmtime `ResourceLimiter` callback wiring in
144
- /// `crate::runtime::Runtime::from_path`
142
+ /// `crate::runtime::Runtime::new_store`
145
143
  /// (`store.limiter(|state| state.limiter_mut())`); kept private to
146
144
  /// the wasm submodule so the only public surface for arming the
147
145
  /// cap goes through `Invocation::arm_memory_cap` /
@@ -150,7 +148,7 @@ impl Invocation {
150
148
  &mut self.limiter
151
149
  }
152
150
 
153
- /// Arm the docs/behavior.md E-20 memory cap for one guest run with
151
+ /// Arm the memory cap for one guest run with
154
152
  /// the current linear-memory size as the baseline. The limiter
155
153
  /// charges only the `memory.grow` delta past `baseline` against
156
154
  /// the cap, so the mruby image's initial allocation and the
@@ -163,23 +161,23 @@ impl Invocation {
163
161
  self.limiter.activate(baseline);
164
162
  }
165
163
 
166
- /// Disarm the docs/behavior.md E-20 memory cap. See
164
+ /// Disarm the memory cap. See
167
165
  /// `Invocation::arm_memory_cap`.
168
166
  pub(super) fn disarm_memory_cap(&mut self) {
169
167
  self.limiter.deactivate();
170
168
  }
171
169
 
172
- /// Stamp the wall-clock entry instant for the docs/behavior.md
173
- /// B-35 `wall_time` measurement. Called at the top of every
170
+ /// Stamp the wall-clock entry instant for the `wall_time`
171
+ /// measurement. Called at the top of every
174
172
  /// invocation immediately before the guest export call so the
175
- /// bracket matches the `timeout` deadline accounting (B-01) and
173
+ /// bracket matches the `timeout` deadline accounting and
176
174
  /// excludes post-run host bookkeeping such as `OUTCOME_BUFFER`
177
175
  /// decoding.
178
176
  pub(super) fn start_wall_clock(&mut self) {
179
177
  self.wall_entry = Some(Instant::now());
180
178
  }
181
179
 
182
- /// Close the docs/behavior.md B-35 `wall_time` measurement
180
+ /// Close the `wall_time` measurement
183
181
  /// started by `Invocation::start_wall_clock`. Idempotent — a
184
182
  /// stop with no matching start (e.g. if the guest export call
185
183
  /// never executed because of a host-side allocation failure)
@@ -191,13 +189,13 @@ impl Invocation {
191
189
  }
192
190
 
193
191
  /// Return the wall-clock duration the most recent invocation
194
- /// spent inside the guest export call (docs/behavior.md B-35).
192
+ /// spent inside the guest export call.
195
193
  /// Zero before the first invocation.
196
194
  pub(super) fn wall_time(&self) -> Duration {
197
195
  self.wall_time
198
196
  }
199
197
 
200
- /// Return the docs/behavior.md B-35 `memory_peak` — the high-
198
+ /// Return the `memory_peak` — the high-
201
199
  /// water mark of the per-invocation `memory.grow` delta past the
202
200
  /// linear-memory size captured at invocation entry. Zero before
203
201
  /// the first invocation.
@@ -207,7 +205,7 @@ impl Invocation {
207
205
  }
208
206
 
209
207
  /// Resource limiter that enforces the per-invocation `memory_limit`
210
- /// cap from docs/behavior.md B-01 / E-20.
208
+ /// cap.
211
209
  ///
212
210
  /// `max_memory` is the byte cap on per-invocation growth (`None` disables
213
211
  /// the cap). `baseline` is the linear-memory size captured at invocation
@@ -250,9 +248,9 @@ impl MemoryLimiter {
250
248
  /// the cap is dormant by default — the module's declared initial
251
249
  /// memory is allocated during `Linker::instantiate` and the
252
250
  /// per-invocation budget excludes anything that existed before
253
- /// arming (docs/behavior.md B-01 Notes, E-20). Also clears the
251
+ /// arming. Also clears the
254
252
  /// per-invocation `MemoryLimiter::peak` high-water so the
255
- /// docs/behavior.md B-35 `memory_peak` accounting restarts from
253
+ /// `memory_peak` accounting restarts from
256
254
  /// zero for the new invocation.
257
255
  fn activate(&mut self, baseline: usize) {
258
256
  self.baseline = baseline;
@@ -271,9 +269,9 @@ impl MemoryLimiter {
271
269
  /// Return the high-water mark of the per-invocation
272
270
  /// `memory.grow` delta past `baseline` observed since the last
273
271
  /// `MemoryLimiter::activate`. Read after the guest export
274
- /// returns to populate `Kobako::Usage#memory_peak`
275
- /// (docs/behavior.md B-35). Pinned to the last accepted grow —
276
- /// rejected `desired` values that trip the docs/behavior.md E-20
272
+ /// returns to populate `Kobako::Usage#memory_peak`.
273
+ /// Pinned to the last accepted grow —
274
+ /// rejected `desired` values that trip the memory
277
275
  /// cap never update the peak, so the reported value never exceeds
278
276
  /// `memory_limit`.
279
277
  pub(super) fn peak(&self) -> usize {
@@ -313,8 +311,9 @@ impl ResourceLimiter for MemoryLimiter {
313
311
  }
314
312
  }
315
313
 
316
- /// Marker error returned from `MemoryLimiter::memory_growing` on
317
- /// docs/behavior.md E-20. Downcast from the wasmtime trap error to surface as
314
+ /// Marker error returned from `MemoryLimiter::memory_growing` when the
315
+ /// per-invocation memory cap is exceeded. Downcast from the wasmtime
316
+ /// trap error to surface as
318
317
  /// `Kobako::MemoryLimitError` on the Ruby side. Callers use the
319
318
  /// `Display` impl below — no field is read directly — so the inner
320
319
  /// state stays private.
@@ -348,9 +347,9 @@ impl std::fmt::Display for MemoryLimitTrap {
348
347
 
349
348
  impl std::error::Error for MemoryLimitTrap {}
350
349
 
351
- /// Marker error returned from the epoch-deadline callback on
352
- /// docs/behavior.md E-19. Downcast from the wasmtime trap error to
353
- /// surface as `Kobako::TimeoutError` on the Ruby side.
350
+ /// Marker error returned from the epoch-deadline callback when the
351
+ /// wall-clock deadline is exceeded. Downcast from the wasmtime trap
352
+ /// error to surface as `Kobako::TimeoutError` on the Ruby side.
354
353
  #[derive(Debug)]
355
354
  pub(crate) struct TimeoutTrap;
356
355
 
@@ -362,62 +361,6 @@ impl std::fmt::Display for TimeoutTrap {
362
361
 
363
362
  impl std::error::Error for TimeoutTrap {}
364
363
 
365
- /// Interior-mutability wrapper around `wasmtime::Store<Invocation>`.
366
- ///
367
- /// Magnus requires `Send + Sync` for wrapped types. `wasmtime::Store` is not
368
- /// `Sync`, so we wrap it in a `RefCell`. `RefCell` alone is sufficient
369
- /// because magnus enforces single-threaded GVL access from Ruby; `Send` and
370
- /// `Sync` are asserted via the unsafe impls below.
371
- pub(super) struct StoreCell {
372
- inner: RefCell<WtStore<Invocation>>,
373
- }
374
-
375
- impl StoreCell {
376
- /// Wrap a freshly-built `wasmtime::Store<Invocation>` so it can be owned
377
- /// by the magnus-wrapped `Runtime`.
378
- pub(super) fn new(store: WtStore<Invocation>) -> Self {
379
- Self {
380
- inner: RefCell::new(store),
381
- }
382
- }
383
-
384
- /// Immutable borrow of the wrapped Store. Panics if a `borrow_mut()`
385
- /// is currently live — matches `RefCell::borrow` semantics.
386
- pub(super) fn borrow(&self) -> Ref<'_, WtStore<Invocation>> {
387
- self.inner.borrow()
388
- }
389
-
390
- /// Mutable borrow of the wrapped Store. Panics if any other borrow is
391
- /// currently live — matches `RefCell::borrow_mut` semantics.
392
- pub(super) fn borrow_mut(&self) -> RefMut<'_, WtStore<Invocation>> {
393
- self.inner.borrow_mut()
394
- }
395
- }
396
-
397
- // SAFETY: magnus requires `Send + Sync` on `#[magnus::wrap]` types. Both
398
- // claims hold under the GVL invariant:
399
- //
400
- // * Send — `wasmtime::Store<Invocation>` is itself `Send` (verified
401
- // upstream by wasmtime; see `wasmtime::Store`'s trait impls).
402
- // `RefCell<T>: Send` whenever `T: Send`. The remaining stored state
403
- // (`Invocation`) holds `Opaque<Value>` for the Ruby Server handle —
404
- // `Opaque<Value>` is documented as `Send` by magnus precisely so
405
- // wrapped objects can satisfy this bound.
406
- //
407
- // * Sync — `RefCell` is *not* `Sync` in the general Rust sense
408
- // (concurrent `borrow_mut` is UB). We assert `Sync` here because the
409
- // GVL serialises every call into Ruby C and every entry into magnus-
410
- // wrapped methods onto a single OS thread at a time: by the time the
411
- // `Sync` bound matters, magnus has already established that only one
412
- // thread can be inside the wrapper. Cross-thread mutation cannot
413
- // occur. If a future magnus release adopts a thread model that
414
- // permits concurrent access to wrapped objects, this assertion would
415
- // have to revert and `StoreCell` would need to switch to
416
- // `Mutex<wasmtime::Store<…>>` — but as of magnus 0.8 the contract
417
- // holds.
418
- unsafe impl Send for StoreCell {}
419
- unsafe impl Sync for StoreCell {}
420
-
421
364
  #[cfg(test)]
422
365
  mod tests {
423
366
  //! Unit tests for `MemoryLimiter` — the per-invocation memory
@@ -17,7 +17,7 @@ use super::invocation::{Invocation, MemoryLimitTrap, TimeoutTrap};
17
17
  use super::{memory_limit_err, setup_err, timeout_err, trap_err};
18
18
 
19
19
  /// Epoch-deadline callback installed on every Store. Read the per-run
20
- /// wall-clock deadline from `Invocation` (docs/behavior.md B-01) and trap with
20
+ /// wall-clock deadline from `Invocation` and trap with
21
21
  /// `TimeoutTrap` once the deadline has passed; otherwise extend the
22
22
  /// next check by one tick of the process-wide epoch ticker. When the
23
23
  /// deadline is `None` the callback should not fire under normal
@@ -41,9 +41,9 @@ pub(super) fn epoch_deadline_callback(
41
41
  /// without the magnus surface.
42
42
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
43
43
  enum TrapClass {
44
- /// docs/behavior.md E-19 wall-clock cap path.
44
+ /// Wall-clock cap path.
45
45
  Timeout,
46
- /// docs/behavior.md E-20 linear-memory cap path.
46
+ /// Linear-memory cap path.
47
47
  MemoryLimit,
48
48
  /// Any other wasmtime error — surfaces as the base
49
49
  /// `Kobako::TrapError`.
@@ -117,8 +117,8 @@ fn other_trap_message(err: &wasmtime::Error) -> String {
117
117
  }
118
118
 
119
119
  /// Map an instantiation error to `Kobako::SetupError`. Instantiation runs
120
- /// during `from_path` construction, before any invocation — docs/behavior.md
121
- /// E-41 classifies every such failure as a construction setup fault, not a
120
+ /// during `from_path` construction, before any invocation — every such
121
+ /// failure is a construction setup fault, not a
122
122
  /// per-invocation cap outcome. The memory cap is dormant during
123
123
  /// instantiation (see `Invocation::arm_memory_cap` /
124
124
  /// `Invocation::disarm_memory_cap`) and the epoch deadline is not yet