Skip to main content

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}