kobako 0.21.1 → 0.22.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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +21 -0
- data/Cargo.lock +4 -4
- data/README.md +9 -9
- data/crates/kobako-runtime/CHANGELOG.md +7 -0
- data/crates/kobako-runtime/Cargo.toml +2 -2
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-transport/CHANGELOG.md +13 -0
- data/crates/kobako-transport/Cargo.toml +1 -1
- data/crates/kobako-transport/README.md +1 -1
- data/crates/kobako-transport/src/envelope/bytes.rs +7 -0
- data/crates/kobako-transport/src/envelope/error_record.rs +1 -2
- data/crates/kobako-transport/src/envelope/fault.rs +72 -23
- data/crates/kobako-transport/src/envelope/reply.rs +3 -5
- data/crates/kobako-wasmtime/CHANGELOG.md +7 -0
- data/crates/kobako-wasmtime/Cargo.toml +3 -3
- data/crates/kobako-wasmtime/README.md +1 -1
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/lib/kobako/codec/encoder.rb +14 -0
- data/lib/kobako/codec/error.rb +2 -1
- data/lib/kobako/errors.rb +34 -1
- data/lib/kobako/outcome.rb +17 -9
- data/lib/kobako/transport/dispatcher.rb +63 -10
- data/lib/kobako/transport/yielder.rb +40 -4
- data/lib/kobako/version.rb +1 -1
- data/sig/kobako/errors.rbs +13 -0
- data/sig/kobako/outcome.rbs +1 -1
- data/sig/kobako/transport/dispatcher.rbs +8 -1
- data/sig/kobako/transport/yielder.rbs +7 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 771a65c05a0d065db98f2644a978e4f82ab0cedb453f3cb7f6e61aea936be707
|
|
4
|
+
data.tar.gz: 330ab0d4762778e9608325be5b6fc737eeb82e0ff1b4d30eeac29ff8fac9902b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6fd6084f9566a7f9466f8b58f308914dae445c2ee43ed4cd1ccd721db01d609b99257f404e0ff00ae6e5fb3bbac45e453afc933eb1d3b23210df709ca9914962
|
|
7
|
+
data.tar.gz: c113d2ef16f413ad41c4d97cb3d5675d4f2ac151879257ffc9f3d918a27f471667d936c7fe13120d9a78e73ab6fee041700e3bed8f3315c9d680d742213014ac
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{".":"0.
|
|
1
|
+
{".":"0.22.0","wasm/kobako-core":"0.14.0","wasm/kobako-mruby":"0.14.0","wasm/kobako-io":"0.14.0","wasm/kobako-json":"0.14.0","wasm/kobako-regexp":"0.14.0","wasm/kobako-baker":"0.14.0","crates/kobako-transport":"0.14.0","crates/kobako-codec":"0.14.0","crates/kobako-runtime":"0.14.0","crates/kobako-wasmtime":"0.14.0","crates/kobako":"0.14.0"}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.22.0](https://github.com/elct9620/kobako/compare/v0.21.1...v0.22.0) (2026-08-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **spec:** give the ubiquitous language a place N-6 can be checked from ([ab44dba](https://github.com/elct9620/kobako/commit/ab44dba0b08e558a5b96b95b3c4d1c70bf6bc7ad))
|
|
9
|
+
* **transport:** give a failed Service call a class that says why ([b39f176](https://github.com/elct9620/kobako/commit/b39f1761e9922b7ac761571393f9a6f4478ae522))
|
|
10
|
+
* **transport:** let a guest block's exception continue as itself ([c8b1de7](https://github.com/elct9620/kobako/commit/c8b1de7154336fab8fa20c0d7a5b3baaad11d551))
|
|
11
|
+
* **transport:** separate a failed exchange from a failed Service ([fb92515](https://github.com/elct9620/kobako/commit/fb92515fc76ffebbb8491b525d53b1725cf97dd0))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **codec:** keep a value the packer cannot walk inside the codec taxonomy ([ac99d58](https://github.com/elct9620/kobako/commit/ac99d58f0c9a274b230d645d7c37aba88516eab2))
|
|
17
|
+
* **regexp:** stop raising where MRI answers no match ([d229974](https://github.com/elct9620/kobako/commit/d2299740a2a0206c88e7c397cdb72dc730c4c1cf))
|
|
18
|
+
* **transport:** answer a held block failure only to the block that raised ([24dfd6e](https://github.com/elct9620/kobako/commit/24dfd6ec999e0ba1587b9c69da6d2b29563fc875))
|
|
19
|
+
* **transport:** report a yield the host cannot write as the Service's ([19459f8](https://github.com/elct9620/kobako/commit/19459f87555190043127290441bf6b95bcad93d6))
|
|
20
|
+
* **transport:** report an answer the host cannot write as the Service's ([bff4cdc](https://github.com/elct9620/kobako/commit/bff4cdccc076859d7da43f5dedb9a283ffed94ee))
|
|
21
|
+
* **transport:** spend a block's failure when its Service yields again ([3042a72](https://github.com/elct9620/kobako/commit/3042a72b9e3bb0d6a1d5cb5734d7eaee6a669e85))
|
|
22
|
+
* **wire:** let a Fault reader survive what it predates ([eeabfb7](https://github.com/elct9620/kobako/commit/eeabfb7aa4de3d08852ca1b41517b924d72a7312))
|
|
23
|
+
|
|
3
24
|
## [0.21.1](https://github.com/elct9620/kobako/compare/v0.21.0...v0.21.1) (2026-07-30)
|
|
4
25
|
|
|
5
26
|
|
data/Cargo.lock
CHANGED
|
@@ -858,7 +858,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|
|
858
858
|
|
|
859
859
|
[[package]]
|
|
860
860
|
name = "kobako"
|
|
861
|
-
version = "0.
|
|
861
|
+
version = "0.22.0"
|
|
862
862
|
dependencies = [
|
|
863
863
|
"kobako-runtime",
|
|
864
864
|
"kobako-transport",
|
|
@@ -869,18 +869,18 @@ dependencies = [
|
|
|
869
869
|
|
|
870
870
|
[[package]]
|
|
871
871
|
name = "kobako-runtime"
|
|
872
|
-
version = "0.
|
|
872
|
+
version = "0.14.0"
|
|
873
873
|
dependencies = [
|
|
874
874
|
"kobako-transport",
|
|
875
875
|
]
|
|
876
876
|
|
|
877
877
|
[[package]]
|
|
878
878
|
name = "kobako-transport"
|
|
879
|
-
version = "0.
|
|
879
|
+
version = "0.14.0"
|
|
880
880
|
|
|
881
881
|
[[package]]
|
|
882
882
|
name = "kobako-wasmtime"
|
|
883
|
-
version = "0.
|
|
883
|
+
version = "0.14.0"
|
|
884
884
|
dependencies = [
|
|
885
885
|
"kobako-runtime",
|
|
886
886
|
"kobako-transport",
|
data/README.md
CHANGED
|
@@ -321,7 +321,7 @@ For workloads that must be isolated from each other (one Sandbox per tenant, per
|
|
|
321
321
|
|
|
322
322
|
For hosts that serve many short invocations, `Kobako::Pool` keeps a bounded set of warm, identically set-up Sandboxes and hands each one to a single exclusive holder at a time ([`docs/behavior/runtime.md`](docs/behavior/runtime.md) B-46..B-48). Construction forwards every `Sandbox.new` keyword verbatim; the optional block is the per-Sandbox setup window and runs exactly once per constructed Sandbox.
|
|
323
323
|
|
|
324
|
-
`Kobako::Pool` is experimental today and is best treated as a convenience for warm, pre-configured reuse rather than a throughput optimisation. B-49 bakes the shared boot state into the artifact and every dynamic script still compiles and runs per invocation, so all a pool actually saves is the
|
|
324
|
+
`Kobako::Pool` is experimental today and is best treated as a convenience for warm, pre-configured reuse rather than a throughput optimisation. B-49 bakes the shared boot state into the artifact and every dynamic script still compiles and runs per invocation, so all a pool actually saves is the host-side `Sandbox.new` — now under 3 µs, an order of magnitude below the invocation that follows it. For the workload kobako is built for — many small, short-lived Sandboxes running dynamic scripts — that is not a gain worth the coupling. What a Pool buys is warm setup and exclusive checkout, not isolation: a Sandbox holds no state from any run, so Threads sharing one are equally safe (see [Concurrency](#concurrency)).
|
|
325
325
|
|
|
326
326
|
```ruby
|
|
327
327
|
pool = Kobako::Pool.new(slots: 4) do |sandbox|
|
|
@@ -498,17 +498,17 @@ Order-of-magnitude figures on macOS arm64, Ruby 3.4.7, YJIT off. Absolute values
|
|
|
498
498
|
| Phase | Cost |
|
|
499
499
|
|--------------------------------------------------------------|-----------------------|
|
|
500
500
|
| First `Sandbox.new` ever for a Guest Binary (Module JIT, then disk-cached) | ~500 ms once per machine |
|
|
501
|
-
| First `Sandbox.new` in a fresh process (`.cwasm` cache warm) | ~3 ms one-time
|
|
502
|
-
| Subsequent `Sandbox.new` (caches warm) | ~
|
|
503
|
-
| Warm `#eval
|
|
504
|
-
| Warm `#run(:Entrypoint, ...)` dispatch | ~
|
|
505
|
-
| Service call amortized inside one invocation | ~
|
|
506
|
-
| Snippet replay per invocation | ~7.
|
|
501
|
+
| First `Sandbox.new` in a fresh process (`.cwasm` cache warm) | ~1.3 ms one-time |
|
|
502
|
+
| Subsequent `Sandbox.new` (caches warm) | ~3 µs |
|
|
503
|
+
| Warm `#eval` with one Service call on a reused Sandbox | ~71 µs |
|
|
504
|
+
| Warm `#run(:Entrypoint, ...)` dispatch | ~81 µs |
|
|
505
|
+
| Service call amortized inside one invocation | ~5.7 µs |
|
|
506
|
+
| Snippet replay per invocation | ~7.5 µs each |
|
|
507
507
|
| Per additional idle Sandbox (RSS) | ~1 KB |
|
|
508
508
|
|
|
509
|
-
The Cranelift JIT runs once per machine and gem version — the compiled artifact persists in a `.cwasm` disk cache, so later processes deserialize in milliseconds. An idle Sandbox holds no wasm instance (the canonical boot state is baked into the artifact and instantiated per invocation), which is why a thousand idle tenants cost ~33 MB total. Under the default `gvl: :hold`, wasm work is GVL-serialized: aggregate throughput stays around
|
|
509
|
+
The Cranelift JIT runs once per machine and gem version — the compiled artifact persists in a `.cwasm` disk cache, so later processes deserialize in milliseconds. An idle Sandbox holds no wasm instance (the canonical boot state is baked into the artifact and instantiated per invocation), which is why a thousand idle tenants cost ~33 MB total. Under the default `gvl: :hold`, wasm work is GVL-serialized: aggregate throughput stays around 16k `#eval`/s regardless of Thread count, though Ruby-side `#eval` setup still overlaps. Opting a Sandbox into `gvl: :release` lifts that ceiling for compute-bound scripts (see [Concurrency](#concurrency)). A +10% regression on any SPEC-mandated benchmark blocks release.
|
|
510
510
|
|
|
511
|
-
Regexp is an opt-in capability gem, excluded from the default binary and the gated set; its throughput is tracked in a separate non-gated characterization (`#11` in [`benchmark/README.md`](benchmark/README.md)). There `=~` (~5 µs/match) costs about 4× `match?` (~1.
|
|
511
|
+
Regexp is an opt-in capability gem, excluded from the default binary and the gated set; its throughput is tracked in a separate non-gated characterization (`#11` in [`benchmark/README.md`](benchmark/README.md)). There `=~` (~5 µs/match) costs about 4.5× `match?` (~1.1 µs), because `=~` eagerly builds the `MatchData` and match globals — prefer `match?` for boolean tests.
|
|
512
512
|
|
|
513
513
|
```bash
|
|
514
514
|
bundle exec rake bench # every gated regression benchmark (~5-8 min)
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.14.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.13.1...kobako-runtime-v0.14.0) (2026-08-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Miscellaneous Chores
|
|
7
|
+
|
|
8
|
+
* **kobako-runtime:** Synchronize kobako crates versions
|
|
9
|
+
|
|
3
10
|
## [0.13.1](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.13.0...kobako-runtime-v0.13.1) (2026-07-30)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
[package]
|
|
15
15
|
name = "kobako-runtime"
|
|
16
|
-
version = "0.
|
|
16
|
+
version = "0.14.0"
|
|
17
17
|
edition = "2021"
|
|
18
18
|
description = "Engine-neutral host runtime contract for embedding kobako Wasm guests."
|
|
19
19
|
license = "Apache-2.0"
|
|
@@ -27,4 +27,4 @@ categories = ["wasm", "virtualization"]
|
|
|
27
27
|
# in-tree builds (and the Ruby gem, which ships both crates) resolving
|
|
28
28
|
# locally.
|
|
29
29
|
[dependencies]
|
|
30
|
-
kobako-transport = { version = "0.
|
|
30
|
+
kobako-transport = { version = "0.14.0", path = "../kobako-transport" }
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.14.0](https://github.com/elct9620/kobako/compare/kobako-transport-v0.13.1...kobako-transport-v0.14.0) (2026-08-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **transport:** let a guest block's exception continue as itself ([c8b1de7](https://github.com/elct9620/kobako/commit/c8b1de7154336fab8fa20c0d7a5b3baaad11d551))
|
|
9
|
+
* **transport:** separate a failed exchange from a failed Service ([fb92515](https://github.com/elct9620/kobako/commit/fb92515fc76ffebbb8491b525d53b1725cf97dd0))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* **wire:** let a Fault reader survive what it predates ([eeabfb7](https://github.com/elct9620/kobako/commit/eeabfb7aa4de3d08852ca1b41517b924d72a7312))
|
|
15
|
+
|
|
3
16
|
## [0.13.1](https://github.com/elct9620/kobako/compare/kobako-transport-v0.13.0...kobako-transport-v0.13.1) (2026-07-30)
|
|
4
17
|
|
|
5
18
|
|
|
@@ -80,6 +80,13 @@ impl<'a> Reader<'a> {
|
|
|
80
80
|
&self.bytes[self.pos..]
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/// Consume whatever is left without reading it. The extension seam:
|
|
84
|
+
/// a field a later contract version appends is invisible to a reader
|
|
85
|
+
/// built before it, and must not read as a framing desync.
|
|
86
|
+
pub(crate) fn skip_unknown_fields(&mut self) {
|
|
87
|
+
self.pos = self.bytes.len();
|
|
88
|
+
}
|
|
89
|
+
|
|
83
90
|
/// Refuse anything left over. Used by envelopes whose last field is
|
|
84
91
|
/// self-delimiting, where trailing bytes signal a framing desync.
|
|
85
92
|
pub(crate) fn finish(self) -> Result<(), DecodeError> {
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
//! A block failure and an invocation failure carry the same three fields,
|
|
4
4
|
//! so the host re-raises from either without consulting a payload codec.
|
|
5
5
|
//! Distinct from a Fault, which travels the other way and is categorized
|
|
6
|
-
//! by
|
|
7
|
-
//! error.
|
|
6
|
+
//! by a reserved category name the guest maps to a proxy-side error.
|
|
8
7
|
|
|
9
8
|
use super::bytes::{Reader, Writer};
|
|
10
9
|
use super::DecodeError;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//! The host refusing or failing a Call.
|
|
2
2
|
//!
|
|
3
|
-
//! Every byte of a Fault is kobako's — a
|
|
3
|
+
//! Every byte of a Fault is kobako's — a category and a message —
|
|
4
4
|
//! so it rides the envelope and a guest reads a refusal with no payload
|
|
5
5
|
//! codec at all.
|
|
6
6
|
//!
|
|
@@ -15,15 +15,20 @@ use super::DecodeError;
|
|
|
15
15
|
const KIND_RUNTIME: u8 = 0;
|
|
16
16
|
const KIND_ARGUMENT: u8 = 1;
|
|
17
17
|
const KIND_UNDEFINED: u8 = 2;
|
|
18
|
+
const KIND_INTERNAL: u8 = 3;
|
|
19
|
+
const KIND_BLOCK: u8 = 4;
|
|
18
20
|
|
|
19
|
-
/// Which
|
|
21
|
+
/// Which failure a Fault reports.
|
|
20
22
|
///
|
|
21
|
-
///
|
|
22
|
-
///
|
|
23
|
-
///
|
|
24
|
-
///
|
|
25
|
-
///
|
|
23
|
+
/// The set grows, so a reader that meets a category it does not know
|
|
24
|
+
/// degrades rather than failing the exchange. `Undefined` stays
|
|
25
|
+
/// indistinguishable across its causes — an unbound path, an unknown
|
|
26
|
+
/// method, a rejected name — so a guest probing the surface learns
|
|
27
|
+
/// nothing from which refusal it got, which is also what makes it the
|
|
28
|
+
/// landing place for a category this reader predates: it attributes
|
|
29
|
+
/// nothing to the Service that a newer category might contradict.
|
|
26
30
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
31
|
+
#[non_exhaustive]
|
|
27
32
|
pub enum FaultKind {
|
|
28
33
|
/// A Ruby exception raised inside the Service method.
|
|
29
34
|
Runtime,
|
|
@@ -31,6 +36,18 @@ pub enum FaultKind {
|
|
|
31
36
|
Argument,
|
|
32
37
|
/// No such member or method.
|
|
33
38
|
Undefined,
|
|
39
|
+
/// The exchange itself failed, so no Service outcome exists to
|
|
40
|
+
/// report — the request was unreadable, or the host ran out of the
|
|
41
|
+
/// resources the exchange needs. Separate from `Runtime` because
|
|
42
|
+
/// retrying the same call against a working host would behave
|
|
43
|
+
/// differently, which is not true of a Service that raised.
|
|
44
|
+
Internal,
|
|
45
|
+
/// The caller's own block raised while the Service was yielding to
|
|
46
|
+
/// it, and the Service did not rescue it. The failure is the
|
|
47
|
+
/// caller's, so it carries no host detail: the caller still holds
|
|
48
|
+
/// the exception it raised and re-raises that, which is what the
|
|
49
|
+
/// same code would do without a Sandbox between the two frames.
|
|
50
|
+
Block,
|
|
34
51
|
}
|
|
35
52
|
|
|
36
53
|
impl FaultKind {
|
|
@@ -39,17 +56,20 @@ impl FaultKind {
|
|
|
39
56
|
FaultKind::Runtime => KIND_RUNTIME,
|
|
40
57
|
FaultKind::Argument => KIND_ARGUMENT,
|
|
41
58
|
FaultKind::Undefined => KIND_UNDEFINED,
|
|
59
|
+
FaultKind::Internal => KIND_INTERNAL,
|
|
60
|
+
FaultKind::Block => KIND_BLOCK,
|
|
42
61
|
}
|
|
43
62
|
}
|
|
44
63
|
|
|
45
|
-
|
|
64
|
+
/// Read a category from its tag, degrading one this reader predates
|
|
65
|
+
/// to the category that claims the least about what failed.
|
|
66
|
+
fn from_tag(tag: u8) -> Self {
|
|
46
67
|
match tag {
|
|
47
|
-
KIND_RUNTIME =>
|
|
48
|
-
KIND_ARGUMENT =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)),
|
|
68
|
+
KIND_RUNTIME => FaultKind::Runtime,
|
|
69
|
+
KIND_ARGUMENT => FaultKind::Argument,
|
|
70
|
+
KIND_INTERNAL => FaultKind::Internal,
|
|
71
|
+
KIND_BLOCK => FaultKind::Block,
|
|
72
|
+
_ => FaultKind::Undefined,
|
|
53
73
|
}
|
|
54
74
|
}
|
|
55
75
|
|
|
@@ -59,16 +79,20 @@ impl FaultKind {
|
|
|
59
79
|
FaultKind::Runtime => "runtime",
|
|
60
80
|
FaultKind::Argument => "argument",
|
|
61
81
|
FaultKind::Undefined => "undefined",
|
|
82
|
+
FaultKind::Internal => "internal",
|
|
83
|
+
FaultKind::Block => "block",
|
|
62
84
|
}
|
|
63
85
|
}
|
|
64
86
|
|
|
65
|
-
/// Read a category from the name a frontend uses, or `None` when
|
|
66
|
-
///
|
|
87
|
+
/// Read a category from the name a frontend uses, or `None` when this
|
|
88
|
+
/// build predates the name.
|
|
67
89
|
pub fn from_name(name: &str) -> Option<Self> {
|
|
68
90
|
match name {
|
|
69
91
|
"runtime" => Some(FaultKind::Runtime),
|
|
70
92
|
"argument" => Some(FaultKind::Argument),
|
|
71
93
|
"undefined" => Some(FaultKind::Undefined),
|
|
94
|
+
"internal" => Some(FaultKind::Internal),
|
|
95
|
+
"block" => Some(FaultKind::Block),
|
|
72
96
|
_ => None,
|
|
73
97
|
}
|
|
74
98
|
}
|
|
@@ -91,8 +115,12 @@ impl Fault {
|
|
|
91
115
|
}
|
|
92
116
|
|
|
93
117
|
pub(crate) fn read(reader: &mut Reader<'_>) -> Result<Self, DecodeError> {
|
|
94
|
-
let kind = FaultKind::from_tag(reader.u8()?)
|
|
118
|
+
let kind = FaultKind::from_tag(reader.u8()?);
|
|
95
119
|
let message = reader.text()?.to_owned();
|
|
120
|
+
// A Fault gains fields as the contract does, and it is the last
|
|
121
|
+
// thing a Reply carries, so whatever follows the fields this
|
|
122
|
+
// reader knows belongs to a later version of them.
|
|
123
|
+
reader.skip_unknown_fields();
|
|
96
124
|
Ok(Fault { kind, message })
|
|
97
125
|
}
|
|
98
126
|
|
|
@@ -111,6 +139,8 @@ mod tests {
|
|
|
111
139
|
FaultKind::Runtime,
|
|
112
140
|
FaultKind::Argument,
|
|
113
141
|
FaultKind::Undefined,
|
|
142
|
+
FaultKind::Internal,
|
|
143
|
+
FaultKind::Block,
|
|
114
144
|
] {
|
|
115
145
|
let fault = Fault::new(kind, "boom");
|
|
116
146
|
let mut w = Writer::new();
|
|
@@ -126,12 +156,29 @@ mod tests {
|
|
|
126
156
|
}
|
|
127
157
|
|
|
128
158
|
#[test]
|
|
129
|
-
fn
|
|
130
|
-
let encoded = vec![
|
|
159
|
+
fn a_kind_the_reader_does_not_know_degrades_to_undefined() {
|
|
160
|
+
let encoded = vec![0xfe, 0, 0, 0, 1, b'x'];
|
|
131
161
|
let mut r = Reader::new(&encoded);
|
|
132
|
-
|
|
133
|
-
Fault::read(&mut r)
|
|
134
|
-
|
|
162
|
+
assert_eq!(
|
|
163
|
+
Fault::read(&mut r),
|
|
164
|
+
Ok(Fault::new(FaultKind::Undefined, "x")),
|
|
165
|
+
"a Fault category added after this reader was built must deliver its message \
|
|
166
|
+
under the category that claims the least, not fail the exchange"
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#[test]
|
|
171
|
+
fn a_field_the_reader_does_not_know_is_skipped() {
|
|
172
|
+
let mut w = Writer::new();
|
|
173
|
+
Fault::new(FaultKind::Undefined, "no such method").write(&mut w);
|
|
174
|
+
w.bytes(b"a field a later contract version appended");
|
|
175
|
+
let encoded = w.into_bytes();
|
|
176
|
+
let mut r = Reader::new(&encoded);
|
|
177
|
+
assert_eq!(
|
|
178
|
+
Fault::read(&mut r),
|
|
179
|
+
Ok(Fault::new(FaultKind::Undefined, "no such method")),
|
|
180
|
+
"a Fault field added after this reader was built must be skipped, so the \
|
|
181
|
+
fields it does know still arrive"
|
|
135
182
|
);
|
|
136
183
|
}
|
|
137
184
|
|
|
@@ -155,6 +202,8 @@ mod tests {
|
|
|
155
202
|
FaultKind::Runtime,
|
|
156
203
|
FaultKind::Argument,
|
|
157
204
|
FaultKind::Undefined,
|
|
205
|
+
FaultKind::Internal,
|
|
206
|
+
FaultKind::Block,
|
|
158
207
|
] {
|
|
159
208
|
assert_eq!(
|
|
160
209
|
FaultKind::from_name(kind.name()),
|
|
@@ -165,7 +214,7 @@ mod tests {
|
|
|
165
214
|
assert_eq!(
|
|
166
215
|
FaultKind::from_name("other"),
|
|
167
216
|
None,
|
|
168
|
-
"a name
|
|
217
|
+
"a name this build predates must not resolve to a kind"
|
|
169
218
|
);
|
|
170
219
|
}
|
|
171
220
|
}
|
|
@@ -32,11 +32,9 @@ impl Reply {
|
|
|
32
32
|
let tag = reader.u8()?;
|
|
33
33
|
match tag {
|
|
34
34
|
TAG_OK => Ok(Reply::Ok(reader.remaining().to_vec())),
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Ok(Reply::Fault(fault))
|
|
39
|
-
}
|
|
35
|
+
// No `finish` here: a Fault owns everything after the tag,
|
|
36
|
+
// including the fields a later contract version appends.
|
|
37
|
+
TAG_FAULT => Ok(Reply::Fault(Fault::read(&mut reader)?)),
|
|
40
38
|
_ => Err(DecodeError::new("Reply tag must be 0 (ok) or 1 (fault)")),
|
|
41
39
|
}
|
|
42
40
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.14.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.13.1...kobako-wasmtime-v0.14.0) (2026-08-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Miscellaneous Chores
|
|
7
|
+
|
|
8
|
+
* **kobako-wasmtime:** Synchronize kobako crates versions
|
|
9
|
+
|
|
3
10
|
## [0.13.1](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.13.0...kobako-wasmtime-v0.13.1) (2026-07-30)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
[package]
|
|
17
17
|
name = "kobako-wasmtime"
|
|
18
|
-
version = "0.
|
|
18
|
+
version = "0.14.0"
|
|
19
19
|
edition = "2021"
|
|
20
20
|
description = "wasmtime implementation of the kobako host runtime contract."
|
|
21
21
|
license = "Apache-2.0"
|
|
@@ -28,10 +28,10 @@ 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.
|
|
31
|
+
kobako-runtime = { version = "0.14.0", path = "../kobako-runtime" }
|
|
32
32
|
# The fixed tier: the core envelope this driver shuttles and the ABI
|
|
33
33
|
# values it drives the exports against.
|
|
34
|
-
kobako-transport = { version = "0.
|
|
34
|
+
kobako-transport = { version = "0.14.0", path = "../kobako-transport" }
|
|
35
35
|
# wasmtime — host-side embedder for kobako.wasm. We disable default-features
|
|
36
36
|
# and opt back in only what kobako needs: a Cranelift-backed runtime that can
|
|
37
37
|
# compile a pre-built wasm32-wasip1 module on the host triple, plus the `wat`
|
data/data/kobako.wasm
CHANGED
|
Binary file
|
data/ext/kobako/Cargo.toml
CHANGED
data/lib/kobako/codec/encoder.rb
CHANGED
|
@@ -30,10 +30,24 @@ module Kobako
|
|
|
30
30
|
# rescue below maps the two violations the guard does not reach onto the
|
|
31
31
|
# same error: an integer outside i64..u64 (+RangeError+) and any
|
|
32
32
|
# packer-internal +NoMethodError+.
|
|
33
|
+
#
|
|
34
|
+
# A value that nests without bound — a reference cycle necessarily
|
|
35
|
+
# does — exhausts the packer's own recursion instead, which Ruby
|
|
36
|
+
# reports outside +StandardError+. Mapping it keeps an unwritable
|
|
37
|
+
# value a wire violation the dispatch boundary can answer, rather than
|
|
38
|
+
# one that escapes every caller's rescue and traps the invocation.
|
|
39
|
+
#
|
|
40
|
+
# The refusal is spent once per thread: a thread that has absorbed one
|
|
41
|
+
# such overflow aborts on the next instead of raising, and a Hash cycle
|
|
42
|
+
# never reaches Ruby at all — the packer walks a Hash through C frames
|
|
43
|
+
# that carry no stack guard. Bounding the walk before the packer is
|
|
44
|
+
# handed the value is what would make the refusal repeatable.
|
|
33
45
|
def self.encode(value)
|
|
34
46
|
FACTORY.dump(value)
|
|
35
47
|
rescue ::RangeError, ::NoMethodError => e
|
|
36
48
|
raise UnsupportedTypeError, e.message
|
|
49
|
+
rescue ::SystemStackError
|
|
50
|
+
raise InvalidTypeError, "value nests deeper than this host can write (a reference cycle necessarily does)"
|
|
37
51
|
end
|
|
38
52
|
end
|
|
39
53
|
end
|
data/lib/kobako/codec/error.rb
CHANGED
|
@@ -18,7 +18,8 @@ module Kobako
|
|
|
18
18
|
class TruncatedInputError < Error; end
|
|
19
19
|
|
|
20
20
|
# The type byte at the current position is not in the 11-entry kobako
|
|
21
|
-
# type mapping (e.g. an unknown ext code, or a reserved msgpack tag)
|
|
21
|
+
# type mapping (e.g. an unknown ext code, or a reserved msgpack tag) —
|
|
22
|
+
# or, on encode, a value nesting past what the packer can walk.
|
|
22
23
|
class InvalidTypeError < Error; end
|
|
23
24
|
|
|
24
25
|
# A msgpack +str+ payload was not valid UTF-8, or an ext 0x00 Symbol
|
data/lib/kobako/errors.rb
CHANGED
|
@@ -116,12 +116,45 @@ module Kobako
|
|
|
116
116
|
|
|
117
117
|
# Service layer. Raised when a Service capability call inside a mruby
|
|
118
118
|
# script reported an application-level failure that the script did not
|
|
119
|
-
# rescue.
|
|
119
|
+
# rescue. The base class covers a Service that ran and raised; the two
|
|
120
|
+
# subclasses below cover the calls that never reached one, so a Host App
|
|
121
|
+
# routes them apart with +rescue+ instead of by reading the message.
|
|
120
122
|
class ServiceError < Error
|
|
121
123
|
include Diagnosable
|
|
122
124
|
include CarriesExecution
|
|
123
125
|
end
|
|
124
126
|
|
|
127
|
+
# The ServiceError subclass raised when the call reached no Service
|
|
128
|
+
# method: the bound path holds nothing, the Capability Handle is not
|
|
129
|
+
# live in this invocation, or the method is absent or outside the guest
|
|
130
|
+
# surface. The causes stay indistinguishable — an opaque target must
|
|
131
|
+
# disclose nothing about which methods it defines — so what a Host App
|
|
132
|
+
# learns is that this call will not succeed by being retried.
|
|
133
|
+
class NoServiceError < ServiceError; end
|
|
134
|
+
|
|
135
|
+
# The ServiceError subclass raised when the call reached the Service
|
|
136
|
+
# method but its arguments did not fit — an unknown keyword, or an
|
|
137
|
+
# arity mismatch.
|
|
138
|
+
class ServiceArgumentError < ServiceError; end
|
|
139
|
+
|
|
140
|
+
# Raised at a Service method's +yield+ site when the guest block it
|
|
141
|
+
# yielded to raised. A Service rescues it the way it would rescue a
|
|
142
|
+
# block's exception without a Sandbox between the two frames; leaving it
|
|
143
|
+
# unrescued returns it to the guest, which re-raises the exception it
|
|
144
|
+
# raised in the first place. It therefore never reaches the Host App as
|
|
145
|
+
# an invocation outcome and carries no Execution. +klass+ names the
|
|
146
|
+
# guest-side class, which has no host counterpart to rebuild.
|
|
147
|
+
class BlockError < Error
|
|
148
|
+
include Diagnosable
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Raised at a Service method's +yield+ site when a yield argument has no
|
|
152
|
+
# wire representation, so the round-trip never left the host and the
|
|
153
|
+
# block never ran. As BlockError, a Service rescues it at that site and
|
|
154
|
+
# may go on, so it never reaches the Host App as an invocation outcome;
|
|
155
|
+
# unrescued, the dispatch answers as that Service having failed.
|
|
156
|
+
class YieldValueError < Error; end
|
|
157
|
+
|
|
125
158
|
# HandleExhaustedError is the canonical SandboxError subclass for the
|
|
126
159
|
# id-cap-hit path. Raised when the per-invocation Handle ID counter in
|
|
127
160
|
# Catalog::Handles reaches +0x7fff_ffff+ (2³¹ − 1) and further
|
data/lib/kobako/outcome.rb
CHANGED
|
@@ -17,12 +17,16 @@ module Kobako
|
|
|
17
17
|
ORIGIN_SANDBOX = "sandbox"
|
|
18
18
|
ORIGIN_SERVICE = "service"
|
|
19
19
|
|
|
20
|
-
# The guest-written class names that
|
|
21
|
-
# A name absent here settles as
|
|
22
|
-
# widens the taxonomy only by naming a class the
|
|
20
|
+
# The guest-written class names that narrow the class a Panic settles
|
|
21
|
+
# as. A name absent here settles as the base class its origin already
|
|
22
|
+
# chose, so the guest widens the taxonomy only by naming a class the
|
|
23
|
+
# host already defines.
|
|
23
24
|
SUBCLASSES = {
|
|
24
25
|
"Kobako::BytecodeError" => BytecodeError,
|
|
25
|
-
"Kobako::UndefinedEntrypointError" => UndefinedEntrypointError
|
|
26
|
+
"Kobako::UndefinedEntrypointError" => UndefinedEntrypointError,
|
|
27
|
+
"Kobako::Transport::Error" => Kobako::Transport::Error,
|
|
28
|
+
"Kobako::NoServiceError" => NoServiceError,
|
|
29
|
+
"Kobako::ServiceArgumentError" => ServiceArgumentError
|
|
26
30
|
}.freeze
|
|
27
31
|
|
|
28
32
|
module_function
|
|
@@ -52,13 +56,17 @@ module Kobako
|
|
|
52
56
|
UndefinedEntrypointError.new(message, name: entrypoint, available: available.map(&:to_sym), **attribution)
|
|
53
57
|
end
|
|
54
58
|
|
|
55
|
-
# +origin
|
|
56
|
-
#
|
|
57
|
-
#
|
|
59
|
+
# +origin+ picks the branch, and the guest-written class name may
|
|
60
|
+
# narrow within it so callers can rescue one path specifically. A name
|
|
61
|
+
# naming a class outside the branch its origin chose is ignored rather
|
|
62
|
+
# than honoured: what a guest calls its exception must not move the
|
|
63
|
+
# failure to a layer the attribution did not put it in.
|
|
58
64
|
def error_class(origin, klass)
|
|
59
|
-
|
|
65
|
+
base = origin == ORIGIN_SERVICE ? ServiceError : SandboxError
|
|
66
|
+
selected = SUBCLASSES.fetch(klass, base)
|
|
67
|
+
return base unless selected <= base
|
|
60
68
|
|
|
61
|
-
|
|
69
|
+
selected
|
|
62
70
|
end
|
|
63
71
|
|
|
64
72
|
# An arm the host cannot settle: the guest wrote nothing, or wrote
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../codec"
|
|
4
|
+
require_relative "../errors"
|
|
4
5
|
require_relative "../payload"
|
|
5
6
|
require_relative "call"
|
|
6
7
|
require_relative "reflection"
|
|
@@ -36,6 +37,29 @@ module Kobako
|
|
|
36
37
|
# not part of the public Kobako error taxonomy.
|
|
37
38
|
class UndefinedTargetError < StandardError; end
|
|
38
39
|
|
|
40
|
+
# The codec fault of a request that never became a call, restated in
|
|
41
|
+
# the vocabulary the guest can act on while staying inside the codec
|
|
42
|
+
# taxonomy the fault boundary sorts on.
|
|
43
|
+
class UnreadableRequestError < Kobako::Codec::Error; end
|
|
44
|
+
|
|
45
|
+
# The category kobako's own refusals answer under, keyed by the class
|
|
46
|
+
# each is raised as and ordered most specific first. A class absent
|
|
47
|
+
# here is the Service's own exception, which answers under +runtime+
|
|
48
|
+
# wearing the +<class>: <message>+ shape that says so.
|
|
49
|
+
#
|
|
50
|
+
# +Kobako::Codec::Error+ is the floor rather than a path of its own:
|
|
51
|
+
# a codec fault reaching the boundary unnamed is the exchange failing,
|
|
52
|
+
# and must not be dressed as something a Service raised.
|
|
53
|
+
OWN_FAULTS = {
|
|
54
|
+
HandleExhaustedError => "internal",
|
|
55
|
+
UndefinedTargetError => "undefined",
|
|
56
|
+
ArgumentError => "argument",
|
|
57
|
+
YieldValueError => "runtime",
|
|
58
|
+
Kobako::Codec::Error => "internal",
|
|
59
|
+
Kobako::SandboxError => "runtime"
|
|
60
|
+
}.freeze
|
|
61
|
+
private_constant :OWN_FAULTS
|
|
62
|
+
|
|
39
63
|
# Answer a single routed Call with +[ok, bytes]+, which the native
|
|
40
64
|
# side puts on the Reply's ok or fault arm. Invoked from the
|
|
41
65
|
# per-invocation dispatch Proc that
|
|
@@ -61,7 +85,7 @@ module Kobako
|
|
|
61
85
|
# stays uncaught and traps the invocation rather than being masked as a
|
|
62
86
|
# rescuable fault.
|
|
63
87
|
rescue StandardError => e
|
|
64
|
-
[false, *caught_fault(e)] # : [bool, String, String?]
|
|
88
|
+
[false, *caught_fault(e, yielder)] # : [bool, String, String?]
|
|
65
89
|
ensure
|
|
66
90
|
yielder&.invalidate!
|
|
67
91
|
end
|
|
@@ -70,12 +94,23 @@ module Kobako
|
|
|
70
94
|
# the +catch+ frame a guest +break+ unwinds to. Split from #dispatch
|
|
71
95
|
# so the reply-shaping and the failure boundary stay one glance wide.
|
|
72
96
|
def run(call, resolver, handler, yielder)
|
|
73
|
-
arguments, carried_handle =
|
|
97
|
+
arguments, carried_handle = decode_arguments(call.payload)
|
|
74
98
|
receiver = resolve_target(call.target, resolver, handler)
|
|
75
99
|
args, kwargs = resolve_call_args(arguments, handler, carried_handle)
|
|
76
100
|
catch(BREAK_THROW) { invoke(receiver, call.method_name, args, kwargs, yielder) }
|
|
77
101
|
end
|
|
78
102
|
|
|
103
|
+
# Decode the Call's payload into its arguments, reporting whether any
|
|
104
|
+
# Capability Handle crossed. A codec fault here is a request that
|
|
105
|
+
# never became a call, restated so it cannot read as an unwritable
|
|
106
|
+
# reply — the same restatement #encode_ok makes in the other
|
|
107
|
+
# direction.
|
|
108
|
+
def decode_arguments(payload)
|
|
109
|
+
Kobako::Codec.track_handles { Payload::Arguments.decode(payload) }
|
|
110
|
+
rescue Kobako::Codec::Error => e
|
|
111
|
+
raise UnreadableRequestError, "Sandbox could not read the request: #{e.message}"
|
|
112
|
+
end
|
|
113
|
+
|
|
79
114
|
# Resolve positional and keyword arguments off the decoded payload in
|
|
80
115
|
# one step. +carried_handle+ reports whether the decode carried any
|
|
81
116
|
# Capability Handle; when it did not, every argument resolves to
|
|
@@ -95,14 +130,24 @@ module Kobako
|
|
|
95
130
|
# is the +StandardError+ caught by #dispatch's rescue; the category
|
|
96
131
|
# tells the guest which kind of failure it was so it can raise the
|
|
97
132
|
# matching proxy-side error.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
133
|
+
#
|
|
134
|
+
# The class prefix marks a Service's own exception and nothing else:
|
|
135
|
+
# it is the +<class>: <message>+ shape a Host App is told to keep
|
|
136
|
+
# secrets out of, so wearing it says the Service raised. kobako's own
|
|
137
|
+
# refusals answer under their own wording instead of borrowing that
|
|
138
|
+
# shape.
|
|
139
|
+
#
|
|
140
|
+
# The guest's own block failing is not the Service's to report at
|
|
141
|
+
# all, so the Yielder that raised it is asked first — it recognises
|
|
142
|
+
# its own by identity and words the failure the guest's way.
|
|
143
|
+
def caught_fault(error, yielder)
|
|
144
|
+
block_failure = yielder&.fault_text(error)
|
|
145
|
+
return fault("block", block_failure) if block_failure
|
|
146
|
+
|
|
147
|
+
own = OWN_FAULTS.find { |klass, _| error.is_a?(klass) }
|
|
148
|
+
return fault(own.last, error.message) if own
|
|
149
|
+
|
|
150
|
+
fault("runtime", "#{error.class}: #{error.message}")
|
|
106
151
|
end
|
|
107
152
|
|
|
108
153
|
# Dispatch +method+ on +target+. +kwargs+ is already Symbol-keyed
|
|
@@ -182,10 +227,18 @@ module Kobako
|
|
|
182
227
|
# +UnsupportedTypeError+; the rescue routes it through the
|
|
183
228
|
# Catalog::Handles via #wrap_as_handle and re-encodes with the
|
|
184
229
|
# Capability Handle in place. The happy path encodes exactly once.
|
|
230
|
+
#
|
|
231
|
+
# Any other codec fault is the answer failing to encode rather than the
|
|
232
|
+
# request failing to decode, and a Service is the only side that can
|
|
233
|
+
# change what it returns — so it is named here, where the direction is
|
|
234
|
+
# known, instead of falling to the boundary's codec floor and reporting
|
|
235
|
+
# as an exchange that produced no Service outcome.
|
|
185
236
|
def encode_ok(value, handler)
|
|
186
237
|
Kobako::Codec::Encoder.encode(value)
|
|
187
238
|
rescue Kobako::Codec::UnsupportedTypeError
|
|
188
239
|
encode_ok(wrap_as_handle(value, handler), handler)
|
|
240
|
+
rescue Kobako::Codec::Error => e
|
|
241
|
+
raise Kobako::SandboxError, "Sandbox could not write the Service's answer: #{e.message}"
|
|
189
242
|
end
|
|
190
243
|
|
|
191
244
|
# Allocate +value+ in the Sandbox's Catalog::Handles and return a +Handle+
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../codec"
|
|
4
|
+
require_relative "../errors"
|
|
4
5
|
|
|
5
6
|
module Kobako
|
|
6
7
|
# See lib/kobako/transport.rb for the umbrella module doc; this file
|
|
@@ -20,8 +21,8 @@ module Kobako
|
|
|
20
21
|
# * ok — return the decoded value to +yield+'s caller
|
|
21
22
|
# * break — +throw break_tag, value+ so the Dispatcher's +catch+
|
|
22
23
|
# frame unwinds the Service method
|
|
23
|
-
# * error — raise
|
|
24
|
-
#
|
|
24
|
+
# * error — raise a BlockError at the Service's yield site,
|
|
25
|
+
# carrying the guest's class on +#klass+
|
|
25
26
|
#
|
|
26
27
|
# The Dispatcher calls #invalidate! from its +ensure+ block once
|
|
27
28
|
# dispatch completes; any later call to a stashed Yielder then raises
|
|
@@ -40,6 +41,23 @@ module Kobako
|
|
|
40
41
|
@break_tag = break_tag
|
|
41
42
|
@handler = handler
|
|
42
43
|
@active = true
|
|
44
|
+
@raised = nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The text the guest's own block failure crosses back as, or +nil+
|
|
48
|
+
# when +error+ is not the BlockError this Yielder raised. Identity
|
|
49
|
+
# rather than class: a Service that rescued the block's failure and
|
|
50
|
+
# raised its own has reported something else, and the guest must
|
|
51
|
+
# hear about that instead.
|
|
52
|
+
#
|
|
53
|
+
# The class travels with the message because not every block failure
|
|
54
|
+
# is an exception the guest holds — a block value the guest refused
|
|
55
|
+
# has a class to raise under and no object to continue.
|
|
56
|
+
def fault_text(error)
|
|
57
|
+
raised = @raised
|
|
58
|
+
return if raised.nil? || !error.equal?(raised)
|
|
59
|
+
|
|
60
|
+
"#{raised.klass}: #{raised.message}"
|
|
43
61
|
end
|
|
44
62
|
|
|
45
63
|
# Re-enter the guest with +args+ and reify the Yield Reply into
|
|
@@ -52,8 +70,8 @@ module Kobako
|
|
|
52
70
|
def yield(*args)
|
|
53
71
|
raise LocalJumpError, "guest block invoked after host dispatch frame returned" unless @active
|
|
54
72
|
|
|
55
|
-
arm, body, klass = @yield_to_guest.call(
|
|
56
|
-
raise
|
|
73
|
+
arm, body, klass = @yield_to_guest.call(encode_args(args))
|
|
74
|
+
raise remember(BlockError.new(body, klass: klass)) if arm == :error
|
|
57
75
|
|
|
58
76
|
value, carried_handle = decode_body(body)
|
|
59
77
|
throw @break_tag, value if arm == :break
|
|
@@ -76,6 +94,24 @@ module Kobako
|
|
|
76
94
|
|
|
77
95
|
private
|
|
78
96
|
|
|
97
|
+
# Encode what the Service is yielding. A value outside the wire type
|
|
98
|
+
# set fails here, before the guest is re-entered, and is restated so
|
|
99
|
+
# the Service reads a refusal of its own argument rather than a codec
|
|
100
|
+
# class it never named.
|
|
101
|
+
def encode_args(args)
|
|
102
|
+
Kobako::Codec::Encoder.encode(args)
|
|
103
|
+
rescue Kobako::Codec::Error => e
|
|
104
|
+
raise YieldValueError, "Service yielded a value the block cannot receive: #{e.message}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Hold onto the BlockError being raised so #fault_text can recognise
|
|
108
|
+
# it if it comes back unrescued, and return it so the raise site reads
|
|
109
|
+
# as one statement. Only the newest is kept: a Service that rescued
|
|
110
|
+
# an earlier one and yielded again has already handled it.
|
|
111
|
+
def remember(error)
|
|
112
|
+
@raised = error
|
|
113
|
+
end
|
|
114
|
+
|
|
79
115
|
# Decode a value-carrying arm's payload, answering the value and
|
|
80
116
|
# whether the decode carried a Capability Handle. The tracking
|
|
81
117
|
# bracket opens only around this decode: the guest re-entry may run
|
data/lib/kobako/version.rb
CHANGED
data/sig/kobako/errors.rbs
CHANGED
|
@@ -47,6 +47,19 @@ module Kobako
|
|
|
47
47
|
include CarriesExecution
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
class NoServiceError < ServiceError
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class ServiceArgumentError < ServiceError
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class BlockError < Error
|
|
57
|
+
include Diagnosable
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class YieldValueError < Error
|
|
61
|
+
end
|
|
62
|
+
|
|
50
63
|
class HandleExhaustedError < SandboxError
|
|
51
64
|
end
|
|
52
65
|
|
data/sig/kobako/outcome.rbs
CHANGED
|
@@ -2,7 +2,7 @@ module Kobako
|
|
|
2
2
|
module Outcome
|
|
3
3
|
ORIGIN_SANDBOX: String
|
|
4
4
|
ORIGIN_SERVICE: String
|
|
5
|
-
SUBCLASSES: Hash[String, singleton(SandboxError)]
|
|
5
|
+
SUBCLASSES: Hash[String, (singleton(SandboxError) | singleton(ServiceError))]
|
|
6
6
|
|
|
7
7
|
type panic_fields = [String, String, String, Array[String], Array[String]]
|
|
8
8
|
|
|
@@ -4,8 +4,13 @@ module Kobako
|
|
|
4
4
|
class UndefinedTargetError < StandardError
|
|
5
5
|
end
|
|
6
6
|
|
|
7
|
+
class UnreadableRequestError < Kobako::Codec::Error
|
|
8
|
+
end
|
|
9
|
+
|
|
7
10
|
BREAK_THROW: Symbol
|
|
8
11
|
|
|
12
|
+
OWN_FAULTS: Hash[singleton(StandardError), String]
|
|
13
|
+
|
|
9
14
|
|
|
10
15
|
|
|
11
16
|
|
|
@@ -15,7 +20,9 @@ module Kobako
|
|
|
15
20
|
|
|
16
21
|
def self?.resolve_call_args: (Kobako::Payload::Arguments arguments, Kobako::Codec::_HandleTable handler, bool carried_handle) -> [Array[untyped], Hash[Symbol, untyped]]
|
|
17
22
|
|
|
18
|
-
def self?.
|
|
23
|
+
def self?.decode_arguments: (String payload) -> [Kobako::Payload::Arguments, bool]
|
|
24
|
+
|
|
25
|
+
def self?.caught_fault: (StandardError error, Kobako::Transport::Yielder? yielder) -> [String, String]
|
|
19
26
|
|
|
20
27
|
def self?.invoke: (untyped target, String method, Array[untyped] args, Hash[Symbol, untyped] kwargs, ?Kobako::Transport::Yielder? yielder) -> untyped
|
|
21
28
|
|
|
@@ -5,9 +5,12 @@ module Kobako
|
|
|
5
5
|
@break_tag: Symbol
|
|
6
6
|
@handler: Kobako::Codec::_HandleTable
|
|
7
7
|
@active: bool
|
|
8
|
+
@raised: Kobako::BlockError?
|
|
8
9
|
|
|
9
10
|
def initialize: (Kobako::Transport::_GuestYielder yield_to_guest, Symbol break_tag, Kobako::Codec::_HandleTable handler) -> void
|
|
10
11
|
|
|
12
|
+
def fault_text: (Exception error) -> String?
|
|
13
|
+
|
|
11
14
|
def yield: (*untyped args) -> untyped
|
|
12
15
|
|
|
13
16
|
def to_proc: () -> Proc
|
|
@@ -16,6 +19,10 @@ module Kobako
|
|
|
16
19
|
|
|
17
20
|
private
|
|
18
21
|
|
|
22
|
+
def encode_args: (Array[untyped] args) -> String
|
|
23
|
+
|
|
24
|
+
def remember: (Kobako::BlockError error) -> Kobako::BlockError
|
|
25
|
+
|
|
19
26
|
def decode_body: (String body) -> [untyped, bool]
|
|
20
27
|
|
|
21
28
|
def restore: (untyped value, bool carried_handle) -> untyped
|