Skip to main content

tocat_wasm_abi/
lib.rs

1//! The tocat WebAssembly guest ABI, version 1.
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 the layout below. A guest reporting a different
44/// version is refused when it loads rather than being read as garbage.
45pub const TOCAT_ABI_VERSION: u32 = 1;
46
47/// Bytes the host reads at `tocat_outbox()`.
48pub const TOCAT_OUTBOX_LEN: u32 = 48;
49
50/// Bytes per record in the log array.
51pub const TOCAT_LOG_RECORD_LEN: u32 = 12;
52
53/// Forward nothing. Emitting nothing means the same thing; this exists so that
54/// a filter can say it on purpose.
55pub const TOCAT_EMIT_PENDING: u32 = 0;
56/// Forward the input unchanged. The host does not read the guest's bytes at
57/// all, and nothing is copied in either direction.
58pub const TOCAT_EMIT_PASSTHROUGH: u32 = 1;
59/// Forward `bytes`, framed by `bounds`.
60pub const TOCAT_EMIT_BUFFERED: u32 = 2;
61
62/// Restart this stage's tick schedule from now.
63pub const TOCAT_FLAG_REARM: u32 = 1 << 0;
64/// End the path: upstream end of stream arriving early, and a success.
65pub const TOCAT_FLAG_HALT: u32 = 1 << 1;
66/// Wait `pace_ns` before reading upstream again.
67pub const TOCAT_FLAG_PACE: u32 = 1 << 2;
68/// Fail the path, with `message` as the reason.
69pub const TOCAT_FLAG_ERROR: u32 = 1 << 3;
70
71pub const TOCAT_TRACE: u32 = 0;
72pub const TOCAT_DEBUG: u32 = 1;
73pub const TOCAT_INFO: u32 = 2;
74pub const TOCAT_WARN: u32 = 3;
75pub const TOCAT_ERROR: u32 = 4;
76
77/// One queued log record: a level, and a string in the guest's memory.
78#[repr(C)]
79#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
80pub struct LogRecord {
81    pub level: u32,
82    pub ptr: u32,
83    pub len: u32,
84}
85
86/// What a call left behind for the host.
87///
88/// Fixed layout, little-endian, [`TOCAT_OUTBOX_LEN`] bytes. `repr(C)` rather
89/// than `packed`: wasm32 puts the `u64` on an eight-byte boundary, which is
90/// where offset 32 already is, so there is no padding to remove and no
91/// unaligned field to read. The assertions below are what keep that true on
92/// every target this crate is built for, including the 64-bit host that reads
93/// the struct back out of guest memory.
94#[repr(C)]
95#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
96pub struct Outbox {
97    pub emit: u32,
98    pub bytes_ptr: u32,
99    pub bytes_len: u32,
100    pub bounds_ptr: u32,
101    pub bounds_len: u32,
102    pub flags: u32,
103    pub message_ptr: u32,
104    pub message_len: u32,
105    pub pace_ns: u64,
106    pub logs_ptr: u32,
107    pub logs_len: u32,
108}
109
110const _: () = {
111    assert!(size_of::<Outbox>() == TOCAT_OUTBOX_LEN as usize);
112    assert!(offset_of!(Outbox, emit) == 0);
113    assert!(offset_of!(Outbox, bytes_ptr) == 4);
114    assert!(offset_of!(Outbox, bytes_len) == 8);
115    assert!(offset_of!(Outbox, bounds_ptr) == 12);
116    assert!(offset_of!(Outbox, bounds_len) == 16);
117    assert!(offset_of!(Outbox, flags) == 20);
118    assert!(offset_of!(Outbox, message_ptr) == 24);
119    assert!(offset_of!(Outbox, message_len) == 28);
120    assert!(offset_of!(Outbox, pace_ns) == 32);
121    assert!(offset_of!(Outbox, logs_ptr) == 40);
122    assert!(offset_of!(Outbox, logs_len) == 44);
123
124    assert!(size_of::<LogRecord>() == TOCAT_LOG_RECORD_LEN as usize);
125};
126
127impl Outbox {
128    pub const fn new() -> Self {
129        Self {
130            emit: TOCAT_EMIT_PENDING,
131            bytes_ptr: 0,
132            bytes_len: 0,
133            bounds_ptr: 0,
134            bounds_len: 0,
135            flags: 0,
136            message_ptr: 0,
137            message_len: 0,
138            pace_ns: 0,
139            logs_ptr: 0,
140            logs_len: 0,
141        }
142    }
143
144    /// Clear it. The struct persists between calls, so a halt flag or a
145    /// message pointer left over from an earlier chunk would be applied again.
146    pub fn reset(&mut self) {
147        *self = Self::new();
148    }
149
150    pub const fn emit(&self) -> Option<Emit> {
151        Emit::from_u32(self.emit)
152    }
153
154    pub const fn set_emit(&mut self, emit: Emit) {
155        self.emit = emit.as_u32();
156    }
157
158    pub const fn has(&self, flag: u32) -> bool {
159        self.flags & flag != 0
160    }
161
162    pub const fn set(&mut self, flag: u32) {
163        self.flags |= flag;
164    }
165}
166
167/// What a stage decided to do with the chunk it was given.
168#[repr(u32)]
169#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
170pub enum Emit {
171    /// Nothing emitted; the chunk stops here.
172    #[default]
173    Pending = TOCAT_EMIT_PENDING,
174    /// Input forwarded verbatim. The host reuses the input slice, copying
175    /// nothing.
176    Passthrough = TOCAT_EMIT_PASSTHROUGH,
177    /// The stage wrote its own bytes into the output buffer, and any framing
178    /// it declared along with them.
179    Buffered = TOCAT_EMIT_BUFFERED,
180}
181
182impl Emit {
183    pub const fn from_u32(value: u32) -> Option<Self> {
184        match value {
185            TOCAT_EMIT_PENDING => Some(Self::Pending),
186            TOCAT_EMIT_PASSTHROUGH => Some(Self::Passthrough),
187            TOCAT_EMIT_BUFFERED => Some(Self::Buffered),
188            _ => None,
189        }
190    }
191
192    pub const fn as_u32(self) -> u32 {
193        self as u32
194    }
195}
196
197/// Severity of a queued log record, in the order every logging library writes
198/// them.
199#[repr(u32)]
200#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
201pub enum Level {
202    Trace = TOCAT_TRACE,
203    Debug = TOCAT_DEBUG,
204    /// What a record with an unrecognised level is read as: a guest that
205    /// bothered to queue one should still be heard.
206    #[default]
207    Info = TOCAT_INFO,
208    Warn = TOCAT_WARN,
209    Error = TOCAT_ERROR,
210}
211
212impl Level {
213    pub const fn from_u32(value: u32) -> Self {
214        match value {
215            TOCAT_TRACE => Self::Trace,
216            TOCAT_DEBUG => Self::Debug,
217            TOCAT_WARN => Self::Warn,
218            TOCAT_ERROR => Self::Error,
219            _ => Self::Info,
220        }
221    }
222
223    pub const fn as_u32(self) -> u32 {
224        self as u32
225    }
226}
227
228/// The names a guest exports, so that a host looks them up from the same place
229/// a guest is documented against.
230pub mod exports {
231    pub const MEMORY: &str = "memory";
232    pub const ABI_VERSION: &str = "tocat_abi_version";
233    pub const OUTBOX: &str = "tocat_outbox";
234    pub const ALLOC: &str = "tocat_alloc";
235    pub const INIT: &str = "tocat_init";
236    pub const ON_BYTES: &str = "tocat_on_bytes";
237    pub const ON_EOF: &str = "tocat_on_eof";
238    pub const ON_TICK: &str = "tocat_on_tick";
239    pub const TICK_INTERVAL_NS: &str = "tocat_tick_interval_ns";
240    pub const DATAGRAM_SAFE: &str = "tocat_datagram_safe";
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn the_enums_are_the_constants() {
249        assert_eq!(Emit::Buffered.as_u32(), TOCAT_EMIT_BUFFERED);
250        assert_eq!(Level::Warn.as_u32(), TOCAT_WARN);
251        assert_eq!(
252            Emit::from_u32(TOCAT_EMIT_PASSTHROUGH),
253            Some(Emit::Passthrough)
254        );
255        assert_eq!(Emit::from_u32(3), None);
256        assert_eq!(Level::from_u32(99), Level::Info);
257    }
258
259    #[test]
260    fn an_outbox_starts_and_resets_empty() {
261        let mut outbox = Outbox::new();
262        assert_eq!(outbox.emit(), Some(Emit::Pending));
263
264        outbox.set(TOCAT_FLAG_HALT);
265        outbox.set_emit(Emit::Buffered);
266        assert!(outbox.has(TOCAT_FLAG_HALT));
267
268        outbox.reset();
269        assert_eq!(outbox, Outbox::new());
270        assert!(!outbox.has(TOCAT_FLAG_HALT));
271    }
272}