tocat_wasm_abi/lib.rs
1//! The tocat WebAssembly guest ABI, version 2.
2//!
3//! One definition of the wire format, used by everything that touches it:
4//!
5//! - the host reads an [`Outbox`] out of guest memory after every call
6//! - `tocat-wasm-sdk` writes one, on behalf of a Rust guest
7//! - `sdk/wasm/include/tocat/abi.h` is generated from this crate, so a C or C++
8//! guest sees the same constants and the same struct rather than a
9//! hand-copied transcription of them
10//!
11//! Regenerate that header with:
12//!
13//! ```console
14//! $ cargo run -p tocat-wasm-abi --example tocat-abi-header
15//! ```
16//!
17//! and check it is current with `--check`, which is what CI should run.
18//!
19//! # What is here and what is not
20//!
21//! Layout, constants, and the conversions between them and Rust types.
22//! Nothing else: no allocation, no error type, no I/O, no dependencies, and
23//! `no_std`, because a guest compiled to wasm32 has none of those.
24//!
25//! Every wire value appears twice by design. `TOCAT_EMIT_BUFFERED` is the name
26//! C sees, and [`Emit::Buffered`] is the name Rust sees; the first is what the
27//! header generator emits, the second is what gets exhaustive matching. They
28//! cannot disagree, because the enum discriminants are the constants.
29//!
30//! # The pointer rule
31//!
32//! Every pointer in an [`Outbox`] is an address in the guest's linear memory,
33//! not an offset into whatever the guest uses as an arena. In Rust, as in C,
34//! that is what a pointer already is, so this is a cast rather than a
35//! calculation. Getting it wrong does not trap: both sides read memory that
36//! exists, and the symptom is an outbox that decodes as all zeroes, which is
37//! [`Emit::Pending`], which is a stage that silently swallows the stream.
38
39#![no_std]
40
41use core::mem::{offset_of, size_of};
42
43/// Bumped for any change to what the host reads or to what a value means: the
44/// struct below, the set of exports, or the interpretation of either. A guest
45/// reporting a different version is refused when it loads rather than being
46/// read as garbage.
47///
48/// The check is exact equality in both directions, which is the point. A host
49/// that silently accepted a newer guest would honour the parts of its contract
50/// it recognised and ignore the rest, and the one it ignored would be a
51/// requirement the guest cannot work without.
52pub const TOCAT_ABI_VERSION: u32 = 2;
53
54/// Bytes the host reads at `tocat_outbox()`.
55pub const TOCAT_OUTBOX_LEN: u32 = 48;
56
57/// Bytes per record in the log array.
58pub const TOCAT_LOG_RECORD_LEN: u32 = 12;
59
60/// Forward nothing. Emitting nothing means the same thing; this exists so that
61/// a filter can say it on purpose.
62pub const TOCAT_EMIT_PENDING: u32 = 0;
63/// Forward the input unchanged. The host does not read the guest's bytes at
64/// all, and nothing is copied in either direction.
65pub const TOCAT_EMIT_PASSTHROUGH: u32 = 1;
66/// Forward `bytes`, framed by `bounds`.
67pub const TOCAT_EMIT_BUFFERED: u32 = 2;
68
69/// Restart this stage's tick schedule from now.
70pub const TOCAT_FLAG_REARM: u32 = 1 << 0;
71/// End the path: upstream end of stream arriving early, and a success.
72pub const TOCAT_FLAG_HALT: u32 = 1 << 1;
73/// Wait `pace_ns` before reading upstream again.
74pub const TOCAT_FLAG_PACE: u32 = 1 << 2;
75/// Fail the path, with `message` as the reason.
76pub const TOCAT_FLAG_ERROR: u32 = 1 << 3;
77
78/// Mask for the boundary effect in `tocat_boundaries`: bits 0 and 1.
79pub const TOCAT_BOUNDARIES_MASK: u32 = 0b11;
80/// The units this stage was given do not reach the stage below. Anything that
81/// buffers across calls, splits, or coalesces.
82pub const TOCAT_BOUNDARIES_FUSE: u32 = 0;
83/// One unit in, one unit out.
84pub const TOCAT_BOUNDARIES_PRESERVE: u32 = 1;
85/// One unit in, one unit out, and the boundary is also written into the bytes,
86/// so it survives a stage below that fuses. What `frame` does.
87pub const TOCAT_BOUNDARIES_SEAL: u32 = 2;
88/// The units below are read out of the bytes rather than inherited from above,
89/// so the ones from above do not survive. What `unframe` does.
90pub const TOCAT_BOUNDARIES_SPLIT: u32 = 3;
91
92/// Mask for the requirement in `tocat_boundaries`: bits 2 and 3.
93pub const TOCAT_NEEDS_MASK: u32 = 0b1100;
94/// The stage works on any path.
95pub const TOCAT_NEEDS_NOTHING: u32 = 0;
96/// Every call must carry one whole message, so boundaries have to reach this
97/// stage from the endpoint above or from a `TOCAT_BOUNDARIES_SPLIT` stage.
98pub const TOCAT_NEEDS_UPSTREAM: u32 = 1 << 2;
99/// The units this stage emits must reach the endpoint below or a
100/// `TOCAT_BOUNDARIES_SEAL` stage, or what it emitted cannot be read back.
101pub const TOCAT_NEEDS_DOWNSTREAM: u32 = 1 << 3;
102/// Both of the above.
103pub const TOCAT_NEEDS_BOTH: u32 = TOCAT_NEEDS_UPSTREAM | TOCAT_NEEDS_DOWNSTREAM;
104
105pub const TOCAT_TRACE: u32 = 0;
106pub const TOCAT_DEBUG: u32 = 1;
107pub const TOCAT_INFO: u32 = 2;
108pub const TOCAT_WARN: u32 = 3;
109pub const TOCAT_ERROR: u32 = 4;
110
111/// One queued log record: a level, and a string in the guest's memory.
112#[repr(C)]
113#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
114pub struct LogRecord {
115 pub level: u32,
116 pub ptr: u32,
117 pub len: u32,
118}
119
120/// What a call left behind for the host.
121///
122/// Fixed layout, little-endian, [`TOCAT_OUTBOX_LEN`] bytes. `repr(C)` rather
123/// than `packed`: wasm32 puts the `u64` on an eight-byte boundary, which is
124/// where offset 32 already is, so there is no padding to remove and no
125/// unaligned field to read. The assertions below are what keep that true on
126/// every target this crate is built for, including the 64-bit host that reads
127/// the struct back out of guest memory.
128#[repr(C)]
129#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
130pub struct Outbox {
131 pub emit: u32,
132 pub bytes_ptr: u32,
133 pub bytes_len: u32,
134 pub bounds_ptr: u32,
135 pub bounds_len: u32,
136 pub flags: u32,
137 pub message_ptr: u32,
138 pub message_len: u32,
139 pub pace_ns: u64,
140 pub logs_ptr: u32,
141 pub logs_len: u32,
142}
143
144const _: () = {
145 assert!(size_of::<Outbox>() == TOCAT_OUTBOX_LEN as usize);
146 assert!(offset_of!(Outbox, emit) == 0);
147 assert!(offset_of!(Outbox, bytes_ptr) == 4);
148 assert!(offset_of!(Outbox, bytes_len) == 8);
149 assert!(offset_of!(Outbox, bounds_ptr) == 12);
150 assert!(offset_of!(Outbox, bounds_len) == 16);
151 assert!(offset_of!(Outbox, flags) == 20);
152 assert!(offset_of!(Outbox, message_ptr) == 24);
153 assert!(offset_of!(Outbox, message_len) == 28);
154 assert!(offset_of!(Outbox, pace_ns) == 32);
155 assert!(offset_of!(Outbox, logs_ptr) == 40);
156 assert!(offset_of!(Outbox, logs_len) == 44);
157
158 assert!(size_of::<LogRecord>() == TOCAT_LOG_RECORD_LEN as usize);
159};
160
161impl Outbox {
162 pub const fn new() -> Self {
163 Self {
164 emit: TOCAT_EMIT_PENDING,
165 bytes_ptr: 0,
166 bytes_len: 0,
167 bounds_ptr: 0,
168 bounds_len: 0,
169 flags: 0,
170 message_ptr: 0,
171 message_len: 0,
172 pace_ns: 0,
173 logs_ptr: 0,
174 logs_len: 0,
175 }
176 }
177
178 /// Clear it. The struct persists between calls, so a halt flag or a
179 /// message pointer left over from an earlier chunk would be applied again.
180 pub fn reset(&mut self) {
181 *self = Self::new();
182 }
183
184 pub const fn emit(&self) -> Option<Emit> {
185 Emit::from_u32(self.emit)
186 }
187
188 pub const fn set_emit(&mut self, emit: Emit) {
189 self.emit = emit.as_u32();
190 }
191
192 pub const fn has(&self, flag: u32) -> bool {
193 self.flags & flag != 0
194 }
195
196 pub const fn set(&mut self, flag: u32) {
197 self.flags |= flag;
198 }
199}
200
201/// What a stage decided to do with the chunk it was given.
202#[repr(u32)]
203#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
204pub enum Emit {
205 /// Nothing emitted; the chunk stops here.
206 #[default]
207 Pending = TOCAT_EMIT_PENDING,
208 /// Input forwarded verbatim. The host reuses the input slice, copying
209 /// nothing.
210 Passthrough = TOCAT_EMIT_PASSTHROUGH,
211 /// The stage wrote its own bytes into the output buffer, and any framing
212 /// it declared along with them.
213 Buffered = TOCAT_EMIT_BUFFERED,
214}
215
216impl Emit {
217 pub const fn from_u32(value: u32) -> Option<Self> {
218 match value {
219 TOCAT_EMIT_PENDING => Some(Self::Pending),
220 TOCAT_EMIT_PASSTHROUGH => Some(Self::Passthrough),
221 TOCAT_EMIT_BUFFERED => Some(Self::Buffered),
222 _ => None,
223 }
224 }
225
226 pub const fn as_u32(self) -> u32 {
227 self as u32
228 }
229}
230
231/// What a stage does to the message boundaries passing through it.
232///
233/// Read once, after `tocat_init`, out of the low two bits of
234/// `tocat_boundaries`. The host folds these along the chain to answer one
235/// question per requiring stage: do that stage's units survive as far as they
236/// have to. Nothing here is consulted on the per-chunk path.
237///
238/// [`Fuse`](Self::Fuse) is the default and the safe answer, because it claims
239/// nothing: a stage that has not thought about boundaries cannot be relied on
240/// to keep them.
241#[repr(u32)]
242#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
243pub enum Boundaries {
244 #[default]
245 Fuse = TOCAT_BOUNDARIES_FUSE,
246 Preserve = TOCAT_BOUNDARIES_PRESERVE,
247 Seal = TOCAT_BOUNDARIES_SEAL,
248 Split = TOCAT_BOUNDARIES_SPLIT,
249}
250
251impl Boundaries {
252 pub const fn from_u32(value: u32) -> Option<Self> {
253 match value {
254 TOCAT_BOUNDARIES_FUSE => Some(Self::Fuse),
255 TOCAT_BOUNDARIES_PRESERVE => Some(Self::Preserve),
256 TOCAT_BOUNDARIES_SEAL => Some(Self::Seal),
257 TOCAT_BOUNDARIES_SPLIT => Some(Self::Split),
258 _ => None,
259 }
260 }
261
262 pub const fn as_u32(self) -> u32 {
263 self as u32
264 }
265
266 /// Whether one message arriving still means one message leaving.
267 ///
268 /// True for [`Preserve`](Self::Preserve) and [`Seal`](Self::Seal): sealing
269 /// writes framing into the payload, which changes the bytes of a datagram
270 /// without changing how many there are. This is what the host warns about
271 /// on a path whose destination is a datagram endpoint.
272 pub const fn preserves_messages(self) -> bool {
273 matches!(self, Self::Preserve | Self::Seal)
274 }
275
276 /// Whether a requirement scanning downwards passes this stage without
277 /// being settled either way.
278 ///
279 /// Only [`Preserve`](Self::Preserve) does. [`Seal`](Self::Seal) settles it
280 /// in favour, the other two against, which is why the scan stops at all
281 /// three and asks [`satisfies_downstream`](Self::satisfies_downstream)
282 /// which it was.
283 pub const fn passes_downstream(self) -> bool {
284 matches!(self, Self::Preserve)
285 }
286
287 /// Whether a requirement scanning upwards passes this stage without being
288 /// settled either way.
289 ///
290 /// [`Seal`](Self::Seal) does, because it emits one unit for every unit it
291 /// was given; sealing only settles a scan going the other way.
292 pub const fn passes_upstream(self) -> bool {
293 matches!(self, Self::Preserve | Self::Seal)
294 }
295
296 /// Whether a downstream requirement that reached this stage is met by it,
297 /// so that nothing below can invalidate it.
298 pub const fn satisfies_downstream(self) -> bool {
299 matches!(self, Self::Seal)
300 }
301
302 /// Whether an upstream requirement that reached this stage is met by it.
303 pub const fn satisfies_upstream(self) -> bool {
304 matches!(self, Self::Split)
305 }
306}
307
308/// What a stage needs of the path it is placed on.
309///
310/// Read once, out of bits 2 and 3 of `tocat_boundaries`. Unlike
311/// [`Boundaries`], which the host only warns about, an unmet requirement is a
312/// configuration error: a stage saying this cannot do its job at all.
313///
314/// The two sides are separate because the stages that want them want opposite
315/// ones. A stage that seals a message and appends a tag makes its own
316/// boundaries and needs them to survive downwards; the stage that verifies and
317/// strips that tag needs whole messages from above and does not care what
318/// happens below it.
319#[repr(u32)]
320#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
321pub enum Needs {
322 #[default]
323 Nothing = TOCAT_NEEDS_NOTHING,
324 Upstream = TOCAT_NEEDS_UPSTREAM,
325 Downstream = TOCAT_NEEDS_DOWNSTREAM,
326 Both = TOCAT_NEEDS_BOTH,
327}
328
329impl Needs {
330 pub const fn from_u32(value: u32) -> Option<Self> {
331 match value {
332 TOCAT_NEEDS_NOTHING => Some(Self::Nothing),
333 TOCAT_NEEDS_UPSTREAM => Some(Self::Upstream),
334 TOCAT_NEEDS_DOWNSTREAM => Some(Self::Downstream),
335 TOCAT_NEEDS_BOTH => Some(Self::Both),
336 _ => None,
337 }
338 }
339
340 pub const fn as_u32(self) -> u32 {
341 self as u32
342 }
343
344 pub const fn upstream(self) -> bool {
345 matches!(self, Self::Upstream | Self::Both)
346 }
347
348 pub const fn downstream(self) -> bool {
349 matches!(self, Self::Downstream | Self::Both)
350 }
351}
352
353/// Pack what `tocat_boundaries` returns.
354pub const fn pack_boundaries(boundaries: Boundaries, needs: Needs) -> u32 {
355 boundaries.as_u32() | needs.as_u32()
356}
357
358/// Read what `tocat_boundaries` returned.
359///
360/// `None` for any bit outside the two masks, which is a guest built against a
361/// later ABI than this host speaks. Refusing it is the point: reading an
362/// unknown value as [`Boundaries::Fuse`] would run a stage whose requirement
363/// the host cannot see, and the symptom would be a corrupt stream rather than
364/// an error.
365///
366/// Zero is a fixed point: it is what a guest that does not export the function
367/// at all is taken to have answered, so [`Boundaries::Fuse`] and
368/// [`Needs::Nothing`] have to stay at 0. Both are the reading that claims
369/// nothing and asks for nothing, which is the only safe thing to assume of a
370/// stage that did not say.
371pub const fn unpack_boundaries(value: u32) -> Option<(Boundaries, Needs)> {
372 if value & !(TOCAT_BOUNDARIES_MASK | TOCAT_NEEDS_MASK) != 0 {
373 return None;
374 }
375
376 match (
377 Boundaries::from_u32(value & TOCAT_BOUNDARIES_MASK),
378 Needs::from_u32(value & TOCAT_NEEDS_MASK),
379 ) {
380 (Some(boundaries), Some(needs)) => Some((boundaries, needs)),
381 _ => None,
382 }
383}
384
385/// Severity of a queued log record, in the order every logging library writes
386/// them.
387#[repr(u32)]
388#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
389pub enum Level {
390 Trace = TOCAT_TRACE,
391 Debug = TOCAT_DEBUG,
392 /// What a record with an unrecognised level is read as: a guest that
393 /// bothered to queue one should still be heard.
394 #[default]
395 Info = TOCAT_INFO,
396 Warn = TOCAT_WARN,
397 Error = TOCAT_ERROR,
398}
399
400impl Level {
401 pub const fn from_u32(value: u32) -> Self {
402 match value {
403 TOCAT_TRACE => Self::Trace,
404 TOCAT_DEBUG => Self::Debug,
405 TOCAT_WARN => Self::Warn,
406 TOCAT_ERROR => Self::Error,
407 _ => Self::Info,
408 }
409 }
410
411 pub const fn as_u32(self) -> u32 {
412 self as u32
413 }
414}
415
416/// The names a guest exports, so that a host looks them up from the same place
417/// a guest is documented against.
418pub mod exports {
419 pub const MEMORY: &str = "memory";
420 pub const ABI_VERSION: &str = "tocat_abi_version";
421 pub const OUTBOX: &str = "tocat_outbox";
422 pub const ALLOC: &str = "tocat_alloc";
423 pub const INIT: &str = "tocat_init";
424 pub const ON_BYTES: &str = "tocat_on_bytes";
425 pub const ON_EOF: &str = "tocat_on_eof";
426 pub const ON_TICK: &str = "tocat_on_tick";
427 pub const TICK_INTERVAL_NS: &str = "tocat_tick_interval_ns";
428 pub const BOUNDARIES: &str = "tocat_boundaries";
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434
435 #[test]
436 fn the_enums_are_the_constants() {
437 assert_eq!(Emit::Buffered.as_u32(), TOCAT_EMIT_BUFFERED);
438 assert_eq!(Level::Warn.as_u32(), TOCAT_WARN);
439 assert_eq!(
440 Emit::from_u32(TOCAT_EMIT_PASSTHROUGH),
441 Some(Emit::Passthrough)
442 );
443 assert_eq!(Emit::from_u32(3), None);
444 assert_eq!(Level::from_u32(99), Level::Info);
445 assert_eq!(Boundaries::Seal.as_u32(), TOCAT_BOUNDARIES_SEAL);
446 assert_eq!(Needs::Downstream.as_u32(), TOCAT_NEEDS_DOWNSTREAM);
447 }
448
449 #[test]
450 fn boundaries_round_trip_through_one_word() {
451 for boundaries in [
452 Boundaries::Fuse,
453 Boundaries::Preserve,
454 Boundaries::Seal,
455 Boundaries::Split,
456 ] {
457 for needs in [
458 Needs::Nothing,
459 Needs::Upstream,
460 Needs::Downstream,
461 Needs::Both,
462 ] {
463 let packed = pack_boundaries(boundaries, needs);
464 assert_eq!(unpack_boundaries(packed), Some((boundaries, needs)));
465 }
466 }
467 }
468
469 /// A guest that does not export the function is read as having answered
470 /// zero, so zero has to keep meaning the claim that asks for nothing.
471 #[test]
472 fn zero_claims_nothing_and_asks_for_nothing() {
473 assert_eq!(
474 unpack_boundaries(0),
475 Some((Boundaries::Fuse, Needs::Nothing))
476 );
477 }
478
479 /// A guest built against a later ABI is refused rather than read as a
480 /// stage that claims nothing.
481 #[test]
482 fn an_unknown_bit_is_refused() {
483 assert_eq!(unpack_boundaries(1 << 4), None);
484 assert_eq!(unpack_boundaries(u32::MAX), None);
485 }
486
487 #[test]
488 fn an_outbox_starts_and_resets_empty() {
489 let mut outbox = Outbox::new();
490 assert_eq!(outbox.emit(), Some(Emit::Pending));
491
492 outbox.set(TOCAT_FLAG_HALT);
493 outbox.set_emit(Emit::Buffered);
494 assert!(outbox.has(TOCAT_FLAG_HALT));
495
496 outbox.reset();
497 assert_eq!(outbox, Outbox::new());
498 assert!(!outbox.has(TOCAT_FLAG_HALT));
499 }
500}