kobako 0.9.1 → 0.10.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.
@@ -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` (docs/behavior.md E-41).
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,28 @@
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
+ //! (docs/behavior.md B-49) 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
+ //! (docs/behavior.md B-03 / B-04).
10
11
  //!
11
12
  //! The slot also carries the per-invocation wall-clock deadline
12
13
  //! (docs/behavior.md B-01, E-19) and the per-invocation linear-memory
13
14
  //! delta cap `MemoryLimiter` (docs/behavior.md B-01, E-20). Both are
14
15
  //! read from the wasmtime `epoch_deadline_callback` / `ResourceLimiter`
15
- //! callbacks installed in `crate::runtime::Runtime::from_path`. The
16
+ //! callbacks installed in `crate::runtime::Runtime::new_store`. The
16
17
  //! 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.
18
+ //! memory size captured at invocation entry — the image's initial
19
+ //! allocation is outside the budget.
20
20
  //!
21
- //! [SPEC.md Single-Invocation Slot]: ../../../SPEC.md
21
+ //! [SPEC.md Single-Invocation Slot]: ../../../../SPEC.md
22
22
 
23
- use std::cell::{Ref, RefCell, RefMut};
24
23
  use std::time::{Duration, Instant};
25
24
 
26
25
  use magnus::{value::Opaque, Value};
27
- use wasmtime::{ResourceLimiter, Store as WtStore};
26
+ use wasmtime::ResourceLimiter;
28
27
  use wasmtime_wasi::p1::WasiP1Ctx;
29
28
  use wasmtime_wasi::p2::pipe::MemoryOutputPipe;
30
29
 
@@ -134,14 +133,14 @@ impl Invocation {
134
133
  }
135
134
 
136
135
  /// Return the current per-run deadline. Read from the epoch-deadline
137
- /// callback installed by `crate::runtime::Runtime::from_path`.
136
+ /// callback installed by `crate::runtime::Runtime::new_store`.
138
137
  pub(super) fn deadline(&self) -> Option<Instant> {
139
138
  self.deadline
140
139
  }
141
140
 
142
141
  /// Mutable handle to the embedded `MemoryLimiter`. Required by
143
142
  /// the wasmtime `ResourceLimiter` callback wiring in
144
- /// `crate::runtime::Runtime::from_path`
143
+ /// `crate::runtime::Runtime::new_store`
145
144
  /// (`store.limiter(|state| state.limiter_mut())`); kept private to
146
145
  /// the wasm submodule so the only public surface for arming the
147
146
  /// cap goes through `Invocation::arm_memory_cap` /
@@ -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