1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
//! Guest-side library for writing cuttlefish proc-blocks.
//!
//! A block is a state machine that the host drives. It never calls the host and
//! waits; it *returns* a [`Command`] saying what it wants, and the host — having
//! done that thing — steps it again with an [`Event`]. See [`cuttlefish_abi`]
//! for why control is inverted, and what that buys: chiefly that cancellation
//! needs no cooperation from the guest, because the host simply stops stepping.
//!
//! This crate exists so block authors do not hand-write that inversion.
//! Implement [`Block`], call [`export_block!`], and the macro emits the raw wasm
//! exports the host expects.
//!
//! # Writing a block
//!
//! ```
//! use cuttlefish_sdk::{Block, Command, Event};
//!
//! #[derive(Default)]
//! struct Shout;
//!
//! impl Block for Shout {
//! fn start(&mut self, input: serde_json::Value) -> Command {
//! match input.get("path").and_then(|v| v.as_str()) {
//! Some(path) => Command::Open { path: path.to_string() },
//! None => Command::Fail {
//! code: "schema_validation_failed".into(),
//! message: "input needs a string `path`".into(),
//! },
//! }
//! }
//!
//! fn step(&mut self, event: Event) -> Command {
//! match event {
//! Event::Opened { handle, len, .. } => Command::Slice { handle, offset: 0, len },
//! Event::Sliced { text, .. } => Command::Done {
//! result: serde_json::json!({ "shouted": text.to_uppercase() }),
//! },
//! other => Command::Fail {
//! code: "unexpected_event".into(),
//! message: format!("{other:?}"),
//! },
//! }
//! }
//! }
//!
//! // A real block adds this to emit the wasm exports:
//! // cuttlefish_sdk::export_block!(Shout);
//! ```
//!
//! Because a block is an ordinary Rust type, it can be unit-tested natively with
//! no wasm involved: construct it, call `start`, then feed it the events its
//! commands would produce. Only the boundary itself needs a wasm harness.
//!
//! # Memory ownership across the boundary
//!
//! Values handed to the host are leaked on purpose. The host reads them
//! immediately after the call returns, and the entire instance is destroyed when
//! the job ends, so there is nothing to reclaim and no cross-language allocator
//! coordination to get wrong.
//!
//! Do not "fix" this by freeing. The host would then read freed memory, and on
//! wasm that is a silent wrong answer rather than a segfault — the linear memory
//! is still perfectly valid to read, it just no longer holds what anyone thinks.
pub use ;
/// How a guest hands a (pointer, length) pair back to the host.
///
/// The obvious alternative is packing both into the single `i64` a wasm export
/// can return. That works only while pointers are 32 bits. Returning a pointer
/// to this struct instead costs one extra memory read per call and keeps every
/// export signature unchanged under a 64-bit guest, where `usize` simply widens.
///
/// Do not replace this with bit-packing; that is precisely what it exists to
/// avoid. The host reads exactly `2 * size_of::<usize>()` bytes at the returned
/// address and splits them down the middle, so this layout is load-bearing —
/// hence `#[repr(C)]`, and hence the tests pinning its size and field order.
/// What a proc-block author implements.
///
/// The host calls [`start`](Block::start) once with the job's input, then
/// [`step`](Block::step) after each command it carries out, until the block
/// returns [`Command::Done`] or [`Command::Fail`].
/// Allocate a buffer for the host to write into, returning its address.
///
/// Leaked deliberately; see the crate docs on memory ownership.
/// Decode JSON the host wrote at `ptr`.
///
/// # Safety
///
/// `ptr` must point at `len` initialized bytes written by the host, normally
/// into a buffer obtained from [`__alloc`].
pub unsafe
/// Serialize `value` into guest memory, returning the address of a [`Desc`]
/// describing it.
///
/// Both the payload and the descriptor are leaked; see the crate docs.
/// Emit the wasm exports for a [`Block`] implementation.
///
/// The block's state lives in a thread-local because the host instantiates one
/// module per job and never shares it — so there is exactly one block instance
/// per module instance, and no cross-job state that could leak between them.
///
/// Every export takes and returns `usize` rather than a packed integer. On
/// `wasm32` that lowers to `i32` parameters and results; on a 64-bit guest the
/// same source compiles to `i64` with nothing here changing.