kobako 0.19.0 → 0.20.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +20 -0
  4. data/Cargo.lock +6 -5
  5. data/crates/kobako-runtime/CHANGELOG.md +7 -0
  6. data/crates/kobako-runtime/Cargo.toml +1 -1
  7. data/crates/kobako-runtime/README.md +1 -1
  8. data/crates/kobako-wasmtime/CHANGELOG.md +7 -0
  9. data/crates/kobako-wasmtime/Cargo.toml +2 -2
  10. data/crates/kobako-wasmtime/README.md +1 -1
  11. data/data/kobako.wasm +0 -0
  12. data/ext/kobako/Cargo.toml +6 -2
  13. data/ext/kobako/src/runtime/bridge.rs +30 -24
  14. data/ext/kobako/src/runtime/gvl.rs +114 -0
  15. data/ext/kobako/src/runtime.rs +211 -190
  16. data/lib/kobako/catalog/extensions.rb +50 -43
  17. data/lib/kobako/catalog/handles.rb +7 -16
  18. data/lib/kobako/catalog/services.rb +22 -36
  19. data/lib/kobako/catalog/snippets.rb +11 -10
  20. data/lib/kobako/context.rb +202 -0
  21. data/lib/kobako/errors.rb +26 -4
  22. data/lib/kobako/execution.rb +52 -0
  23. data/lib/kobako/extension.rb +24 -10
  24. data/lib/kobako/pool.rb +1 -7
  25. data/lib/kobako/sandbox.rb +43 -182
  26. data/lib/kobako/sandbox_options.rb +23 -2
  27. data/lib/kobako/transport/dispatcher.rb +16 -15
  28. data/lib/kobako/unresolved.rb +17 -0
  29. data/lib/kobako/usage.rb +6 -6
  30. data/lib/kobako/version.rb +1 -1
  31. data/lib/kobako.rb +1 -0
  32. data/sig/kobako/catalog/extensions.rbs +4 -6
  33. data/sig/kobako/catalog/handles.rbs +0 -2
  34. data/sig/kobako/catalog/services.rbs +3 -3
  35. data/sig/kobako/catalog/snippets.rbs +2 -0
  36. data/sig/kobako/context.rbs +36 -0
  37. data/sig/kobako/errors.rbs +12 -3
  38. data/sig/kobako/execution.rbs +24 -0
  39. data/sig/kobako/extension.rbs +4 -2
  40. data/sig/kobako/runtime.rbs +37 -9
  41. data/sig/kobako/sandbox.rbs +5 -24
  42. data/sig/kobako/sandbox_options.rbs +9 -2
  43. data/sig/kobako/transport/dispatcher.rbs +3 -3
  44. data/sig/kobako/unresolved.rbs +4 -0
  45. metadata +8 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 890cacf8f9985d53603b280d11bcf5283da99c4bc5bf3eb0249ded25b9b8f4b5
4
- data.tar.gz: e0303f7fe91a066a12e792094842325d1dc968f674ccc3bc9b05a82988adbbe8
3
+ metadata.gz: 3c3d1c75e0967f01b1284848372d66adda8cc65566b828bb1634f1624b1b4938
4
+ data.tar.gz: 223e0cc856970daf5c702775c1a3df2851d96beb72db1793e24c776747205a0c
5
5
  SHA512:
6
- metadata.gz: c573074f4ef2cd26c8221f87a6fd3cb3ca50aa7738c28df7965ece8a8fc6953d29b5651acc97bfb10094ffb14dea6ad401f55a66205920e15af6e6dba9d3cdbd
7
- data.tar.gz: 3d625d01a1fb1ca14213b20de469ffd095d49257553bfcf4b1960cd878675d5e48099dc058f723d77a6f0eb66363b15af690f7750896344d8e47c25b019490c4
6
+ metadata.gz: 4b6050756d4ed75616ea4aa82fe56febc11cc97c8b1cba165c607e70b6921dfad322c1594746b3e105338653fce93f5e208b4f2516c39f2f3cb4d9b85647b79b
7
+ data.tar.gz: 8c50704f755219097efdd41c5e47ba304fa98fe8843e5f39ee49df4c22cac7d7916cd8a96be472ffae3535d4f285422e0cca81917c722584b44165f262611212
@@ -1 +1 @@
1
- {".":"0.19.0","wasm/kobako-core":"0.11.0","wasm/kobako-mruby":"0.11.0","wasm/kobako-io":"0.11.0","wasm/kobako-json":"0.11.0","wasm/kobako-regexp":"0.11.0","wasm/kobako-baker":"0.11.0","crates/kobako-codec":"0.11.0","crates/kobako-runtime":"0.11.0","crates/kobako-wasmtime":"0.11.0","crates/kobako":"0.11.0"}
1
+ {".":"0.20.0","wasm/kobako-core":"0.12.0","wasm/kobako-mruby":"0.12.0","wasm/kobako-io":"0.12.0","wasm/kobako-json":"0.12.0","wasm/kobako-regexp":"0.12.0","wasm/kobako-baker":"0.12.0","crates/kobako-codec":"0.12.0","crates/kobako-runtime":"0.12.0","crates/kobako-wasmtime":"0.12.0","crates/kobako":"0.12.0"}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.20.0](https://github.com/elct9620/kobako/compare/v0.19.0...v0.20.0) (2026-07-24)
4
+
5
+
6
+ ### Features
7
+
8
+ * **bench:** mark a run in progress with a pid lock file ([3ca85fb](https://github.com/elct9620/kobako/commit/3ca85fbe9860ce32d58c1a5bf4ff85cbdc210780))
9
+ * **execution:** tell a nil-value success from a failure with #failed? ([2f3d382](https://github.com/elct9620/kobako/commit/2f3d382b24d65fabda7771429cd3adfc90a8c7e8))
10
+ * **extension:** declare a backend's kind by explicit keyword ([3f6c628](https://github.com/elct9620/kobako/commit/3f6c6283080ba4346ac8ef878e469dab8039e650))
11
+ * **sandbox:** add the gvl: hold/release scheduling option ([ca2a884](https://github.com/elct9620/kobako/commit/ca2a884415b3271c44b20d131536b3fe7c5a3077))
12
+ * **sandbox:** declare fillable Service paths defaulting to Kobako::Unresolved ([1d60f76](https://github.com/elct9620/kobako/commit/1d60f764ddff3b5b10206993e2b281c838ec0022))
13
+ * **sandbox:** fill and override declared paths per invocation with ctx.bind ([629ef7d](https://github.com/elct9620/kobako/commit/629ef7dfa35273c952685fefb4c5793b11886aee))
14
+ * **sandbox:** return a frozen Kobako::Invocation from #eval and #run ([e9f4634](https://github.com/elct9620/kobako/commit/e9f463403bea26fad32e52d560b979684f555ed1))
15
+ * **tasks:** gate the wasmtime driver against a magnus dependency ([268c3f6](https://github.com/elct9620/kobako/commit/268c3f65f53d64b465458bca5c0c9b0d0467df44))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **context:** raise ArgumentError for a spent ctx.bind ([2783bba](https://github.com/elct9620/kobako/commit/2783bba20f139554178389d4c860e0c280cab550))
21
+ * **extension:** re-assert dependencies after a failed seal ([72839ba](https://github.com/elct9620/kobako/commit/72839bac6186e81a1d4323bd3f1eb4f10abf3d64))
22
+
3
23
  ## [0.19.0](https://github.com/elct9620/kobako/compare/v0.18.0...v0.19.0) (2026-07-19)
4
24
 
5
25
 
data/Cargo.lock CHANGED
@@ -43,9 +43,9 @@ dependencies = [
43
43
 
44
44
  [[package]]
45
45
  name = "anyhow"
46
- version = "1.0.103"
46
+ version = "1.0.104"
47
47
  source = "registry+https://github.com/rust-lang/crates.io-index"
48
- checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
48
+ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
49
49
 
50
50
  [[package]]
51
51
  name = "arbitrary"
@@ -929,20 +929,21 @@ dependencies = [
929
929
 
930
930
  [[package]]
931
931
  name = "kobako"
932
- version = "0.19.0"
932
+ version = "0.20.0"
933
933
  dependencies = [
934
934
  "kobako-runtime",
935
935
  "kobako-wasmtime",
936
936
  "magnus",
937
+ "rb-sys",
937
938
  ]
938
939
 
939
940
  [[package]]
940
941
  name = "kobako-runtime"
941
- version = "0.11.0"
942
+ version = "0.12.0"
942
943
 
943
944
  [[package]]
944
945
  name = "kobako-wasmtime"
945
- version = "0.11.0"
946
+ version = "0.12.0"
946
947
  dependencies = [
947
948
  "kobako-runtime",
948
949
  "libc",
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.12.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.11.0...kobako-runtime-v0.12.0) (2026-07-24)
4
+
5
+
6
+ ### Miscellaneous Chores
7
+
8
+ * **kobako-runtime:** Synchronize kobako crates versions
9
+
3
10
  ## [0.11.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.10.2...kobako-runtime-v0.11.0) (2026-07-19)
4
11
 
5
12
 
@@ -13,7 +13,7 @@
13
13
 
14
14
  [package]
15
15
  name = "kobako-runtime"
16
- version = "0.11.0"
16
+ version = "0.12.0"
17
17
  edition = "2021"
18
18
  description = "Engine-neutral host runtime contract for embedding kobako Wasm guests."
19
19
  license = "Apache-2.0"
@@ -26,7 +26,7 @@ map the neutral types onto their own language surface.
26
26
 
27
27
  ```toml
28
28
  [dependencies]
29
- kobako-runtime = "0.11.0" # x-release-please-version
29
+ kobako-runtime = "0.12.0" # x-release-please-version
30
30
  ```
31
31
 
32
32
  ## License
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.12.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.11.0...kobako-wasmtime-v0.12.0) (2026-07-24)
4
+
5
+
6
+ ### Miscellaneous Chores
7
+
8
+ * **kobako-wasmtime:** Synchronize kobako crates versions
9
+
3
10
  ## [0.11.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.10.2...kobako-wasmtime-v0.11.0) (2026-07-19)
4
11
 
5
12
 
@@ -15,7 +15,7 @@
15
15
 
16
16
  [package]
17
17
  name = "kobako-wasmtime"
18
- version = "0.11.0"
18
+ version = "0.12.0"
19
19
  edition = "2021"
20
20
  description = "wasmtime implementation of the kobako host runtime contract."
21
21
  license = "Apache-2.0"
@@ -28,7 +28,7 @@ categories = ["wasm", "virtualization"]
28
28
  # The engine-neutral contract this crate implements. The version pin
29
29
  # rides the linked release group; the path keeps in-tree builds (and
30
30
  # the Ruby gem, which ships both crates) resolving locally.
31
- kobako-runtime = { version = "0.11.0", path = "../kobako-runtime" }
31
+ kobako-runtime = { version = "0.12.0", path = "../kobako-runtime" }
32
32
  # wasmtime — host-side embedder for kobako.wasm. We disable default-features
33
33
  # and opt back in only what kobako needs: a Cranelift-backed runtime that can
34
34
  # compile a pre-built wasm32-wasip1 module on the host triple, plus the `wat`
@@ -24,7 +24,7 @@ SDK consumes the same surface.
24
24
 
25
25
  ```toml
26
26
  [dependencies]
27
- kobako-wasmtime = "0.11.0" # x-release-please-version
27
+ kobako-wasmtime = "0.12.0" # x-release-please-version
28
28
  ```
29
29
 
30
30
  ## License
data/data/kobako.wasm CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "kobako"
3
- version = "0.19.0"
3
+ version = "0.20.0"
4
4
  edition = "2021"
5
5
  authors = ["Aotokitsuruya <contact@aotoki.me>"]
6
6
  license = "Apache-2.0"
@@ -10,7 +10,11 @@ publish = false
10
10
  crate-type = ["cdylib"]
11
11
 
12
12
  [dependencies]
13
- magnus = { version = "0.8.2" }
13
+ magnus = { version = "0.8.2", features = ["rb-sys"] }
14
+ # Raw Ruby FFI for the GVL release/re-acquire calls magnus does not wrap
15
+ # (`rb_thread_call_without_gvl` / `rb_thread_call_with_gvl`). Pinned to the
16
+ # rb-sys magnus resolves so both share one binding instance.
17
+ rb-sys = { version = "0.9", default-features = false }
14
18
  # The engine-neutral host runtime contract plus its wasmtime driver —
15
19
  # the ext is a thin magnus shim over them. Pure path dependencies: both
16
20
  # crates ship inside the gem, so installs never resolve them against
@@ -104,11 +104,12 @@ impl GuestYielder {
104
104
  }
105
105
  }
106
106
 
107
- /// The Ruby-Proc bridge: a `DispatchHandler` backed by the host-side
108
- /// dispatch `Proc` registered through `Runtime#on_dispatch=`. This is the
109
- /// one place the dispatch seam touches `magnus`; the wasm runtime sees
110
- /// only the trait. The Proc is GC-rooted by `Runtime`'s `mark`; this
111
- /// struct holds an `Opaque` copy of the same handle.
107
+ /// The Ruby-Proc bridge: a `DispatchHandler` backed by the per-invocation
108
+ /// dispatch `Proc` handed to `Runtime#eval` / `#run` as a call argument.
109
+ /// This is the one place the dispatch seam touches `magnus`; the wasm
110
+ /// runtime sees only the trait. The Proc stays GC-rooted as a live argument
111
+ /// on the Ruby stack for the synchronous call; this struct holds an
112
+ /// `Opaque` copy of the same handle.
112
113
  pub(super) struct RubyDispatchHandler {
113
114
  on_dispatch: Opaque<Value>,
114
115
  }
@@ -127,24 +128,29 @@ impl DispatchHandler for RubyDispatchHandler {
127
128
  /// violation surfaced as `None` — the dispatcher then walks the
128
129
  /// 0-return wire-fault path.
129
130
  fn dispatch(&self, request: &[u8], yielder: &mut dyn Yielder) -> Option<Vec<u8>> {
130
- // The wasmtime callback runs on the same Ruby thread that called
131
- // the active Sandbox invocation (#eval or #run) the invariant
132
- // SPEC Implementation Standards Architecture pins for the host gem
133
- // — so `Ruby::get()` is always available here. Panicking with
134
- // `expect` localises the violation rather than letting a nonsense
135
- // error propagate.
136
- let ruby = Ruby::get().expect("Ruby handle unavailable in __kobako_dispatch");
137
- let proc_value: Value = ruby.get_inner(self.on_dispatch);
138
- let req_str = ruby.str_from_slice(request);
139
- // Hand the Proc a frame-scoped yielder object as its second arg and
140
- // invalidate it the instant the Proc returns, so a guest block that
141
- // escapes the dispatch frame can never deref the freed stack
142
- // pointer. `guest_yielder` holds no Ruby Value, so it needs no GC
143
- // mark the GC has nothing to trace through it.
144
- let guest_yielder = ruby.obj_wrap(GuestYielder::new(yielder));
145
- let resp: Result<RString, magnus::Error> =
146
- proc_value.funcall("call", (req_str, guest_yielder));
147
- guest_yielder.invalidate();
148
- resp.ok().map(super::rstring_to_vec)
131
+ // The guest may be running GVL-free (`gvl: :release`); re-acquire the
132
+ // GVL for the whole callback before touching any Ruby VALUE. In hold
133
+ // mode `reenter` runs the body inline the GVL is already held.
134
+ super::gvl::reenter(|| {
135
+ // The wasmtime callback runs on the same Ruby thread that called
136
+ // the active Sandbox invocation (#eval or #run) — the invariant
137
+ // SPEC Implementation Standards Architecture pins for the host gem
138
+ // so `Ruby::get()` is available here once the GVL is held.
139
+ // Panicking with `expect` localises the violation rather than
140
+ // letting a nonsense error propagate.
141
+ let ruby = Ruby::get().expect("Ruby handle unavailable in __kobako_dispatch");
142
+ let proc_value: Value = ruby.get_inner(self.on_dispatch);
143
+ let req_str = ruby.str_from_slice(request);
144
+ // Hand the Proc a frame-scoped yielder object as its second arg and
145
+ // invalidate it the instant the Proc returns, so a guest block that
146
+ // escapes the dispatch frame can never deref the freed stack
147
+ // pointer. `guest_yielder` holds no Ruby Value, so it needs no GC
148
+ // mark — the GC has nothing to trace through it.
149
+ let guest_yielder = ruby.obj_wrap(GuestYielder::new(yielder));
150
+ let resp: Result<RString, magnus::Error> =
151
+ proc_value.funcall("call", (req_str, guest_yielder));
152
+ guest_yielder.invalidate();
153
+ resp.ok().map(super::rstring_to_vec)
154
+ })
149
155
  }
150
156
  }
@@ -0,0 +1,114 @@
1
+ //! Releasing Ruby's GVL for the guest-execution span so distinct Sandboxes on
2
+ //! distinct Threads run their guest code in parallel.
3
+ //!
4
+ //! `region` brackets one invocation's guest span, releasing the GVL when the
5
+ //! Sandbox asks for it; `reenter` re-acquires the GVL for a guest→host dispatch
6
+ //! callback, which is ordinary Ruby and must hold the lock. A thread-local flag
7
+ //! stops a nested dispatch from re-acquiring a second time, and both directions
8
+ //! run their closure through a `catch_unwind` trampoline because a panic must
9
+ //! never cross the C frame of `rb_thread_call_without_gvl` /
10
+ //! `rb_thread_call_with_gvl` (undefined behaviour). The flag is per-thread,
11
+ //! matching the one-thread-one-Sandbox contract, and is restored through a drop
12
+ //! guard so an unwinding panic cannot leave it corrupt for the next invocation.
13
+
14
+ use std::cell::Cell;
15
+ use std::os::raw::c_void;
16
+ use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
17
+ use std::ptr;
18
+
19
+ thread_local! {
20
+ // True while this thread sits inside a region whose GVL we released and
21
+ // have not yet re-acquired. Drives `reenter`'s one-shot re-acquire.
22
+ static GVL_RELEASED: Cell<bool> = const { Cell::new(false) };
23
+ }
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
+ pub(crate) fn region<F, R>(release: bool, f: F) -> R
28
+ where
29
+ F: FnOnce() -> R,
30
+ {
31
+ if !release {
32
+ return f();
33
+ }
34
+ let _guard = ReleasedGuard::set(true);
35
+ call_via(f, |func, data| unsafe {
36
+ rb_sys::rb_thread_call_without_gvl(Some(func), data, None, ptr::null_mut())
37
+ })
38
+ }
39
+
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.
44
+ pub(crate) fn reenter<F, R>(f: F) -> R
45
+ where
46
+ F: FnOnce() -> R,
47
+ {
48
+ if !GVL_RELEASED.with(|g| g.get()) {
49
+ return f();
50
+ }
51
+ let _guard = ReleasedGuard::set(false);
52
+ call_via(f, |func, data| unsafe {
53
+ rb_sys::rb_thread_call_with_gvl(Some(func), data)
54
+ })
55
+ }
56
+
57
+ /// Restores `GVL_RELEASED` to its prior value on drop, so an unwinding panic
58
+ /// through a released region cannot leave the flag stuck.
59
+ struct ReleasedGuard {
60
+ prev: bool,
61
+ }
62
+
63
+ impl ReleasedGuard {
64
+ fn set(value: bool) -> Self {
65
+ Self {
66
+ prev: GVL_RELEASED.with(|g| g.replace(value)),
67
+ }
68
+ }
69
+ }
70
+
71
+ impl Drop for ReleasedGuard {
72
+ fn drop(&mut self) {
73
+ GVL_RELEASED.with(|g| g.set(self.prev));
74
+ }
75
+ }
76
+
77
+ struct GvlCtx<F, R> {
78
+ f: Option<F>,
79
+ result: Option<std::thread::Result<R>>,
80
+ }
81
+
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`.
85
+ unsafe extern "C" fn trampoline<F, R>(data: *mut c_void) -> *mut c_void
86
+ where
87
+ F: FnOnce() -> R,
88
+ {
89
+ let ctx = unsafe { &mut *data.cast::<GvlCtx<F, R>>() };
90
+ let f = ctx.f.take().expect("gvl trampoline runs its closure once");
91
+ ctx.result = Some(catch_unwind(AssertUnwindSafe(f)));
92
+ ptr::null_mut()
93
+ }
94
+
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
+ fn call_via<F, R>(
99
+ f: F,
100
+ invoke: impl FnOnce(unsafe extern "C" fn(*mut c_void) -> *mut c_void, *mut c_void) -> *mut c_void,
101
+ ) -> R
102
+ where
103
+ F: FnOnce() -> R,
104
+ {
105
+ let mut ctx = GvlCtx::<F, R> {
106
+ f: Some(f),
107
+ result: None,
108
+ };
109
+ invoke(trampoline::<F, R>, ptr::addr_of_mut!(ctx).cast::<c_void>());
110
+ match ctx.result.take().expect("gvl trampoline stores a result") {
111
+ Ok(value) => value,
112
+ Err(payload) => resume_unwind(payload),
113
+ }
114
+ }