kobako 0.19.0 → 0.21.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 +127 -0
- data/Cargo.lock +15 -5
- data/README.md +96 -44
- data/ROADMAP.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +44 -0
- data/crates/kobako-runtime/Cargo.toml +8 -1
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-runtime/src/dispatch.rs +17 -11
- data/crates/kobako-runtime/src/error.rs +38 -6
- data/crates/kobako-runtime/src/lib.rs +13 -2
- data/crates/kobako-runtime/src/profile.rs +8 -0
- data/crates/kobako-runtime/src/runtime.rs +7 -2
- data/crates/kobako-runtime/src/snapshot.rs +13 -4
- data/crates/kobako-runtime/src/yielder.rs +4 -4
- data/crates/kobako-transport/CHANGELOG.md +31 -0
- data/crates/kobako-transport/Cargo.toml +27 -0
- data/crates/kobako-transport/README.md +43 -0
- data/crates/kobako-transport/src/abi.rs +89 -0
- data/crates/kobako-transport/src/envelope/bytes.rs +234 -0
- data/crates/kobako-transport/src/envelope/call.rs +222 -0
- data/crates/kobako-transport/src/envelope/error_record.rs +101 -0
- data/crates/kobako-transport/src/envelope/fault.rs +171 -0
- data/crates/kobako-transport/src/envelope/invocation_frames.rs +232 -0
- data/crates/kobako-transport/src/envelope/outcome.rs +261 -0
- data/crates/kobako-transport/src/envelope/reply.rs +240 -0
- data/crates/kobako-transport/src/envelope/run.rs +85 -0
- data/crates/kobako-transport/src/envelope.rs +73 -0
- data/crates/kobako-transport/src/lib.rs +21 -0
- data/crates/kobako-wasmtime/CHANGELOG.md +30 -0
- data/crates/kobako-wasmtime/Cargo.toml +5 -2
- data/crates/kobako-wasmtime/README.md +1 -1
- data/crates/kobako-wasmtime/src/config.rs +19 -12
- data/crates/kobako-wasmtime/src/dispatch.rs +15 -8
- data/crates/kobako-wasmtime/src/driver.rs +8 -27
- data/crates/kobako-wasmtime/src/frames.rs +11 -9
- data/crates/kobako-wasmtime/src/guest_mem.rs +7 -15
- data/crates/kobako-wasmtime/src/instance_pre.rs +2 -2
- data/crates/kobako-wasmtime/src/invocation.rs +1 -1
- data/crates/kobako-wasmtime/src/lib.rs +1 -1
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +7 -2
- data/ext/kobako/src/runtime/bridge.rs +103 -40
- data/ext/kobako/src/runtime/errors.rs +13 -5
- data/ext/kobako/src/runtime/gvl.rs +114 -0
- data/ext/kobako/src/runtime.rs +307 -207
- data/lib/kobako/catalog/extensions.rb +50 -43
- data/lib/kobako/catalog/handles.rb +7 -16
- data/lib/kobako/catalog/services.rb +24 -46
- data/lib/kobako/catalog/snippets.rb +16 -33
- data/lib/kobako/codec/decoder.rb +10 -13
- data/lib/kobako/codec/encoder.rb +7 -7
- data/lib/kobako/codec/error.rb +6 -6
- data/lib/kobako/codec/ext_types.rb +12 -72
- data/lib/kobako/codec/handle_walk.rb +6 -6
- data/lib/kobako/codec/state.rb +4 -56
- data/lib/kobako/codec/utils.rb +4 -4
- data/lib/kobako/codec.rb +11 -21
- data/lib/kobako/context.rb +208 -0
- data/lib/kobako/errors.rb +59 -16
- data/lib/kobako/execution.rb +52 -0
- data/lib/kobako/extension.rb +24 -10
- data/lib/kobako/outcome.rb +71 -128
- data/lib/kobako/payload/arguments.rb +59 -0
- data/lib/kobako/payload.rb +19 -0
- data/lib/kobako/pool.rb +1 -7
- data/lib/kobako/sandbox.rb +51 -191
- data/lib/kobako/sandbox_options.rb +24 -3
- data/lib/kobako/snippet/binary.rb +3 -4
- data/lib/kobako/snippet/source.rb +5 -6
- data/lib/kobako/snippet.rb +2 -2
- data/lib/kobako/transport/call.rb +21 -0
- data/lib/kobako/transport/dispatcher.rb +94 -142
- data/lib/kobako/transport/error.rb +21 -5
- data/lib/kobako/transport/reflection.rb +82 -0
- data/lib/kobako/transport/run.rb +25 -33
- data/lib/kobako/transport/yielder.rb +27 -36
- data/lib/kobako/transport.rb +11 -8
- data/lib/kobako/unresolved.rb +17 -0
- data/lib/kobako/usage.rb +7 -7
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/release-please-config.json +47 -2
- data/sig/kobako/catalog/extensions.rbs +4 -6
- data/sig/kobako/catalog/handles.rbs +0 -2
- data/sig/kobako/catalog/services.rbs +4 -4
- data/sig/kobako/catalog/snippets.rbs +4 -2
- data/sig/kobako/codec/error.rbs +4 -4
- data/sig/kobako/codec/ext_types.rbs +0 -7
- data/sig/kobako/codec/state.rbs +0 -4
- data/sig/kobako/codec.rbs +4 -5
- data/sig/kobako/context.rbs +36 -0
- data/sig/kobako/errors.rbs +27 -6
- data/sig/kobako/execution.rbs +24 -0
- data/sig/kobako/extension.rbs +4 -2
- data/sig/kobako/outcome.rbs +12 -13
- data/sig/kobako/payload/arguments.rbs +26 -0
- data/sig/kobako/payload.rbs +4 -0
- data/sig/kobako/pool.rbs +0 -2
- data/sig/kobako/runtime.rbs +44 -14
- data/sig/kobako/sandbox.rbs +5 -24
- data/sig/kobako/sandbox_options.rbs +9 -2
- data/sig/kobako/snippet/binary.rbs +1 -1
- data/sig/kobako/snippet/source.rbs +1 -1
- data/sig/kobako/transport/call.rbs +21 -0
- data/sig/kobako/transport/dispatcher.rbs +9 -11
- data/sig/kobako/transport/error.rbs +11 -0
- data/sig/kobako/transport/reflection.rbs +15 -0
- data/sig/kobako/transport/run.rbs +1 -1
- data/sig/kobako/transport/yielder.rbs +2 -2
- data/sig/kobako/transport.rbs +8 -2
- data/sig/kobako/unresolved.rbs +4 -0
- metadata +30 -11
- data/lib/kobako/fault.rb +0 -38
- data/lib/kobako/outcome/panic.rb +0 -42
- data/lib/kobako/transport/request.rb +0 -76
- data/lib/kobako/transport/response.rb +0 -83
- data/lib/kobako/transport/yield.rb +0 -94
- data/sig/kobako/fault.rbs +0 -19
- data/sig/kobako/outcome/panic.rbs +0 -34
- data/sig/kobako/transport/request.rbs +0 -29
- data/sig/kobako/transport/response.rbs +0 -33
- data/sig/kobako/transport/yield.rbs +0 -34
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
//! The Reply envelope and its reverse-direction sibling, the Yield Reply.
|
|
2
|
+
//!
|
|
3
|
+
//! Success-versus-fault is decided by a tag byte rather than a reserved
|
|
4
|
+
//! payload value, so a side learns the outcome of a Call whatever schema
|
|
5
|
+
//! the payload carries.
|
|
6
|
+
|
|
7
|
+
use super::bytes::{Reader, Writer};
|
|
8
|
+
use super::{DecodeError, ErrorRecord, Fault};
|
|
9
|
+
|
|
10
|
+
const TAG_OK: u8 = 0;
|
|
11
|
+
const TAG_FAULT: u8 = 1;
|
|
12
|
+
|
|
13
|
+
const YIELD_OK: u8 = 0x01;
|
|
14
|
+
const YIELD_BREAK: u8 = 0x02;
|
|
15
|
+
const YIELD_RESERVED: u8 = 0x03;
|
|
16
|
+
const YIELD_ERROR: u8 = 0x04;
|
|
17
|
+
|
|
18
|
+
/// The answer to one dispatch Call.
|
|
19
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
20
|
+
pub enum Reply {
|
|
21
|
+
/// The method returned; the bytes are the value, codec-encoded.
|
|
22
|
+
Ok(Vec<u8>),
|
|
23
|
+
/// The method refused or failed. Typed rather than codec-encoded:
|
|
24
|
+
/// every field of a Fault is kobako's, so a guest reads a refusal
|
|
25
|
+
/// whatever schema it speaks.
|
|
26
|
+
Fault(Fault),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl Reply {
|
|
30
|
+
pub fn decode(bytes: &[u8]) -> Result<Self, DecodeError> {
|
|
31
|
+
let mut reader = Reader::new(bytes);
|
|
32
|
+
let tag = reader.u8()?;
|
|
33
|
+
match tag {
|
|
34
|
+
TAG_OK => Ok(Reply::Ok(reader.remaining().to_vec())),
|
|
35
|
+
TAG_FAULT => {
|
|
36
|
+
let fault = Fault::read(&mut reader)?;
|
|
37
|
+
reader.finish()?;
|
|
38
|
+
Ok(Reply::Fault(fault))
|
|
39
|
+
}
|
|
40
|
+
_ => Err(DecodeError::new("Reply tag must be 0 (ok) or 1 (fault)")),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
pub fn encode(&self) -> Vec<u8> {
|
|
45
|
+
let mut writer = Writer::new();
|
|
46
|
+
match self {
|
|
47
|
+
Reply::Ok(body) => {
|
|
48
|
+
writer.u8(TAG_OK).remainder(body);
|
|
49
|
+
}
|
|
50
|
+
Reply::Fault(fault) => {
|
|
51
|
+
writer.u8(TAG_FAULT);
|
|
52
|
+
fault.write(&mut writer);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
writer.into_bytes()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// The answer to one Yield Call. Carries a `Break` outcome the dispatch
|
|
60
|
+
/// Reply has no counterpart for, because a block can end its caller.
|
|
61
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
62
|
+
pub enum YieldReply {
|
|
63
|
+
/// The block completed; the bytes are its value, codec-encoded.
|
|
64
|
+
Ok(Vec<u8>),
|
|
65
|
+
/// The block ran `break`; the bytes are the break value,
|
|
66
|
+
/// codec-encoded. It returns to the guest rather than to host code.
|
|
67
|
+
Break(Vec<u8>),
|
|
68
|
+
/// The block raised, or failed in a way the host re-raises at the
|
|
69
|
+
/// yield site.
|
|
70
|
+
Error(ErrorRecord),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
impl YieldReply {
|
|
74
|
+
pub fn decode(bytes: &[u8]) -> Result<Self, DecodeError> {
|
|
75
|
+
let mut reader = Reader::new(bytes);
|
|
76
|
+
let tag = reader.u8()?;
|
|
77
|
+
match tag {
|
|
78
|
+
YIELD_OK => Ok(YieldReply::Ok(reader.remaining().to_vec())),
|
|
79
|
+
YIELD_BREAK => Ok(YieldReply::Break(reader.remaining().to_vec())),
|
|
80
|
+
YIELD_ERROR => {
|
|
81
|
+
let record = ErrorRecord::read(&mut reader)?;
|
|
82
|
+
reader.finish()?;
|
|
83
|
+
Ok(YieldReply::Error(record))
|
|
84
|
+
}
|
|
85
|
+
YIELD_RESERVED => Err(DecodeError::new("Yield Reply tag 0x03 is reserved")),
|
|
86
|
+
_ => Err(DecodeError::new("Yield Reply tag is not recognised")),
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
pub fn encode(&self) -> Vec<u8> {
|
|
91
|
+
let mut writer = Writer::new();
|
|
92
|
+
match self {
|
|
93
|
+
YieldReply::Ok(body) => {
|
|
94
|
+
writer.u8(YIELD_OK).remainder(body);
|
|
95
|
+
}
|
|
96
|
+
YieldReply::Break(body) => {
|
|
97
|
+
writer.u8(YIELD_BREAK).remainder(body);
|
|
98
|
+
}
|
|
99
|
+
YieldReply::Error(record) => {
|
|
100
|
+
writer.u8(YIELD_ERROR);
|
|
101
|
+
record.write(&mut writer);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
writer.into_bytes()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[cfg(test)]
|
|
109
|
+
mod tests {
|
|
110
|
+
use super::*;
|
|
111
|
+
|
|
112
|
+
#[test]
|
|
113
|
+
fn both_reply_arms_round_trip() {
|
|
114
|
+
for reply in [
|
|
115
|
+
Reply::Ok(vec![0x2a]),
|
|
116
|
+
Reply::Fault(Fault::new(
|
|
117
|
+
super::super::FaultKind::Undefined,
|
|
118
|
+
"no such method",
|
|
119
|
+
)),
|
|
120
|
+
] {
|
|
121
|
+
let encoded = reply.encode();
|
|
122
|
+
assert_eq!(
|
|
123
|
+
Reply::decode(&encoded),
|
|
124
|
+
Ok(reply),
|
|
125
|
+
"a Reply must survive a host encode and decode on both arms"
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[test]
|
|
131
|
+
fn an_empty_ok_body_round_trips() {
|
|
132
|
+
let encoded = Reply::Ok(Vec::new()).encode();
|
|
133
|
+
assert_eq!(
|
|
134
|
+
Reply::decode(&encoded),
|
|
135
|
+
Ok(Reply::Ok(Vec::new())),
|
|
136
|
+
"a Reply whose payload is empty must decode as an empty value, not a truncation"
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#[test]
|
|
141
|
+
fn an_unknown_reply_tag_is_refused() {
|
|
142
|
+
assert!(
|
|
143
|
+
Reply::decode(&[9, 0]).is_err(),
|
|
144
|
+
"a Reply tag that is neither ok nor fault must be rejected as a wire violation"
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#[test]
|
|
149
|
+
fn a_zero_length_reply_is_refused() {
|
|
150
|
+
assert!(
|
|
151
|
+
Reply::decode(&[]).is_err(),
|
|
152
|
+
"a zero-length Reply must be rejected rather than read as a missing tag"
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#[test]
|
|
157
|
+
fn every_live_yield_arm_round_trips() {
|
|
158
|
+
let arms = [
|
|
159
|
+
YieldReply::Ok(vec![0xa4]),
|
|
160
|
+
YieldReply::Break(vec![0xa4]),
|
|
161
|
+
YieldReply::Error(ErrorRecord {
|
|
162
|
+
name: "LocalJumpError".into(),
|
|
163
|
+
message: "no block".into(),
|
|
164
|
+
backtrace: vec!["(eval):1".into()],
|
|
165
|
+
}),
|
|
166
|
+
];
|
|
167
|
+
for arm in arms {
|
|
168
|
+
let encoded = arm.encode();
|
|
169
|
+
assert_eq!(
|
|
170
|
+
YieldReply::decode(&encoded),
|
|
171
|
+
Ok(arm),
|
|
172
|
+
"every live Yield Reply arm must survive a host encode and decode"
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[test]
|
|
178
|
+
fn the_reserved_yield_tag_is_refused() {
|
|
179
|
+
assert!(
|
|
180
|
+
YieldReply::decode(&[0x03]).is_err(),
|
|
181
|
+
"Yield Reply tag 0x03 must be rejected by both peers so it stays reserved"
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#[test]
|
|
186
|
+
fn an_unknown_yield_tag_is_refused() {
|
|
187
|
+
assert!(
|
|
188
|
+
YieldReply::decode(&[0x7f]).is_err(),
|
|
189
|
+
"a Yield Reply tag outside the live set must be rejected as a wire violation"
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#[test]
|
|
194
|
+
fn golden_layout_pins_the_reply_tags() {
|
|
195
|
+
// The same body on both arms: what separates "the Service returned
|
|
196
|
+
// this" from "the Service failed with this" is the tag alone, which
|
|
197
|
+
// is what lets a side read the outcome without a payload codec.
|
|
198
|
+
assert_eq!(
|
|
199
|
+
Reply::Ok(vec![0x2a]).encode(),
|
|
200
|
+
vec![0, 0x2a],
|
|
201
|
+
"a successful Reply must encode as tag byte 0 followed by the body alone"
|
|
202
|
+
);
|
|
203
|
+
assert_eq!(
|
|
204
|
+
Reply::Fault(Fault::new(super::super::FaultKind::Runtime, "x")).encode(),
|
|
205
|
+
vec![1, 0, 0, 0, 0, 1, b'x'],
|
|
206
|
+
"a fault Reply must encode as tag byte 1 followed by the Fault's own fixed \
|
|
207
|
+
layout, so a guest reads a refusal without reaching a payload codec"
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#[test]
|
|
212
|
+
fn golden_layout_pins_the_yield_tags() {
|
|
213
|
+
assert_eq!(
|
|
214
|
+
YieldReply::Ok(vec![0xc0]).encode(),
|
|
215
|
+
vec![0x01, 0xc0],
|
|
216
|
+
"a Yield Reply carrying the block's value must encode as tag byte 0x01"
|
|
217
|
+
);
|
|
218
|
+
assert_eq!(
|
|
219
|
+
YieldReply::Break(vec![0xc0]).encode(),
|
|
220
|
+
vec![0x02, 0xc0],
|
|
221
|
+
"a Yield Reply carrying a break value must encode as tag byte 0x02"
|
|
222
|
+
);
|
|
223
|
+
assert_eq!(
|
|
224
|
+
YieldReply::Error(ErrorRecord {
|
|
225
|
+
name: "E".into(),
|
|
226
|
+
message: "m".into(),
|
|
227
|
+
backtrace: Vec::new(),
|
|
228
|
+
})
|
|
229
|
+
.encode(),
|
|
230
|
+
vec![
|
|
231
|
+
0x04, // tag: error
|
|
232
|
+
0, 0, 0, 1, b'E', // name
|
|
233
|
+
0, 0, 0, 1, b'm', // message
|
|
234
|
+
0, 0, 0, 0, // backtrace count
|
|
235
|
+
],
|
|
236
|
+
"a Yield Reply carrying a guest failure must encode as tag byte 0x04 followed \
|
|
237
|
+
by an Error Record rather than a codec-encoded body"
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
//! The Run envelope — the host→guest request one `#run` invocation carries.
|
|
2
|
+
//!
|
|
3
|
+
//! Run is Call's reverse-direction sibling — `entrypoint` routes it, the
|
|
4
|
+
//! payload feeds it. It carries no `method` because the entrypoint is
|
|
5
|
+
//! invoked through its own `#call`, and no `block_given` because `#run`
|
|
6
|
+
//! supplies no block. It rides the command buffer rather than a stdin
|
|
7
|
+
//! frame, so its length reaches the guest as an export argument.
|
|
8
|
+
|
|
9
|
+
use super::bytes::{Reader, Writer};
|
|
10
|
+
use super::DecodeError;
|
|
11
|
+
|
|
12
|
+
/// One `#run` invocation: which top-level constant, and its arguments.
|
|
13
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
14
|
+
pub struct Run {
|
|
15
|
+
pub entrypoint: String,
|
|
16
|
+
pub payload: Vec<u8>,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
impl Run {
|
|
20
|
+
pub fn decode(bytes: &[u8]) -> Result<Self, DecodeError> {
|
|
21
|
+
let mut reader = Reader::new(bytes);
|
|
22
|
+
let entrypoint = reader.text()?.to_owned();
|
|
23
|
+
Ok(Run {
|
|
24
|
+
entrypoint,
|
|
25
|
+
payload: reader.remaining().to_vec(),
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pub fn encode(&self) -> Vec<u8> {
|
|
30
|
+
let mut writer = Writer::new();
|
|
31
|
+
writer
|
|
32
|
+
.bytes(self.entrypoint.as_bytes())
|
|
33
|
+
.remainder(&self.payload);
|
|
34
|
+
writer.into_bytes()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[cfg(test)]
|
|
39
|
+
mod tests {
|
|
40
|
+
use super::*;
|
|
41
|
+
|
|
42
|
+
#[test]
|
|
43
|
+
fn a_run_round_trips() {
|
|
44
|
+
let run = Run {
|
|
45
|
+
entrypoint: "Entry".into(),
|
|
46
|
+
payload: vec![0x92, 0x90, 0x80],
|
|
47
|
+
};
|
|
48
|
+
let encoded = run.encode();
|
|
49
|
+
assert_eq!(
|
|
50
|
+
Run::decode(&encoded),
|
|
51
|
+
Ok(run),
|
|
52
|
+
"a Run must survive a host encode and decode unchanged"
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#[test]
|
|
57
|
+
fn a_run_with_no_arguments_round_trips() {
|
|
58
|
+
let run = Run {
|
|
59
|
+
entrypoint: "Entry".into(),
|
|
60
|
+
payload: Vec::new(),
|
|
61
|
+
};
|
|
62
|
+
let encoded = run.encode();
|
|
63
|
+
assert_eq!(
|
|
64
|
+
Run::decode(&encoded),
|
|
65
|
+
Ok(run),
|
|
66
|
+
"a Run with an empty payload must decode as empty, not as a truncation"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[test]
|
|
71
|
+
fn golden_layout_pins_the_run_field_order() {
|
|
72
|
+
let run = Run {
|
|
73
|
+
entrypoint: "E".into(),
|
|
74
|
+
payload: vec![0xc0],
|
|
75
|
+
};
|
|
76
|
+
assert_eq!(
|
|
77
|
+
run.encode(),
|
|
78
|
+
vec![
|
|
79
|
+
0, 0, 0, 1, b'E', // entrypoint
|
|
80
|
+
0xc0, // payload remainder
|
|
81
|
+
],
|
|
82
|
+
"the Run byte layout must stay fixed for both peers to agree"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//! The core envelope — the fixed-layout outer frame of every host↔guest
|
|
2
|
+
//! message.
|
|
3
|
+
//!
|
|
4
|
+
//! Routing and outcome attribution live here; everything a resolved method
|
|
5
|
+
//! consumes rides through as an opaque `payload` this layer never reads. A
|
|
6
|
+
//! tier that only routes messages therefore needs no payload codec at all,
|
|
7
|
+
//! and the one an assembly does use is its own choice.
|
|
8
|
+
//!
|
|
9
|
+
//! A decoded envelope borrows the buffer it came from, so a payload reaches
|
|
10
|
+
//! its reader as a view rather than a copy. The two fields a reader keeps
|
|
11
|
+
//! past that buffer's life — a binding's path and a snippet's body, read
|
|
12
|
+
//! from a frame at boot and consumed later — are copied at decode.
|
|
13
|
+
//!
|
|
14
|
+
//! No type here is `#[non_exhaustive]`. Every arm is a tag byte the layout
|
|
15
|
+
//! document fixes, so the set grows only with an ABI version — and when it
|
|
16
|
+
//! does, a peer that has not been taught the new arm should fail to
|
|
17
|
+
//! compile rather than fall into a wildcard and answer a message it cannot
|
|
18
|
+
//! read.
|
|
19
|
+
//!
|
|
20
|
+
//! This layer has one implementation, so its `golden_layout_*` tests are
|
|
21
|
+
//! what hold it to the layout document. They spell each tag as the literal
|
|
22
|
+
//! byte that document fixes, never as the constant beside it: a golden
|
|
23
|
+
//! written from the constant compares the implementation to itself.
|
|
24
|
+
//!
|
|
25
|
+
//! [core envelope]: ../../../docs/wire/envelope.md
|
|
26
|
+
|
|
27
|
+
pub(crate) mod bytes;
|
|
28
|
+
pub mod call;
|
|
29
|
+
pub mod error_record;
|
|
30
|
+
pub mod fault;
|
|
31
|
+
pub mod invocation_frames;
|
|
32
|
+
pub mod outcome;
|
|
33
|
+
pub mod reply;
|
|
34
|
+
pub mod run;
|
|
35
|
+
|
|
36
|
+
pub use call::{Call, Target};
|
|
37
|
+
pub use error_record::ErrorRecord;
|
|
38
|
+
pub use fault::{Fault, FaultKind};
|
|
39
|
+
pub use invocation_frames::{Bindings, Snippet, Snippets};
|
|
40
|
+
pub use outcome::{Origin, Outcome, Panic};
|
|
41
|
+
pub use reply::{Reply, YieldReply};
|
|
42
|
+
pub use run::Run;
|
|
43
|
+
|
|
44
|
+
use std::fmt;
|
|
45
|
+
|
|
46
|
+
/// A message that does not conform to the core envelope layout. Only
|
|
47
|
+
/// decoding produces one — every `encode` is infallible — so the name says
|
|
48
|
+
/// which direction it comes from.
|
|
49
|
+
///
|
|
50
|
+
/// The reason is a fixed string because every case is a wire violation the
|
|
51
|
+
/// receiving side rejects rather than a condition a caller recovers from by
|
|
52
|
+
/// inspecting it; it stays behind `message` so a later reason can carry more
|
|
53
|
+
/// than a literal.
|
|
54
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
55
|
+
pub struct DecodeError(&'static str);
|
|
56
|
+
|
|
57
|
+
impl DecodeError {
|
|
58
|
+
pub const fn new(reason: &'static str) -> Self {
|
|
59
|
+
DecodeError(reason)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub fn message(&self) -> &str {
|
|
63
|
+
self.0
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
impl fmt::Display for DecodeError {
|
|
68
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
69
|
+
f.write_str(self.0)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
impl std::error::Error for DecodeError {}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//! kobako-transport — the fixed tier of the kobako wire.
|
|
2
|
+
//!
|
|
3
|
+
//! A host, a payload codec, and a guest are chosen independently only
|
|
4
|
+
//! because two things are the same in every assembly, and this crate is
|
|
5
|
+
//! both of them:
|
|
6
|
+
//!
|
|
7
|
+
//! - `envelope` — the outer frame every host↔guest message rides in,
|
|
8
|
+
//! carrying routing and outcome attribution and handing the payload
|
|
9
|
+
//! through as bytes it never reads
|
|
10
|
+
//! - `abi` — the version, the packed-return layout, the frame prefix, and
|
|
11
|
+
//! the size cap the two sides must already agree on to exchange a byte
|
|
12
|
+
//!
|
|
13
|
+
//! Every value here is spelled once. Nothing depends on an engine, an
|
|
14
|
+
//! interpreter, or a schema — and nothing may come to: a tier every other
|
|
15
|
+
//! tier composes against cannot carry a choice any of them might want to
|
|
16
|
+
//! make differently.
|
|
17
|
+
//!
|
|
18
|
+
//! [core envelope]: ../../../docs/wire/envelope.md
|
|
19
|
+
|
|
20
|
+
pub mod abi;
|
|
21
|
+
pub mod envelope;
|
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.13.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.12.0...kobako-wasmtime-v0.13.0) (2026-07-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* **wasmtime:** put every cap in one struct, named the way the field is read
|
|
9
|
+
* **runtime:** give the engine contract names an implementer can write
|
|
10
|
+
* **transport:** name the core envelope's types after what they carry
|
|
11
|
+
* **wire:** route every tier through the one envelope
|
|
12
|
+
* **wire:** carry dispatch over the core envelope with an opaque payload
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* **wire:** carry dispatch over the core envelope with an opaque payload ([556104b](https://github.com/elct9620/kobako/commit/556104bf86fdf481b5368b70af83eb0add4b2708))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Code Refactoring
|
|
20
|
+
|
|
21
|
+
* **runtime:** give the engine contract names an implementer can write ([0a41491](https://github.com/elct9620/kobako/commit/0a41491360cf17def8245c0758679e92572b990f))
|
|
22
|
+
* **transport:** name the core envelope's types after what they carry ([b44de65](https://github.com/elct9620/kobako/commit/b44de6502953826c567d4ef8a594561479f50c1d))
|
|
23
|
+
* **wasmtime:** put every cap in one struct, named the way the field is read ([3f57860](https://github.com/elct9620/kobako/commit/3f578608b044d1ade0ae35231266cf3c9e517c02))
|
|
24
|
+
* **wire:** route every tier through the one envelope ([c5cd33a](https://github.com/elct9620/kobako/commit/c5cd33a5346f49857fa6e1f45c9cf9b9bea0ff77))
|
|
25
|
+
|
|
26
|
+
## [0.12.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.11.0...kobako-wasmtime-v0.12.0) (2026-07-24)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Miscellaneous Chores
|
|
30
|
+
|
|
31
|
+
* **kobako-wasmtime:** Synchronize kobako crates versions
|
|
32
|
+
|
|
3
33
|
## [0.11.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.10.2...kobako-wasmtime-v0.11.0) (2026-07-19)
|
|
4
34
|
|
|
5
35
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
[package]
|
|
17
17
|
name = "kobako-wasmtime"
|
|
18
|
-
version = "0.
|
|
18
|
+
version = "0.13.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,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.13.0", path = "../kobako-runtime" }
|
|
32
|
+
# The fixed tier: the core envelope this driver shuttles and the ABI
|
|
33
|
+
# values it drives the exports against.
|
|
34
|
+
kobako-transport = { version = "0.13.0", path = "../kobako-transport" }
|
|
32
35
|
# wasmtime — host-side embedder for kobako.wasm. We disable default-features
|
|
33
36
|
# and opt back in only what kobako needs: a Cranelift-backed runtime that can
|
|
34
37
|
# compile a pre-built wasm32-wasip1 module on the host triple, plus the `wat`
|
|
@@ -1,29 +1,36 @@
|
|
|
1
1
|
//! Per-`Driver` execution configuration.
|
|
2
2
|
//!
|
|
3
|
-
//!
|
|
4
|
-
//!
|
|
5
|
-
//!
|
|
6
|
-
//!
|
|
7
|
-
//!
|
|
8
|
-
//!
|
|
9
|
-
//!
|
|
3
|
+
//! Every cap a frontend forwards into `Driver::new`. A plain value carrier
|
|
4
|
+
//! owned by the `Driver` — distinct from the process-wide engine/module
|
|
5
|
+
//! `crate::cache` (which is shared across every sandbox) and from the
|
|
6
|
+
//! per-invocation `crate::invocation::Invocation` (which the wasm engine
|
|
7
|
+
//! mutates from inside a run).
|
|
8
|
+
//!
|
|
9
|
+
//! Field-for-field the same shape as the SDK's `kobako::Options`, and
|
|
10
|
+
//! deliberately not shared with it: this struct is free to name a wasmtime
|
|
11
|
+
//! type, and `Options` is not — an engine-free host must still be able to
|
|
12
|
+
//! build the SDK.
|
|
10
13
|
|
|
11
14
|
use std::time::Duration;
|
|
12
15
|
|
|
13
16
|
use kobako_runtime::profile::Profile;
|
|
14
17
|
|
|
15
|
-
///
|
|
16
|
-
///
|
|
18
|
+
/// The caps and requested isolation profile for one `Driver`. `None` on
|
|
19
|
+
/// any cap field disables that cap.
|
|
17
20
|
pub struct Config {
|
|
18
21
|
/// Wall-clock cap for one guest `#eval` / `#run`. Stamped into a
|
|
19
22
|
/// per-run `Instant` deadline by `Driver::prime_caps`.
|
|
20
23
|
pub timeout: Option<Duration>,
|
|
24
|
+
/// Guest linear-memory cap, in bytes. Threaded into each fresh
|
|
25
|
+
/// `Invocation`, where the wasmtime `ResourceLimiter` callback
|
|
26
|
+
/// consumes it from inside the engine.
|
|
27
|
+
pub memory_limit: Option<usize>,
|
|
21
28
|
/// Byte cap for guest stdout capture.
|
|
22
29
|
/// Sizes the per-run `MemoryOutputPipe` and computes the truncation
|
|
23
30
|
/// flag in `Driver::build_snapshot`.
|
|
24
|
-
pub
|
|
25
|
-
/// Byte cap for guest stderr capture. Mirror of `
|
|
26
|
-
pub
|
|
31
|
+
pub stdout_limit: Option<usize>,
|
|
32
|
+
/// Byte cap for guest stderr capture. Mirror of `stdout_limit`.
|
|
33
|
+
pub stderr_limit: Option<usize>,
|
|
27
34
|
/// Isolation posture the frontend requested. The per-invocation
|
|
28
35
|
/// WASI context is built to this rung — `Hermetic` freezes ambient
|
|
29
36
|
/// time and entropy (`crate::ambient`), `Permissive` leaves the
|
|
@@ -5,19 +5,19 @@
|
|
|
5
5
|
//! through the closure registered by `instance_pre::build_linker`.
|
|
6
6
|
//! That closure delegates here. The dispatcher:
|
|
7
7
|
//!
|
|
8
|
-
//! 1. Reads the
|
|
8
|
+
//! 1. Reads the Call bytes from guest linear memory.
|
|
9
9
|
//! 2. Invokes the bound `DispatchHandler` (the frontend's dispatch
|
|
10
|
-
//! bridge, e.g. a Ruby Proc) and recovers
|
|
10
|
+
//! bridge, e.g. a Ruby Proc) and recovers Reply bytes.
|
|
11
11
|
//! 3. Allocates a guest buffer via `__kobako_alloc(len)` invoked
|
|
12
12
|
//! through `Caller::get_export`.
|
|
13
|
-
//! 4. Writes the
|
|
13
|
+
//! 4. Writes the Reply bytes into the guest buffer.
|
|
14
14
|
//! 5. Returns packed `(ptr<<32)|len` for the guest to decode.
|
|
15
15
|
//!
|
|
16
16
|
//! Returns 0 on any step failure. `Kobako::Sandbox#initialize` always
|
|
17
17
|
//! installs the dispatch handler before any invocation, so reaching the
|
|
18
18
|
//! dispatcher with no handler bound is itself a wire-layer fault; the
|
|
19
19
|
//! guest maps a 0 return to a trap. Failures during normal dispatch
|
|
20
|
-
//!
|
|
20
|
+
//! take the Reply's fault arm from
|
|
21
21
|
//! `Kobako::Transport::Dispatcher.dispatch` itself — they never reach
|
|
22
22
|
//! this 0-return path.
|
|
23
23
|
//!
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
|
|
48
48
|
use wasmtime::Caller;
|
|
49
49
|
|
|
50
|
+
use kobako_transport::envelope::Call;
|
|
51
|
+
|
|
50
52
|
use crate::invocation::Invocation;
|
|
51
53
|
|
|
52
54
|
/// Drive a single `__kobako_dispatch` invocation end-to-end. Entry point
|
|
@@ -56,7 +58,7 @@ use crate::invocation::Invocation;
|
|
|
56
58
|
/// wire-layer fault. Failure paths log a `[kobako-dispatch]` line to
|
|
57
59
|
/// `stderr` so operators have a breadcrumb when the guest sees a 0
|
|
58
60
|
/// return and traps. The bound dispatch handler is contracted never to
|
|
59
|
-
/// raise (it folds Service exceptions
|
|
61
|
+
/// raise (it folds Service exceptions onto the Reply's fault arm),
|
|
60
62
|
/// so reaching the failure path is always a wiring bug or wire-layer
|
|
61
63
|
/// fault rather than an expected path.
|
|
62
64
|
pub(crate) fn handle(caller: &mut Caller<'_, Invocation>, req_ptr: i32, req_len: i32) -> i64 {
|
|
@@ -77,6 +79,11 @@ fn try_handle(
|
|
|
77
79
|
req_len: i32,
|
|
78
80
|
) -> Result<i64, &'static str> {
|
|
79
81
|
let req_bytes = crate::guest_mem::read(caller, req_ptr, req_len)?;
|
|
82
|
+
// The driver decodes the core envelope so the frontend never sees a
|
|
83
|
+
// frame; the payload inside it stays bytes the whole way through.
|
|
84
|
+
let call = Call::decode(&req_bytes).map_err(|_| {
|
|
85
|
+
"the guest sent a malformed Call envelope — please report this as a kobako bug"
|
|
86
|
+
})?;
|
|
80
87
|
|
|
81
88
|
// `Kobako::Sandbox` always installs the dispatch handler before
|
|
82
89
|
// invoking the runtime, so reaching this branch indicates a misuse
|
|
@@ -90,15 +97,15 @@ fn try_handle(
|
|
|
90
97
|
// handler. The borrow ends with the block, freeing the Caller for
|
|
91
98
|
// `write_response`; nested dispatch frames each build their own, so
|
|
92
99
|
// the LIFO re-entry lives on the Rust stack — no shared slot.
|
|
93
|
-
let
|
|
100
|
+
let reply = {
|
|
94
101
|
let mut yielder = crate::guest_mem::CallerYielder::new(caller);
|
|
95
|
-
handler.dispatch(
|
|
102
|
+
handler.dispatch(call, &mut yielder)
|
|
96
103
|
}
|
|
97
104
|
.ok_or(
|
|
98
105
|
"a Sandbox callback raised an exception instead of returning a fault — please report this as a kobako bug",
|
|
99
106
|
)?;
|
|
100
107
|
|
|
101
|
-
write_response(caller, &
|
|
108
|
+
write_response(caller, &reply.encode())
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
/// Allocate a guest-side buffer and copy the response bytes into it via
|