kobako 0.26.0 → 0.28.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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +103 -0
  4. data/Cargo.lock +117 -140
  5. data/README.md +156 -118
  6. data/ROADMAP.md +23 -22
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +38 -0
  9. data/crates/kobako-runtime/Cargo.toml +2 -2
  10. data/crates/kobako-runtime/README.md +10 -12
  11. data/crates/kobako-runtime/src/error.rs +8 -9
  12. data/crates/kobako-runtime/src/lib.rs +1 -1
  13. data/crates/kobako-runtime/src/profile.rs +1 -1
  14. data/crates/kobako-runtime/src/runtime.rs +31 -17
  15. data/crates/kobako-runtime/src/snapshot.rs +13 -10
  16. data/crates/kobako-runtime/src/yielder.rs +9 -6
  17. data/crates/kobako-transport/CHANGELOG.md +35 -0
  18. data/crates/kobako-transport/Cargo.toml +1 -1
  19. data/crates/kobako-transport/README.md +43 -27
  20. data/crates/kobako-transport/src/abi.rs +11 -14
  21. data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
  22. data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
  23. data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
  24. data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
  25. data/crates/kobako-transport/src/envelope/reply.rs +77 -53
  26. data/crates/kobako-transport/src/envelope/request.rs +295 -0
  27. data/crates/kobako-transport/src/envelope/run.rs +1 -2
  28. data/crates/kobako-transport/src/envelope/tag.rs +49 -0
  29. data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
  30. data/crates/kobako-transport/src/envelope.rs +19 -12
  31. data/crates/kobako-transport/src/lib.rs +0 -2
  32. data/crates/kobako-wasmtime/CHANGELOG.md +51 -0
  33. data/crates/kobako-wasmtime/Cargo.toml +7 -5
  34. data/crates/kobako-wasmtime/README.md +8 -10
  35. data/crates/kobako-wasmtime/src/abi.rs +5 -8
  36. data/crates/kobako-wasmtime/src/ambient.rs +2 -2
  37. data/crates/kobako-wasmtime/src/cache.rs +25 -59
  38. data/crates/kobako-wasmtime/src/capture.rs +4 -11
  39. data/crates/kobako-wasmtime/src/config.rs +11 -14
  40. data/crates/kobako-wasmtime/src/dispatch.rs +70 -80
  41. data/crates/kobako-wasmtime/src/driver.rs +133 -86
  42. data/crates/kobako-wasmtime/src/exports.rs +88 -34
  43. data/crates/kobako-wasmtime/src/guest_mem.rs +145 -138
  44. data/crates/kobako-wasmtime/src/instance_pre.rs +11 -22
  45. data/crates/kobako-wasmtime/src/invocation.rs +55 -372
  46. data/crates/kobako-wasmtime/src/io.rs +286 -0
  47. data/crates/kobako-wasmtime/src/lib.rs +3 -24
  48. data/crates/kobako-wasmtime/src/limiter.rs +253 -0
  49. data/crates/kobako-wasmtime/src/trap.rs +32 -45
  50. data/crates/kobako-wasmtime/tests/preamble_flags.rs +69 -0
  51. data/crates/kobako-wasmtime/tests/snippet_compile.rs +94 -0
  52. data/crates/kobako-wasmtime/tests/support/mod.rs +34 -0
  53. data/crates/kobako-wasmtime/tests/unsupported_call.rs +50 -0
  54. data/data/kobako.wasm +0 -0
  55. data/ext/kobako/Cargo.toml +2 -2
  56. data/ext/kobako/src/runtime/bridge.rs +38 -47
  57. data/ext/kobako/src/runtime/errors.rs +26 -32
  58. data/ext/kobako/src/runtime/gvl.rs +4 -12
  59. data/ext/kobako/src/runtime.rs +169 -212
  60. data/lib/kobako/capture.rb +1 -4
  61. data/lib/kobako/catalog/extensions.rb +21 -59
  62. data/lib/kobako/catalog/flags.rb +41 -0
  63. data/lib/kobako/catalog/handles.rb +7 -54
  64. data/lib/kobako/catalog/services.rb +7 -61
  65. data/lib/kobako/catalog/snippets.rb +42 -66
  66. data/lib/kobako/catalog.rb +2 -10
  67. data/lib/kobako/codec/decoder.rb +9 -28
  68. data/lib/kobako/codec/encoder.rb +3 -22
  69. data/lib/kobako/codec/error.rb +6 -27
  70. data/lib/kobako/codec/ext_types.rb +72 -94
  71. data/lib/kobako/codec/handle_walk.rb +65 -130
  72. data/lib/kobako/codec/nesting.rb +3 -8
  73. data/lib/kobako/codec/state.rb +4 -10
  74. data/lib/kobako/codec/utils.rb +6 -34
  75. data/lib/kobako/codec.rb +3 -22
  76. data/lib/kobako/constant_name.rb +8 -0
  77. data/lib/kobako/context.rb +51 -112
  78. data/lib/kobako/errors.rb +51 -127
  79. data/lib/kobako/execution.rb +16 -21
  80. data/lib/kobako/extension.rb +23 -40
  81. data/lib/kobako/handle.rb +7 -12
  82. data/lib/kobako/outcome.rb +51 -80
  83. data/lib/kobako/payload/arguments.rb +1 -16
  84. data/lib/kobako/payload.rb +4 -12
  85. data/lib/kobako/pool.rb +23 -53
  86. data/lib/kobako/runtime.rb +7 -21
  87. data/lib/kobako/sandbox.rb +96 -133
  88. data/lib/kobako/sandbox_options.rb +38 -84
  89. data/lib/kobako/snippet/binary.rb +1 -16
  90. data/lib/kobako/snippet/source.rb +1 -16
  91. data/lib/kobako/snippet.rb +1 -9
  92. data/lib/kobako/transport/call.rb +1 -14
  93. data/lib/kobako/transport/dispatcher.rb +112 -195
  94. data/lib/kobako/transport/error.rb +6 -20
  95. data/lib/kobako/transport/exposure.rb +5 -10
  96. data/lib/kobako/transport/reflection.rb +39 -50
  97. data/lib/kobako/transport/run.rb +17 -71
  98. data/lib/kobako/transport/yielder.rb +21 -73
  99. data/lib/kobako/transport.rb +3 -15
  100. data/lib/kobako/unresolved.rb +4 -11
  101. data/lib/kobako/usage.rb +2 -6
  102. data/lib/kobako/version.rb +1 -1
  103. data/sig/kobako/catalog/extensions.rbs +2 -2
  104. data/sig/kobako/catalog/flags.rbs +17 -0
  105. data/sig/kobako/catalog/services.rbs +0 -6
  106. data/sig/kobako/catalog/snippets.rbs +4 -4
  107. data/sig/kobako/codec/ext_types.rbs +10 -8
  108. data/sig/kobako/codec/handle_walk.rbs +9 -7
  109. data/sig/kobako/codec/utils.rbs +2 -2
  110. data/sig/kobako/codec.rbs +2 -2
  111. data/sig/kobako/constant_name.rbs +3 -0
  112. data/sig/kobako/context.rbs +2 -3
  113. data/sig/kobako/errors.rbs +6 -0
  114. data/sig/kobako/outcome.rbs +6 -8
  115. data/sig/kobako/runtime.rbs +12 -8
  116. data/sig/kobako/sandbox.rbs +4 -0
  117. data/sig/kobako/sandbox_options.rbs +2 -6
  118. data/sig/kobako/transport/dispatcher.rbs +14 -18
  119. data/sig/kobako/transport/reflection.rbs +5 -3
  120. data/sig/kobako/transport/run.rbs +1 -3
  121. data/sig/kobako/transport/yielder.rbs +3 -3
  122. data/sig/kobako/transport.rbs +7 -6
  123. metadata +16 -4
  124. data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
  125. data/crates/kobako-wasmtime/src/frames.rs +0 -273
  126. data/rust-toolchain.toml +0 -11
@@ -22,8 +22,6 @@ thread_local! {
22
22
  static GVL_RELEASED: Cell<bool> = const { Cell::new(false) };
23
23
  }
24
24
 
25
- /// Run `f` (one invocation's guest-execution span) and return its value,
26
- /// releasing the GVL for the span iff `release`. When holding, `f` runs inline.
27
25
  pub(crate) fn region<F, R>(release: bool, f: F) -> R
28
26
  where
29
27
  F: FnOnce() -> R,
@@ -37,10 +35,8 @@ where
37
35
  })
38
36
  }
39
37
 
40
- /// Run `f` (a guest→host dispatch callback) under the GVL. Re-acquires the GVL
41
- /// when the enclosing region released it, and runs inline when the GVL is
42
- /// already held — hold mode, or a nested dispatch whose outer frame re-acquired
43
- /// it.
38
+ /// Runs inline when the GVL is already held — hold mode, or a nested
39
+ /// dispatch whose outer frame re-acquired it.
44
40
  pub(crate) fn reenter<F, R>(f: F) -> R
45
41
  where
46
42
  F: FnOnce() -> R,
@@ -79,9 +75,8 @@ struct GvlCtx<F, R> {
79
75
  result: Option<std::thread::Result<R>>,
80
76
  }
81
77
 
82
- /// The `extern "C"` shim the `rb_thread_call_*` functions call: it runs the
83
- /// erased closure under `catch_unwind` so a panic is captured here rather than
84
- /// unwinding across the C frame, and stashes the outcome for `call_via`.
78
+ /// Runs the closure under `catch_unwind`, so a panic is captured here rather
79
+ /// than unwinding across the C frame.
85
80
  unsafe extern "C" fn trampoline<F, R>(data: *mut c_void) -> *mut c_void
86
81
  where
87
82
  F: FnOnce() -> R,
@@ -92,9 +87,6 @@ where
92
87
  ptr::null_mut()
93
88
  }
94
89
 
95
- /// Drive `f` through one of the `rb_thread_call_*` entry points (`invoke`),
96
- /// erasing it to the C callback shape and re-raising on the caller's thread any
97
- /// panic the trampoline caught.
98
90
  fn call_via<F, R>(
99
91
  f: F,
100
92
  invoke: impl FnOnce(unsafe extern "C" fn(*mut c_void) -> *mut c_void, *mut c_void) -> *mut c_void,
@@ -15,13 +15,7 @@
15
15
  //! `kobako-wasmtime` crate behind the `kobako_runtime` contract; no wasm
16
16
  //! engine type reaches this crate or the Host App.
17
17
  //!
18
- //! Module layout — one responsibility per file:
19
- //!
20
- //! * `bridge` — the magnus dispatch bridge: `RubyDispatchHandler` plus the
21
- //! frame-scoped `GuestYielder` Ruby class.
22
- //! * `errors` — the single boundary mapping the neutral `Trap` /
23
- //! `SetupError` channels onto the `Kobako::*` classes for a failure that
24
- //! never produced a `Snapshot` (a could-not-start fault).
18
+ //! Each module holds one responsibility and opens with its own doc.
25
19
 
26
20
  mod bridge;
27
21
  mod errors;
@@ -29,7 +23,7 @@ mod gvl;
29
23
 
30
24
  use magnus::{
31
25
  function, method, prelude::*, typed_data::DataTypeFunctions, value::Opaque,
32
- Error as MagnusError, RArray, RModule, RString, Ruby, Symbol, TypedData, Value,
26
+ Error as MagnusError, Exception, RArray, RModule, RString, Ruby, Symbol, TypedData, Value,
33
27
  };
34
28
 
35
29
  use std::path::Path;
@@ -37,11 +31,10 @@ use std::sync::Arc;
37
31
  use std::time::Duration;
38
32
 
39
33
  use kobako_runtime::dispatch::DispatchHandler;
40
- use kobako_runtime::error::Trap;
41
34
  use kobako_runtime::profile::Profile;
42
- use kobako_runtime::runtime::{Entry, Frames, Runtime as ContractRuntime};
43
- use kobako_runtime::snapshot::{Capture, Completion, Snapshot as RuntimeSnapshot, Usage};
44
- use kobako_transport::envelope::{Bindings, Outcome, Run, Snippet, Snippets};
35
+ use kobako_runtime::runtime::{Entry, Runtime as ContractRuntime};
36
+ use kobako_runtime::snapshot::{Completion, Snapshot as RuntimeSnapshot};
37
+ use kobako_transport::envelope::{Bindings, Flag, Outcome, Preamble, Snippet, Snippets};
45
38
  use kobako_wasmtime::{Config, Driver};
46
39
 
47
40
  /// A Panic's fields as they cross to Ruby: origin, class, message,
@@ -50,40 +43,24 @@ use kobako_wasmtime::{Config, Driver};
50
43
  /// shape, matching the `Kobako::Outcome::panic_fields` alias.
51
44
  type PanicFields = (String, String, String, Vec<String>, Vec<String>);
52
45
 
53
- /// Copy the bytes of `s` into a fresh `Vec<u8>`. Single safe entry to
54
- /// what would otherwise be an inline `unsafe { rstring.as_slice() }
55
- /// .to_vec()` duplicated at every host-↔-guest boundary. The borrow
56
- /// does not outlive this call, so no Ruby allocation can move the
57
- /// underlying RString between the borrow and the copy — the safety
58
- /// invariant the inline form relied on is established once here.
46
+ /// The one place the boundary borrows an RString's bytes: the borrow does
47
+ /// not outlive this call, so no Ruby allocation can move the RString
48
+ /// between the borrow and the copy.
59
49
  fn rstring_to_vec(s: RString) -> Vec<u8> {
60
50
  // SAFETY: see item doc.
61
51
  unsafe { s.as_slice() }.to_vec()
62
52
  }
63
53
 
64
- /// Frame the Frame 1 preamble from the Service registry's bind paths.
65
- /// The core envelope's byte layout lives on this side of the boundary,
66
- /// so the registry stays a registry and never holds a wire image.
67
- fn frame_preamble(paths: RArray) -> Result<Vec<u8>, MagnusError> {
68
- Ok(Bindings {
69
- paths: paths.to_vec()?,
70
- }
71
- .encode())
72
- }
73
-
74
- /// Frame the Frame 3 snippet table from the registry's entries — one
75
- /// `[kind, name, body]` triple each, `kind` a Symbol naming the form so
76
- /// the wire's discriminant byte stays here. An off-ladder kind raises
77
- /// rather than defaulting, the same fail-closed posture `from_path`
78
- /// takes on its Symbol options.
79
- fn frame_snippets(ruby: &Ruby, entries: RArray) -> Result<Vec<u8>, MagnusError> {
80
- let mut frame = Snippets {
54
+ /// `kind` arrives as a Symbol so the registry never names the wire's
55
+ /// discriminant; an off-ladder kind raises rather than defaulting.
56
+ fn snippets_from(ruby: &Ruby, entries: RArray) -> Result<Snippets, MagnusError> {
57
+ let mut snippets = Snippets {
81
58
  entries: Vec::with_capacity(entries.len()),
82
59
  };
83
60
  for index in 0..entries.len() as isize {
84
61
  let entry: RArray = entries.entry(index)?;
85
62
  let kind: Symbol = entry.entry(0)?;
86
- frame.entries.push(match kind.name()?.as_ref() {
63
+ snippets.entries.push(match kind.name()?.as_ref() {
87
64
  "source" => Snippet::Source {
88
65
  name: entry.entry(1)?,
89
66
  body: entry.entry(2)?,
@@ -99,7 +76,30 @@ fn frame_snippets(ruby: &Ruby, entries: RArray) -> Result<Vec<u8>, MagnusError>
99
76
  }
100
77
  });
101
78
  }
102
- Ok(frame.encode())
79
+ Ok(snippets)
80
+ }
81
+
82
+ /// One owner for the Preamble every guest entry reads, so an invocation
83
+ /// and a compile cannot drift in what they send.
84
+ fn preamble_from(
85
+ ruby: &Ruby,
86
+ paths: Vec<String>,
87
+ snippets: RArray,
88
+ flags: RArray,
89
+ ) -> Result<Preamble, MagnusError> {
90
+ let mut preamble = Preamble::default();
91
+ preamble.bindings = Bindings { paths };
92
+ preamble.snippets = snippets_from(ruby, snippets)?;
93
+ preamble.flags = flags
94
+ .to_vec::<(String, String, bool)>()?
95
+ .into_iter()
96
+ .map(|(name, value, critical)| Flag {
97
+ name,
98
+ value,
99
+ critical,
100
+ })
101
+ .collect();
102
+ Ok(preamble)
103
103
  }
104
104
 
105
105
  // ---------------------------------------------------------------------------
@@ -113,9 +113,10 @@ pub fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
113
113
 
114
114
  let runtime = kobako.define_class("Runtime", ruby.class_object())?;
115
115
  runtime.define_singleton_method("from_path", function!(Runtime::from_path, 7))?;
116
- runtime.define_method("eval", method!(Runtime::eval, 4))?;
117
- runtime.define_method("run", method!(Runtime::run, 5))?;
116
+ runtime.define_method("eval", method!(Runtime::eval, 5))?;
117
+ runtime.define_method("run", method!(Runtime::run, 6))?;
118
118
  runtime.define_method("profile", method!(Runtime::profile, 0))?;
119
+ runtime.define_method("compile", method!(Runtime::compile, 3))?;
119
120
  // The guest re-enters for a block yield through a frame-scoped
120
121
  // `Kobako::Runtime::GuestYielder` the dispatcher hands the Proc, not a
121
122
  // method on Runtime.
@@ -124,9 +125,7 @@ pub fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
124
125
  // Snapshot — the per-invocation result object each entry point returns.
125
126
  let snapshot = runtime.define_class("Snapshot", ruby.class_object())?;
126
127
  snapshot.define_method("outcome", method!(Snapshot::outcome, 0))?;
127
- snapshot.define_method("trapped?", method!(Snapshot::trapped, 0))?;
128
- snapshot.define_method("trap_kind", method!(Snapshot::trap_kind, 0))?;
129
- snapshot.define_method("trap_message", method!(Snapshot::trap_message, 0))?;
128
+ snapshot.define_method("trap_error", method!(Snapshot::trap_error, 0))?;
130
129
  snapshot.define_method("wall_time", method!(Snapshot::wall_time, 0))?;
131
130
  snapshot.define_method("memory_peak", method!(Snapshot::memory_peak, 0))?;
132
131
  snapshot.define_method("stdout", method!(Snapshot::stdout, 0))?;
@@ -157,22 +156,8 @@ struct Runtime {
157
156
  impl DataTypeFunctions for Runtime {}
158
157
 
159
158
  impl Runtime {
160
- /// Construct a Runtime from a wasm file path, using the process-wide
161
- /// shared Engine and per-path Module / InstancePre caches. The single
162
- /// Ruby-facing constructor for `Kobako::Runtime` — Engine and Module
163
- /// are never visible to Ruby.
164
- ///
165
- /// `timeout_seconds` is the wall-clock cap in seconds
166
- /// (`None` disables); `memory_limit` is the linear-memory cap in
167
- /// bytes (`None` disables); `stdout_limit` / `stderr_limit`
168
- /// are the per-channel output caps (`None`
169
- /// disables); `profile` is the isolation rung the driver builds
170
- /// (`:permissive` / `:hermetic`); `gvl` is the scheduling mode
171
- /// (`:hold` / `:release`) deciding whether each invocation releases
172
- /// the GVL for its guest span. All six are validated by the caller
173
- /// (`Kobako::Sandbox`); this method only refuses non-finite or
174
- /// non-positive timeouts, off-ladder profiles, and unrecognized gvl
175
- /// modes as a defence in depth.
159
+ /// The only Ruby-facing constructor, so Engine and Module are never
160
+ /// visible to Ruby.
176
161
  fn from_path(
177
162
  path: String,
178
163
  timeout_seconds: Option<f64>,
@@ -246,90 +231,103 @@ impl Runtime {
246
231
  // Run-path methods. Each takes the run's dispatch handler as its first
247
232
  // argument and returns a `Snapshot` for any completed invocation —
248
233
  // success or trap alike. Only a could-not-start fault (a missing export
249
- // or a fault before the export call) raises a `Kobako::TrapError`
250
- // directly, since it yields no `Snapshot`.
234
+ // or a fault before the export call) raises directly, as the class
235
+ // `errors::to_magnus` gives its cause, since it yields no `Snapshot`.
251
236
  // -----------------------------------------------------------------
252
237
 
253
- /// One-shot mruby source execution (`#eval`). Builds the dispatch
254
- /// handler from `dispatch` (the per-invocation Proc), frames the two
255
- /// stdin invocation frames from the registry state (`paths`,
256
- /// `snippets`), hands them and the source to the driver, and returns
257
- /// the run's `Snapshot`.
258
238
  fn eval(
259
239
  &self,
260
240
  dispatch: Value,
261
241
  paths: RArray,
262
- source: RString,
263
242
  snippets: RArray,
243
+ flags: RArray,
244
+ source: RString,
264
245
  ) -> Result<Snapshot, MagnusError> {
265
- let ruby = Ruby::get().expect("Ruby thread");
266
- let handler = build_handler(dispatch);
267
- let preamble = frame_preamble(paths)?;
268
246
  let source = rstring_to_vec(source);
269
- let snippets = frame_snippets(&ruby, snippets)?;
270
- // Release the GVL around the guest span iff this Sandbox asks for it;
271
- // the closure touches no Ruby VALUE (the driver is magnus-free, and a
272
- // guest→host dispatch re-acquires the GVL through the bridge).
273
- let result = gvl::region(self.release_gvl, || {
274
- self.driver.invoke(
275
- Entry::Eval { source: &source },
276
- Frames {
277
- preamble: &preamble,
278
- snippets: &snippets,
279
- },
280
- handler,
281
- )
282
- });
283
- let snapshot = result.map_err(|e| errors::to_magnus(&ruby, e))?;
284
- Ok(Snapshot::from(snapshot))
247
+ self.invoke(
248
+ dispatch,
249
+ paths,
250
+ snippets,
251
+ flags,
252
+ Entry::Eval { source: &source },
253
+ )
285
254
  }
286
255
 
287
- /// Execute one entrypoint dispatch (`__kobako_run`) and return its
288
- /// `Snapshot`.
289
- ///
290
- /// The two-frame stdin protocol (preamble + snippets; no user source
291
- /// frame — docs/wire-codec.md § Invocation channels) plus the Run
292
- /// envelope copied into guest linear memory — `entrypoint` routes it,
293
- /// `payload` feeds it, and this side frames the two together. Cap
294
- /// semantics match `#eval`.
295
256
  fn run(
296
257
  &self,
297
258
  dispatch: Value,
298
259
  paths: RArray,
299
260
  snippets: RArray,
261
+ flags: RArray,
300
262
  entrypoint: String,
301
263
  payload: RString,
264
+ ) -> Result<Snapshot, MagnusError> {
265
+ self.invoke(
266
+ dispatch,
267
+ paths,
268
+ snippets,
269
+ flags,
270
+ Entry::Run {
271
+ entrypoint: &entrypoint,
272
+ payload: rstring_to_vec(payload),
273
+ },
274
+ )
275
+ }
276
+
277
+ /// One owner for the wiring both verbs share, so a Preamble or handler
278
+ /// change cannot drift between them.
279
+ fn invoke(
280
+ &self,
281
+ dispatch: Value,
282
+ paths: RArray,
283
+ snippets: RArray,
284
+ flags: RArray,
285
+ entry: Entry<'_>,
302
286
  ) -> Result<Snapshot, MagnusError> {
303
287
  let ruby = Ruby::get().expect("Ruby thread");
288
+ let verb = match entry {
289
+ Entry::Eval { .. } => "eval",
290
+ Entry::Run { .. } => "run",
291
+ };
304
292
  let handler = build_handler(dispatch);
305
- let preamble = frame_preamble(paths)?;
306
- let snippets = frame_snippets(&ruby, snippets)?;
307
- let envelope = Run {
308
- entrypoint,
309
- payload: rstring_to_vec(payload),
310
- }
311
- .encode();
293
+ let preamble = preamble_from(&ruby, paths.to_vec()?, snippets, flags)?;
312
294
  // Release the GVL around the guest span iff this Sandbox asks for it;
313
- // see the note in `#eval`.
295
+ // the closure touches no Ruby VALUE (the driver is magnus-free, and a
296
+ // guest→host dispatch re-acquires the GVL through the bridge).
314
297
  let result = gvl::region(self.release_gvl, || {
315
- self.driver.invoke(
316
- Entry::Run {
317
- envelope: &envelope,
318
- },
319
- Frames {
320
- preamble: &preamble,
321
- snippets: &snippets,
322
- },
323
- handler,
324
- )
298
+ self.driver.invoke(entry, &preamble, handler)
325
299
  });
326
- let snapshot = result.map_err(|e| errors::to_magnus(&ruby, e))?;
327
- Ok(Snapshot::from(snapshot))
300
+ result
301
+ .map(|snapshot| Snapshot(snapshot, verb))
302
+ .map_err(|e| errors::to_magnus(&ruby, e))
303
+ }
304
+
305
+ /// The bytecode of the source snippets, one binary String each in
306
+ /// order, or `nil` when the guest compiled nothing; no failure raises,
307
+ /// since the Sandbox keeps the source either way. Takes the Preamble an
308
+ /// invocation sends, `paths` included, so the driver can tell whether
309
+ /// the bytecode still fits it; no dispatch handler is bound, since
310
+ /// compiling runs no script.
311
+ fn compile(
312
+ &self,
313
+ paths: RArray,
314
+ snippets: RArray,
315
+ flags: RArray,
316
+ ) -> Result<Option<RArray>, MagnusError> {
317
+ let ruby = Ruby::get().expect("Ruby thread");
318
+ let preamble = preamble_from(&ruby, paths.to_vec()?, snippets, flags)?;
319
+ let bytecodes = gvl::region(self.release_gvl, || self.driver.compile(&preamble));
320
+ Ok(bytecodes.map(|bytecodes| {
321
+ ruby.ary_from_iter(
322
+ bytecodes
323
+ .iter()
324
+ .map(|bytecode| ruby.str_from_slice(bytecode)),
325
+ )
326
+ }))
328
327
  }
329
328
 
330
- /// Return the isolation profile the driver built, as a Symbol
331
- /// (`:hermetic` / `:permissive`) — the declaration the Sandbox
332
- /// compares against the posture its `profile:` option requested.
329
+ /// The driver's declaration, which the Sandbox checks against the
330
+ /// posture its `profile:` option requested.
333
331
  fn profile(&self) -> Symbol {
334
332
  let ruby = Ruby::get().expect("Ruby thread");
335
333
  match self.driver.profile() {
@@ -339,11 +337,9 @@ impl Runtime {
339
337
  }
340
338
  }
341
339
 
342
- /// Build the dispatch handler for one invocation from the per-call `dispatch`
343
- /// Proc. A `nil` Proc yields no handler. The Proc stays GC-rooted for the
344
- /// duration of the synchronous `#eval` / `#run` call as a live method
345
- /// argument on the Ruby stack, so the driver only borrows it (the safety
346
- /// contract on `kobako_runtime::runtime::Runtime`).
340
+ /// The Proc stays GC-rooted for the synchronous `#eval` / `#run` call as a
341
+ /// live method argument on the Ruby stack, so the driver only borrows it
342
+ /// (the safety contract on `kobako_runtime::runtime::Runtime`).
347
343
  fn build_handler(dispatch: Value) -> Option<Arc<dyn DispatchHandler>> {
348
344
  if dispatch.is_nil() {
349
345
  return None;
@@ -358,130 +354,91 @@ fn build_handler(dispatch: Value) -> Option<Arc<dyn DispatchHandler>> {
358
354
  /// driver produced, exposed as `Kobako::Runtime::Snapshot`. Usage and the
359
355
  /// two output captures are present on every outcome, so the trap path
360
356
  /// carries them just like the value path; the completion is read as either
361
- /// the outcome bytes (`#outcome`) or a trap (`#trapped?` / `#trap_kind` /
362
- /// `#trap_message`), and the Sandbox layer maps a trap onto its
363
- /// `Kobako::TrapError` family.
357
+ /// the Outcome (`#outcome`) or a trap (`#trap_error`). The verb names the
358
+ /// invocation, so an unsupported one says which operation is lacking.
364
359
  #[derive(TypedData)]
365
360
  #[magnus(class = "Kobako::Runtime::Snapshot", free_immediately, size)]
366
- struct Snapshot {
367
- completion: Completion,
368
- stdout: Capture,
369
- stderr: Capture,
370
- usage: Usage,
371
- }
361
+ struct Snapshot(RuntimeSnapshot, &'static str);
372
362
 
373
363
  impl DataTypeFunctions for Snapshot {}
374
364
 
375
- impl From<RuntimeSnapshot> for Snapshot {
376
- fn from(snapshot: RuntimeSnapshot) -> Self {
377
- let RuntimeSnapshot {
378
- completion,
379
- stdout,
380
- stderr,
381
- usage,
382
- } = snapshot;
383
- Self {
384
- completion,
385
- stdout,
386
- stderr,
387
- usage,
388
- }
389
- }
390
- }
391
-
392
365
  impl Snapshot {
393
- /// One completed run's outcome, already split off the core envelope:
394
- /// `[kind, payload, panic]`. `kind` names the arm — `:ok`,
395
- /// `:panic`, `:absent` (nothing written), or `:malformed` (bytes the
396
- /// envelope cannot frame). `payload` is the invocation's
397
- /// codec-encoded value, carried only by `:ok`; every other arm
398
- /// answers empty. `panic` carries the Panic's own fields on `:panic`
399
- /// and is `nil` otherwise, so the Ruby side maps a failure onto its
400
- /// error taxonomy without decoding a payload byte.
401
- ///
402
- /// A trap answers `:absent` — `#trapped?` is the authoritative
403
- /// discriminator there and this is never read.
404
- fn outcome(&self) -> (Symbol, RString, Option<PanicFields>) {
366
+ /// Already split off the core envelope, so the Ruby side maps a failure
367
+ /// onto its error taxonomy without decoding a payload byte: the ok
368
+ /// arm's payload, or a Panic's fields beside an empty payload. A trap
369
+ /// has no Outcome; `#trap_error` is the discriminator there.
370
+ fn outcome(&self) -> Result<(RString, Option<PanicFields>), MagnusError> {
405
371
  let ruby = Ruby::get().expect("Ruby thread");
406
- let empty = || ruby.str_from_slice(&[]);
407
- let Completion::Outcome(bytes) = &self.completion else {
408
- return (ruby.to_symbol("absent"), empty(), None);
409
- };
410
- match Outcome::decode(bytes) {
411
- Ok(Outcome::Ok(value)) => (ruby.to_symbol("ok"), ruby.str_from_slice(&value), None),
412
- Ok(Outcome::Panic(panic)) => (
413
- ruby.to_symbol("panic"),
414
- empty(),
372
+ match &self.0.completion {
373
+ Completion::Outcome(Outcome::Ok(value)) => Ok((ruby.str_from_slice(value), None)),
374
+ Completion::Outcome(Outcome::Panic(panic)) => Ok((
375
+ ruby.str_from_slice(&[]),
415
376
  Some((
416
377
  panic.origin.name().to_owned(),
417
- panic.error.name,
418
- panic.error.message,
419
- panic.error.backtrace,
420
- panic.available,
378
+ panic.error.name.clone(),
379
+ panic.error.message.clone(),
380
+ panic.error.backtrace.clone(),
381
+ panic.available.clone(),
421
382
  )),
422
- ),
423
- Err(_) if bytes.is_empty() => (ruby.to_symbol("absent"), empty(), None),
424
- Err(_) => (ruby.to_symbol("malformed"), empty(), None),
383
+ )),
384
+ // The Guest Binary answered rather than broke, so it reads as
385
+ // the Sandbox's failure, worded by the host.
386
+ Completion::Outcome(Outcome::Unsupported(_)) => Ok((
387
+ ruby.str_from_slice(&[]),
388
+ Some((
389
+ "sandbox".to_owned(),
390
+ "Kobako::SandboxError".to_owned(),
391
+ format!(
392
+ "the Sandbox runtime does not support #{}; \
393
+ rebuild it against the kobako version in use",
394
+ self.1
395
+ ),
396
+ Vec::new(),
397
+ Vec::new(),
398
+ )),
399
+ )),
400
+ Completion::Trap(_) => Err(MagnusError::new(
401
+ ruby.exception_runtime_error(),
402
+ "a trapped run has no outcome",
403
+ )),
425
404
  }
426
405
  }
427
406
 
428
- /// `true` iff the invocation completed via an engine trap.
429
- fn trapped(&self) -> bool {
430
- matches!(self.completion, Completion::Trap(_))
431
- }
432
-
433
- /// The trap's neutral kind as a Symbol (`:timeout` / `:memory_limit` /
434
- /// `:trap`), or `nil` on a completed run. The Sandbox maps this onto the
435
- /// named `Kobako::TrapError` subclass.
436
- fn trap_kind(&self) -> Option<Symbol> {
407
+ /// Unraised, so the Sandbox layer can attach the run's Execution before
408
+ /// raising it.
409
+ fn trap_error(&self) -> Result<Option<Exception>, MagnusError> {
410
+ let Completion::Trap(trap) = &self.0.completion else {
411
+ return Ok(None);
412
+ };
437
413
  let ruby = Ruby::get().expect("Ruby thread");
438
- match &self.completion {
439
- Completion::Trap(Trap::Timeout(_)) => Some(ruby.to_symbol("timeout")),
440
- Completion::Trap(Trap::MemoryLimit(_)) => Some(ruby.to_symbol("memory_limit")),
441
- // A cap this frontend has no named subclass for is still a
442
- // trap; the base class is what the taxonomy owes it.
443
- Completion::Trap(_) => Some(ruby.to_symbol("trap")),
444
- Completion::Outcome(_) => None,
445
- }
446
- }
447
-
448
- /// The trap's message, or `nil` on a completed run.
449
- fn trap_message(&self) -> Option<String> {
450
- match &self.completion {
451
- Completion::Trap(trap) => Some(trap.to_string()),
452
- Completion::Outcome(_) => None,
453
- }
414
+ errors::trap_class(&ruby, trap)
415
+ .new_instance((trap.to_string(),))
416
+ .map(Some)
454
417
  }
455
418
 
456
- /// Wall-clock seconds the guest export call spent inside wasmtime.
457
419
  fn wall_time(&self) -> f64 {
458
- self.usage.wall_time
420
+ self.0.usage.wall_time
459
421
  }
460
422
 
461
- /// High-water `memory.grow` delta in bytes past the entry-time baseline.
462
423
  fn memory_peak(&self) -> usize {
463
- self.usage.memory_peak
424
+ self.0.usage.memory_peak
464
425
  }
465
426
 
466
- /// Bytes captured on the guest's stdout channel, clipped to the cap.
467
427
  fn stdout(&self) -> RString {
468
428
  let ruby = Ruby::get().expect("Ruby thread");
469
- ruby.str_from_slice(&self.stdout.bytes)
429
+ ruby.str_from_slice(&self.0.stdout.bytes)
470
430
  }
471
431
 
472
- /// `true` iff the stdout channel reached its cap during this run.
473
432
  fn stdout_truncated(&self) -> bool {
474
- self.stdout.truncated
433
+ self.0.stdout.truncated
475
434
  }
476
435
 
477
- /// Bytes captured on the guest's stderr channel, clipped to the cap.
478
436
  fn stderr(&self) -> RString {
479
437
  let ruby = Ruby::get().expect("Ruby thread");
480
- ruby.str_from_slice(&self.stderr.bytes)
438
+ ruby.str_from_slice(&self.0.stderr.bytes)
481
439
  }
482
440
 
483
- /// `true` iff the stderr channel reached its cap during this run.
484
441
  fn stderr_truncated(&self) -> bool {
485
- self.stderr.truncated
442
+ self.0.stderr.truncated
486
443
  }
487
444
  }
@@ -1,10 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- # What one invocation wrote to stdout or stderr, up to that channel's
5
- # cap, together with whether it wrote past the cap. Frozen, so the bytes
6
- # and the flag always travel together.
7
- class Capture
4
+ class Capture # :nodoc:
8
5
  attr_reader :bytes
9
6
 
10
7
  # The bytes read as UTF-8 when they are valid UTF-8, and as binary