kobako 0.12.1 → 0.12.2

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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +11 -0
  4. data/Cargo.lock +15 -2
  5. data/Cargo.toml +6 -2
  6. data/README.md +1 -1
  7. data/crates/kobako-runtime/CHANGELOG.md +8 -0
  8. data/crates/kobako-runtime/Cargo.toml +23 -0
  9. data/crates/kobako-runtime/README.md +34 -0
  10. data/crates/kobako-runtime/src/dispatch.rs +22 -0
  11. data/crates/kobako-runtime/src/error.rs +64 -0
  12. data/crates/kobako-runtime/src/lib.rs +16 -0
  13. data/crates/kobako-runtime/src/runtime.rs +50 -0
  14. data/crates/kobako-runtime/src/snapshot.rs +46 -0
  15. data/crates/kobako-runtime/src/yielder.rs +22 -0
  16. data/crates/kobako-wasmtime/CHANGELOG.md +8 -0
  17. data/crates/kobako-wasmtime/Cargo.toml +62 -0
  18. data/crates/kobako-wasmtime/README.md +32 -0
  19. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/ambient.rs +3 -3
  20. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/cache.rs +30 -41
  21. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/capture.rs +2 -2
  22. data/crates/kobako-wasmtime/src/config.rs +25 -0
  23. data/crates/kobako-wasmtime/src/dispatch.rs +110 -0
  24. data/crates/kobako-wasmtime/src/driver.rs +285 -0
  25. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/exports.rs +5 -6
  26. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/frames.rs +70 -82
  27. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/guest_mem.rs +38 -15
  28. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/instance_pre.rs +13 -21
  29. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/invocation.rs +54 -49
  30. data/crates/kobako-wasmtime/src/lib.rs +47 -0
  31. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/trap.rs +29 -35
  32. data/data/kobako.wasm +0 -0
  33. data/ext/kobako/Cargo.toml +9 -32
  34. data/ext/kobako/src/runtime/bridge.rs +150 -0
  35. data/ext/kobako/src/runtime/errors.rs +45 -13
  36. data/ext/kobako/src/runtime.rs +156 -406
  37. data/ext/kobako/src/snapshot.rs +27 -62
  38. data/lib/kobako/catalog/handles.rb +3 -3
  39. data/lib/kobako/catalog/namespaces.rb +4 -0
  40. data/lib/kobako/catalog/snippets.rb +4 -0
  41. data/lib/kobako/codec/encoder.rb +5 -1
  42. data/lib/kobako/codec/factory.rb +41 -13
  43. data/lib/kobako/codec/handle_walk.rb +4 -0
  44. data/lib/kobako/errors.rb +18 -16
  45. data/lib/kobako/sandbox.rb +20 -18
  46. data/lib/kobako/sandbox_options.rb +25 -9
  47. data/lib/kobako/snapshot.rb +7 -13
  48. data/lib/kobako/transport/dispatcher.rb +2 -2
  49. data/lib/kobako/transport/response.rb +14 -14
  50. data/lib/kobako/transport/run.rb +2 -6
  51. data/lib/kobako/transport/yield.rb +1 -1
  52. data/lib/kobako/transport/yielder.rb +2 -2
  53. data/lib/kobako/version.rb +1 -1
  54. data/release-please-config.json +48 -3
  55. data/sig/kobako/codec/factory.rbs +3 -0
  56. data/sig/kobako/errors.rbs +7 -14
  57. data/sig/kobako/runtime.rbs +8 -3
  58. data/sig/kobako/sandbox.rbs +2 -2
  59. data/sig/kobako/sandbox_options.rbs +4 -2
  60. data/sig/kobako/snapshot.rbs +0 -3
  61. data/sig/kobako/transport/dispatcher.rbs +1 -1
  62. data/sig/kobako/transport/run.rbs +2 -2
  63. data/sig/kobako/transport/yielder.rbs +2 -2
  64. data/sig/kobako/transport.rbs +8 -0
  65. metadata +27 -12
  66. data/ext/kobako/src/runtime/config.rs +0 -25
  67. data/ext/kobako/src/runtime/dispatch.rs +0 -211
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3
3
  "release-type": "ruby",
4
- "last-release-sha": "5694da60b08931ea260e13025689b8d8c47d767a",
4
+ "last-release-sha": "98509af508988f708bf0d7a76a718bb0428a177e",
5
5
  "packages": {
6
6
  ".": {
7
7
  "component": "kobako",
@@ -103,13 +103,58 @@
103
103
  "path": "/wasm/kobako-baker/README.md"
104
104
  }
105
105
  ]
106
+ },
107
+ "crates/kobako-runtime": {
108
+ "component": "kobako-runtime",
109
+ "release-type": "rust",
110
+ "extra-files": [
111
+ {
112
+ "type": "toml",
113
+ "path": "/crates/Cargo.lock",
114
+ "jsonpath": "$.package[?(@.name=='kobako-runtime')].version"
115
+ },
116
+ {
117
+ "type": "toml",
118
+ "path": "/Cargo.lock",
119
+ "jsonpath": "$.package[?(@.name=='kobako-runtime')].version"
120
+ },
121
+ {
122
+ "type": "generic",
123
+ "path": "/crates/kobako-runtime/README.md"
124
+ }
125
+ ]
126
+ },
127
+ "crates/kobako-wasmtime": {
128
+ "component": "kobako-wasmtime",
129
+ "release-type": "rust",
130
+ "extra-files": [
131
+ {
132
+ "type": "toml",
133
+ "path": "/crates/Cargo.lock",
134
+ "jsonpath": "$.package[?(@.name=='kobako-wasmtime')].version"
135
+ },
136
+ {
137
+ "type": "toml",
138
+ "path": "/Cargo.lock",
139
+ "jsonpath": "$.package[?(@.name=='kobako-wasmtime')].version"
140
+ },
141
+ {
142
+ "type": "toml",
143
+ "path": "/crates/kobako-wasmtime/Cargo.toml",
144
+ "jsonpath": "$.dependencies['kobako-runtime'].version"
145
+ },
146
+ {
147
+ "type": "generic",
148
+ "path": "/crates/kobako-wasmtime/README.md"
149
+ }
150
+ ]
106
151
  }
107
152
  },
108
153
  "plugins": [
109
154
  {
110
155
  "type": "linked-versions",
111
- "groupName": "kobako guest crates",
112
- "components": ["kobako-core", "kobako-rs", "kobako-io", "kobako-json", "kobako-regexp", "kobako-baker"]
156
+ "groupName": "kobako crates",
157
+ "components": ["kobako-core", "kobako-rs", "kobako-io", "kobako-json", "kobako-regexp", "kobako-baker", "kobako-runtime", "kobako-wasmtime"]
113
158
  }
114
159
  ],
115
160
  "extra-files": [
@@ -8,6 +8,8 @@ module Kobako
8
8
  EXT_SYMBOL: Integer
9
9
  EXT_HANDLE: Integer
10
10
  EXT_ERRENV: Integer
11
+ MAX_EXT_DEPTH: Integer
12
+ EXT_DEPTH_KEY: Symbol
11
13
 
12
14
  def dump: (untyped value) -> String
13
15
  def load: (String bytes) -> untyped
@@ -26,6 +28,7 @@ module Kobako
26
28
  def unpack_handle: (String payload) -> Kobako::Handle
27
29
  def pack_fault: (Kobako::Fault fault) -> String
28
30
  def unpack_fault: (String payload) -> Kobako::Fault
31
+ def within_ext_frame: [T] (singleton(Kobako::Codec::Error) over_limit) { () -> T } -> T
29
32
  end
30
33
  end
31
34
  end
@@ -17,7 +17,7 @@ module Kobako
17
17
  class ModuleNotBuiltError < SetupError
18
18
  end
19
19
 
20
- class SandboxError < Error
20
+ module StructuredError : Error
21
21
  attr_reader origin: String?
22
22
  attr_reader klass: String?
23
23
  attr_reader backtrace_lines: Array[String]?
@@ -32,22 +32,15 @@ module Kobako
32
32
  ) -> void
33
33
  end
34
34
 
35
- class ServiceError < Error
36
- attr_reader origin: String?
37
- attr_reader klass: String?
38
- attr_reader backtrace_lines: Array[String]?
39
- attr_reader details: untyped
35
+ class SandboxError < Error
36
+ include StructuredError
37
+ end
40
38
 
41
- def initialize: (
42
- String message,
43
- ?origin: String?,
44
- ?klass: String?,
45
- ?backtrace_lines: Array[String]?,
46
- ?details: untyped
47
- ) -> void
39
+ class ServiceError < Error
40
+ include StructuredError
48
41
  end
49
42
 
50
- class HandlerExhaustedError < SandboxError
43
+ class HandleExhaustedError < SandboxError
51
44
  end
52
45
 
53
46
  class BytecodeError < SandboxError
@@ -10,14 +10,19 @@ module Kobako
10
10
  Integer? stderr_limit_bytes
11
11
  ) -> Runtime
12
12
 
13
- def on_dispatch=: (^(String) -> String dispatch_proc) -> void
14
-
15
- def yield_to_active_invocation: (String args_bytes) -> String
13
+ def on_dispatch=: (^(String, Kobako::Transport::_GuestYielder) -> String dispatch_proc) -> void
16
14
 
17
15
  def eval: (String preamble, String source, String snippets) -> Kobako::Snapshot
18
16
 
19
17
  def run: (String preamble, String snippets, String envelope) -> Kobako::Snapshot
20
18
 
21
19
  def usage: () -> [Float, Integer]
20
+
21
+ # Frame-scoped guest re-entry handle the ext hands the dispatch Proc as
22
+ # its second argument; stands in as the +String -> String+ callable the
23
+ # host +Transport::Yielder+ invokes for a block yield.
24
+ class GuestYielder
25
+ def call: (String args_bytes) -> String
26
+ end
22
27
  end
23
28
  end
@@ -8,8 +8,8 @@ module Kobako
8
8
  # Forwarded to @options via Forwardable#def_delegators.
9
9
  def timeout: () -> Float?
10
10
  def memory_limit: () -> Integer?
11
- def stdout_limit: () -> Integer
12
- def stderr_limit: () -> Integer
11
+ def stdout_limit: () -> Integer?
12
+ def stderr_limit: () -> Integer?
13
13
 
14
14
  def initialize: (
15
15
  ?wasm_path: String?,
@@ -6,8 +6,8 @@ module Kobako
6
6
 
7
7
  attr_reader timeout: Float?
8
8
  attr_reader memory_limit: Integer?
9
- attr_reader stdout_limit: Integer
10
- attr_reader stderr_limit: Integer
9
+ attr_reader stdout_limit: Integer?
10
+ attr_reader stderr_limit: Integer?
11
11
 
12
12
  def self.new: (
13
13
  ?timeout: (Float | Integer)?,
@@ -28,5 +28,7 @@ module Kobako
28
28
  def normalize_timeout: ((Float | Integer)? timeout) -> Float?
29
29
 
30
30
  def normalize_memory_limit: (Integer? memory_limit) -> Integer?
31
+
32
+ def normalize_output_limit: (Integer? limit, String name) -> Integer?
31
33
  end
32
34
  end
@@ -5,11 +5,8 @@ module Kobako
5
5
  def stdout_truncated: () -> bool
6
6
  def stderr_bytes: () -> String
7
7
  def stderr_truncated: () -> bool
8
- def wall_time: () -> Float
9
- def memory_peak: () -> Integer
10
8
 
11
9
  def stdout: () -> Kobako::Capture
12
10
  def stderr: () -> Kobako::Capture
13
- def usage: () -> Kobako::Usage
14
11
  end
15
12
  end
@@ -12,7 +12,7 @@ module Kobako
12
12
 
13
13
  CALLABLE_ALLOW: Array[Symbol]
14
14
 
15
- def self?.dispatch: (String request_bytes, Kobako::Catalog::Namespaces namespaces, Kobako::Catalog::Handles handler, ^(String) -> String yield_to_guest) -> String
15
+ def self?.dispatch: (String request_bytes, Kobako::Catalog::Namespaces namespaces, Kobako::Catalog::Handles handler, Kobako::Transport::_GuestYielder yield_to_guest) -> String
16
16
 
17
17
  def self?.resolve_call_args: (Kobako::Transport::Request request, Kobako::Catalog::Handles handler) -> [Array[untyped], Hash[Symbol, untyped]]
18
18
 
@@ -17,9 +17,9 @@ module Kobako
17
17
 
18
18
  def normalize_entrypoint: (Symbol | String target) -> Symbol
19
19
 
20
- def validate_args!: (Array[untyped] args) -> Array[untyped]
20
+ def validate_args!: (Array[untyped] args) -> void
21
21
 
22
- def validate_kwargs!: (Hash[untyped, untyped] kwargs) -> Hash[Symbol, untyped]
22
+ def validate_kwargs!: (Hash[untyped, untyped] kwargs) -> void
23
23
 
24
24
  def forged_handle_message: (String slot) -> String
25
25
  end
@@ -1,12 +1,12 @@
1
1
  module Kobako
2
2
  module Transport
3
3
  class Yielder
4
- @yield_to_guest: ^(String) -> String
4
+ @yield_to_guest: Kobako::Transport::_GuestYielder
5
5
  @break_tag: Symbol
6
6
  @handler: Kobako::Catalog::Handles
7
7
  @active: bool
8
8
 
9
- def initialize: (^(String) -> String yield_to_guest, Symbol break_tag, Kobako::Catalog::Handles handler) -> void
9
+ def initialize: (Kobako::Transport::_GuestYielder yield_to_guest, Symbol break_tag, Kobako::Catalog::Handles handler) -> void
10
10
 
11
11
  def yield: (*untyped args) -> untyped
12
12
 
@@ -1,4 +1,12 @@
1
1
  module Kobako
2
2
  module Transport
3
+ # The guest re-entry callable a dispatch hands to a Yielder: invoked
4
+ # with the encoded yield args, it returns the encoded YieldResponse.
5
+ # The ext's Kobako::Runtime::GuestYielder is the production conformer;
6
+ # modelled structurally so the Transport layer needs no upward
7
+ # dependency on Runtime.
8
+ interface _GuestYielder
9
+ def call: (String) -> String
10
+ end
3
11
  end
4
12
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kobako
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.1
4
+ version: 0.12.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aotokitsuruya
@@ -54,23 +54,38 @@ files:
54
54
  - LICENSE
55
55
  - README.md
56
56
  - SECURITY.md
57
+ - crates/kobako-runtime/CHANGELOG.md
58
+ - crates/kobako-runtime/Cargo.toml
59
+ - crates/kobako-runtime/README.md
60
+ - crates/kobako-runtime/src/dispatch.rs
61
+ - crates/kobako-runtime/src/error.rs
62
+ - crates/kobako-runtime/src/lib.rs
63
+ - crates/kobako-runtime/src/runtime.rs
64
+ - crates/kobako-runtime/src/snapshot.rs
65
+ - crates/kobako-runtime/src/yielder.rs
66
+ - crates/kobako-wasmtime/CHANGELOG.md
67
+ - crates/kobako-wasmtime/Cargo.toml
68
+ - crates/kobako-wasmtime/README.md
69
+ - crates/kobako-wasmtime/src/ambient.rs
70
+ - crates/kobako-wasmtime/src/cache.rs
71
+ - crates/kobako-wasmtime/src/capture.rs
72
+ - crates/kobako-wasmtime/src/config.rs
73
+ - crates/kobako-wasmtime/src/dispatch.rs
74
+ - crates/kobako-wasmtime/src/driver.rs
75
+ - crates/kobako-wasmtime/src/exports.rs
76
+ - crates/kobako-wasmtime/src/frames.rs
77
+ - crates/kobako-wasmtime/src/guest_mem.rs
78
+ - crates/kobako-wasmtime/src/instance_pre.rs
79
+ - crates/kobako-wasmtime/src/invocation.rs
80
+ - crates/kobako-wasmtime/src/lib.rs
81
+ - crates/kobako-wasmtime/src/trap.rs
57
82
  - data/kobako.wasm
58
83
  - ext/kobako/Cargo.toml
59
84
  - ext/kobako/extconf.rb
60
85
  - ext/kobako/src/lib.rs
61
86
  - ext/kobako/src/runtime.rs
62
- - ext/kobako/src/runtime/ambient.rs
63
- - ext/kobako/src/runtime/cache.rs
64
- - ext/kobako/src/runtime/capture.rs
65
- - ext/kobako/src/runtime/config.rs
66
- - ext/kobako/src/runtime/dispatch.rs
87
+ - ext/kobako/src/runtime/bridge.rs
67
88
  - ext/kobako/src/runtime/errors.rs
68
- - ext/kobako/src/runtime/exports.rs
69
- - ext/kobako/src/runtime/frames.rs
70
- - ext/kobako/src/runtime/guest_mem.rs
71
- - ext/kobako/src/runtime/instance_pre.rs
72
- - ext/kobako/src/runtime/invocation.rs
73
- - ext/kobako/src/runtime/trap.rs
74
89
  - ext/kobako/src/snapshot.rs
75
90
  - lib/kobako.rb
76
91
  - lib/kobako/capture.rb
@@ -1,25 +0,0 @@
1
- //! Per-`Runtime` execution configuration.
2
- //!
3
- //! The wall-clock and per-channel capture caps a `Kobako::Sandbox`
4
- //! forwards into `Runtime::from_path`. A plain value carrier owned by the
5
- //! `Runtime` — distinct from the process-wide engine/module `super::cache`
6
- //! (which is shared across every Sandbox) and from the per-invocation
7
- //! `super::invocation::Invocation` (which the wasm engine mutates from
8
- //! inside a run). These caps are read only by `Runtime` methods between
9
- //! runs, so they live here.
10
-
11
- use std::time::Duration;
12
-
13
- /// Wall-clock and output caps for one `Runtime`. `None` on any field
14
- /// disables that cap.
15
- pub(crate) struct Config {
16
- /// Wall-clock cap for one guest `#eval` / `#run`. Stamped into a
17
- /// per-run `Instant` deadline by `Runtime::prime_caps`.
18
- pub(crate) timeout: Option<Duration>,
19
- /// Byte cap for guest stdout capture.
20
- /// Sizes the per-run `MemoryOutputPipe` and computes the truncation
21
- /// flag in `Runtime::build_snapshot`.
22
- pub(crate) stdout_limit_bytes: Option<usize>,
23
- /// Byte cap for guest stderr capture. Mirror of `stdout_limit_bytes`.
24
- pub(crate) stderr_limit_bytes: Option<usize>,
25
- }
@@ -1,211 +0,0 @@
1
- //! Host-side dispatch for the `__kobako_dispatch` import.
2
- //!
3
- //! When the guest invokes the wasm import declared in
4
- //! `wasm/kobako-core/src/abi.rs`, wasmtime calls back into the host
5
- //! through the closure registered by `instance_pre::build_linker`.
6
- //! That closure delegates here. The dispatcher:
7
- //!
8
- //! 1. Reads the Request bytes from guest linear memory.
9
- //! 2. Invokes the Ruby-side dispatch Proc bound via
10
- //! `Runtime#on_dispatch=` and recovers Response bytes.
11
- //! 3. Allocates a guest buffer via `__kobako_alloc(len)` invoked
12
- //! through `Caller::get_export`.
13
- //! 4. Writes the Response bytes into the guest buffer.
14
- //! 5. Returns packed `(ptr<<32)|len` for the guest to decode.
15
- //!
16
- //! Returns 0 on any step failure. `Kobako::Sandbox#initialize` always
17
- //! installs the dispatch Proc before any invocation, so reaching the
18
- //! dispatcher with no Proc bound is itself a wire-layer fault; the
19
- //! guest maps a 0 return to a trap. Failures during normal dispatch
20
- //! surface as Response.err envelopes from
21
- //! `Kobako::Transport::Dispatcher.dispatch` itself — they never reach
22
- //! this 0-return path.
23
- //!
24
- //! ## Why this module writes to `stderr`
25
- //!
26
- //! This file is the one place in `ext/` that deliberately prints
27
- //! through `eprintln!`. The host normally surfaces faults by
28
- //! raising a `MagnusError` back into Ruby; the dispatcher contract
29
- //! is the exception — it must return a packed `i64` to the guest
30
- //! and cannot raise, so a 0 return is the only signal the wasm side
31
- //! receives. The guest collapses every 0 into the same trap, so the
32
- //! Ruby host has no way to attribute the failure to a specific step
33
- //! (missing `memory` export vs. no dispatch Proc bound vs. the Proc
34
- //! raised vs. `__kobako_alloc` returned 0 vs. `memory.write`
35
- //! rejected).
36
- //!
37
- //! `handle` writes a single `[kobako-dispatch] <reason>` line to
38
- //! `stderr` on each failure path so operators have a breadcrumb to
39
- //! correlate the trap with the actual cause. The line is emitted in
40
- //! both debug and release builds on purpose: dispatcher failures are
41
- //! wire-layer faults rather than expected error paths (`Kobako::Sandbox`
42
- //! always installs the Proc, the Proc is contracted never to raise,
43
- //! etc.), so the "release-build noise" cost is bounded — under normal
44
- //! operation the line is never written. Operators that need to silence
45
- //! the stream can redirect the host process's stderr, but the kobako
46
- //! convention is "ext never logs" plus this single, named exception.
47
-
48
- use core::cell::Cell;
49
- use core::ptr::NonNull;
50
-
51
- use magnus::value::{Opaque, ReprValue};
52
- use magnus::{Error as MagnusError, RString, Ruby, Value};
53
- use wasmtime::Caller;
54
-
55
- use super::invocation::Invocation;
56
-
57
- // ============================================================
58
- // Active-caller pointer for the per-thread Invocation slot
59
- // (SPEC.md Single-Invocation Slot).
60
- // ============================================================
61
- //
62
- // `Runtime#yield_to_active_invocation` (whose body is the
63
- // `__kobako_yield_to_block` guest export) runs synchronously inside a
64
- // Ruby Service callback that itself was invoked from inside this
65
- // dispatcher — at that moment we are several stack frames deep in
66
- // `try_handle`, with the original `&mut Caller<'_, Invocation>` parked
67
- // unused on the Rust stack while Ruby code is running. The yield path
68
- // needs the same Caller to call the guest export, but the Rust borrow
69
- // type is non-`'static` so it cannot be stored on the `Invocation`
70
- // struct directly (the `&mut Caller` outlives no struct field — its
71
- // lifetime ends when `handle` returns to wasmtime).
72
- //
73
- // The pointer is therefore erased to `NonNull<()>` and parked in a
74
- // per-thread slot — the materialised form of the SPEC.md
75
- // "Single-Invocation Slot" invariant. The single-threaded wasm
76
- // execution per Sandbox plus the LIFO re-entry shape of nested
77
- // dispatch frames ensures no aliasing across threads or across
78
- // frames; the recovery invariant lives at `current_caller`. The
79
- // pointer is set on entry to `handle` and restored to the outer
80
- // frame's value on every exit through a drop guard.
81
-
82
- thread_local! {
83
- static ACTIVE_CALLER: Cell<Option<NonNull<()>>> = const { Cell::new(None) };
84
- }
85
-
86
- /// RAII guard that saves the previous `ACTIVE_CALLER` value on
87
- /// installation and restores it on drop. Nested `__kobako_dispatch`
88
- /// frames stack within one Invocation — the inner frame's `set`
89
- /// swaps in its own pointer while remembering the outer's; drop
90
- /// restores the outer so its continuation (e.g. iterating over another
91
- /// guest block) still finds a live caller.
92
- pub(crate) struct CallerGuard {
93
- previous: Option<NonNull<()>>,
94
- }
95
-
96
- impl CallerGuard {
97
- fn set(ptr: NonNull<()>) -> Self {
98
- let previous = ACTIVE_CALLER.with(|c| c.replace(Some(ptr)));
99
- Self { previous }
100
- }
101
- }
102
-
103
- impl Drop for CallerGuard {
104
- fn drop(&mut self) {
105
- ACTIVE_CALLER.with(|c| c.set(self.previous));
106
- }
107
- }
108
-
109
- /// Recover the active `&mut Caller<'_, Invocation>` set by the
110
- /// enclosing `handle` frame. Returns `None` when no dispatch frame is
111
- /// active on this thread.
112
- ///
113
- /// # Safety
114
- ///
115
- /// The returned reference aliases the original `&mut Caller` borrow
116
- /// held on the Rust stack inside `handle`'s enclosing frame. The
117
- /// original borrow is logically inactive while Ruby code is running
118
- /// (it is parked on the stack between `invoke_on_dispatch` and the
119
- /// eventual `funcall` return), and the SPEC.md Single-Invocation Slot
120
- /// invariant (one Invocation per OS thread for the duration of any
121
- /// invocation) guarantees no other Rust frame can observe it. Callers
122
- /// must not retain the returned `&mut` past the synchronous Ruby
123
- /// callback that requested it — i.e. only use it inside one short
124
- /// magnus method body and let the borrow end before the method returns.
125
- pub(crate) fn current_caller<'a>() -> Option<&'a mut Caller<'a, Invocation>> {
126
- let raw: NonNull<()> = ACTIVE_CALLER.with(|c| c.get())?;
127
- // SAFETY: see item doc.
128
- Some(unsafe { &mut *raw.as_ptr().cast::<Caller<'a, Invocation>>() })
129
- }
130
-
131
- /// Drive a single `__kobako_dispatch` invocation end-to-end. Entry point
132
- /// from the wasmtime closure registered by `instance_pre::build_linker`.
133
- ///
134
- /// Returns the packed `(ptr<<32)|len` u64 on success, 0 on any
135
- /// wire-layer fault. Failure paths log a `[kobako-dispatch]` line to
136
- /// `stderr` so operators have a breadcrumb when the guest sees a 0
137
- /// return and traps. The bound dispatch Proc is contracted never to
138
- /// raise (it folds Service exceptions into Response.err envelopes),
139
- /// so reaching the failure path is always a wiring bug or wire-layer
140
- /// fault rather than an expected path.
141
- pub(crate) fn handle(caller: &mut Caller<'_, Invocation>, req_ptr: i32, req_len: i32) -> i64 {
142
- // SAFETY: lifetime erased to `NonNull<()>` per the module's
143
- // Invocation-slot doc. The pointer is restored by `_caller_guard`
144
- // before this function returns, and only
145
- // `Runtime#yield_to_active_invocation` (running inside a Ruby
146
- // callback we are about to invoke) reads it through `current_caller`.
147
- let ptr: NonNull<()> = NonNull::from(&mut *caller).cast();
148
- let _caller_guard = CallerGuard::set(ptr);
149
-
150
- match try_handle(caller, req_ptr, req_len) {
151
- Ok(packed) => packed,
152
- Err(reason) => {
153
- eprintln!("[kobako-dispatch] {}", reason);
154
- 0
155
- }
156
- }
157
- }
158
-
159
- /// Result-returning core of `handle`. Pulled out so each early
160
- /// failure path carries a diagnostic string instead of an opaque 0.
161
- fn try_handle(
162
- caller: &mut Caller<'_, Invocation>,
163
- req_ptr: i32,
164
- req_len: i32,
165
- ) -> Result<i64, &'static str> {
166
- let req_bytes = super::guest_mem::read(caller, req_ptr, req_len)?;
167
-
168
- // `Kobako::Sandbox` always installs the dispatch Proc before
169
- // invoking the runtime, so reaching this branch indicates a misuse
170
- // rather than a normal control path.
171
- let on_dispatch = caller
172
- .data()
173
- .on_dispatch()
174
- .ok_or("a Sandbox callback fired outside an active Sandbox#run — please report this as a kobako bug")?;
175
-
176
- let resp_bytes = invoke_on_dispatch(on_dispatch, &req_bytes).map_err(|_| {
177
- "a Sandbox callback raised an exception instead of returning a fault — please report this as a kobako bug"
178
- })?;
179
-
180
- write_response(caller, &resp_bytes)
181
- }
182
-
183
- /// Invoke the Ruby-side dispatch `Proc` with the request bytes and return
184
- /// the encoded Response bytes. The Proc is contracted to fold every
185
- /// dispatch failure into a `Response.err` envelope (see
186
- /// `Kobako::Transport::Dispatcher.dispatch`), so reaching the error
187
- /// branch is itself a wire-layer fault rather than a normal control path.
188
- fn invoke_on_dispatch(
189
- on_dispatch: Opaque<Value>,
190
- req_bytes: &[u8],
191
- ) -> Result<Vec<u8>, MagnusError> {
192
- // The wasmtime callback runs on the same Ruby thread that called the
193
- // active Sandbox invocation (#eval or #run) — the invariant SPEC
194
- // Implementation Standards Architecture pins for the host gem — so
195
- // `Ruby::get()` is always available here. Panicking with `expect`
196
- // localises the violation rather than letting a nonsense error
197
- // propagate.
198
- let ruby = Ruby::get().expect("Ruby handle unavailable in __kobako_dispatch");
199
- let proc_value: Value = ruby.get_inner(on_dispatch);
200
- let req_str = ruby.str_from_slice(req_bytes);
201
- let resp: RString = proc_value.funcall("call", (req_str,))?;
202
- Ok(super::rstring_to_vec(resp))
203
- }
204
-
205
- /// Allocate a guest-side buffer and copy the response bytes into it via
206
- /// `super::guest_mem::alloc_and_write`, returning the packed
207
- /// `(ptr<<32)|len` u64 the guest's `__kobako_dispatch` import expects.
208
- fn write_response(caller: &mut Caller<'_, Invocation>, bytes: &[u8]) -> Result<i64, &'static str> {
209
- let ptr = super::guest_mem::alloc_and_write(caller, bytes)?;
210
- Ok(((ptr as i64) << 32) | (bytes.len() as i64))
211
- }