Skip to main content

hurray_ffi/stream/
transport.rs

1//! The caller-supplied transport vtable (ADR-041 § 8).
2//!
3//! Hurray never opens a socket, a pipe or a file for a C caller. It is handed a context
4//! pointer and a handful of callbacks, and asks them for bytes. That is what keeps an
5//! async runtime out of a C consumer's binary, and it composes with anything the caller
6//! can read from or write to.
7//!
8//! A stream needs [`read`][HurrayTransport::read] or [`write`][HurrayTransport::write]; a
9//! file reader needs [`read_at`][HurrayTransport::read_at] and
10//! [`size`][HurrayTransport::size], because the container is addressed rather than
11//! consumed (ADR-041 § 7). A file *writer* needs only `write`: it emits one forward pass
12//! and never seeks back.
13//!
14//! The callbacks are **blocking by contract**, and that contract belongs here rather than
15//! to the sans-io core: a callback that blocks until it has bytes is correct and expected,
16//! precisely because the core's own progress API never blocks.
17
18use std::ffi::c_void;
19
20use crate::status::{HurrayStatus, HURRAY_ERR_NULL_POINTER, HURRAY_ERR_TRANSPORT, HURRAY_OK};
21
22// ── Callback types ────────────────────────────────────────────────────────────
23
24/// Reads up to `len` bytes into `dst`, reporting the count through `out_filled`.
25///
26/// - Returns [`HURRAY_OK`] and writes the number of bytes transferred, which MAY be fewer
27///   than `len`. A short read is normal and is not an error.
28/// - Writing `0` with [`HURRAY_OK`] means **end of input**.
29/// - On any other return value the callback MUST have transferred **no bytes** and MUST
30///   write `0` through `out_filled`. A transport that partially succeeds and then fails
31///   MUST report the partial count with [`HURRAY_OK`] and surface the failure on the next
32///   call — exactly what POSIX `read` does, and the one place a hand-written adapter
33///   tends to go wrong.
34///
35/// A callback MUST NOT call any Hurray function on the handle that invoked it.
36pub type HurrayTransportReadFn = Option<
37    unsafe extern "C" fn(
38        ctx: *mut c_void,
39        dst: *mut u8,
40        len: usize,
41        out_filled: *mut usize,
42    ) -> HurrayStatus,
43>;
44
45/// Writes up to `len` bytes from `src`, reporting the count through `out_written`.
46///
47/// - Returns [`HURRAY_OK`] and writes the number of bytes accepted, which MAY be fewer
48///   than `len`.
49/// - Writing `0` with [`HURRAY_OK`] is **not** success: it means the sink will accept no
50///   more bytes. Hurray reports that as [`HURRAY_ERR_SINK_CLOSED`][crate::HURRAY_ERR_SINK_CLOSED]
51///   when an item is in flight.
52/// - On any other return value the callback MUST have transferred **no bytes** and MUST
53///   write `0` through `out_written`, per the rule on [`HurrayTransportReadFn`].
54///
55/// A callback MUST NOT call any Hurray function on the handle that invoked it.
56pub type HurrayTransportWriteFn = Option<
57    unsafe extern "C" fn(
58        ctx: *mut c_void,
59        src: *const u8,
60        len: usize,
61        out_written: *mut usize,
62    ) -> HurrayStatus,
63>;
64
65/// Reads up to `len` bytes from absolute offset `offset` into `dst`, reporting the count
66/// through `out_filled`.
67///
68/// Required by a file reader, which addresses its input rather than consuming it: the
69/// trailer sits at `size - 40`, the index is located from it, and tensors are found by
70/// name. Every request is absolute, so this callback keeps no cursor — `pread`,
71/// `ReadFile` with an `OVERLAPPED` offset, or a seek-and-read pair the callback does
72/// itself.
73///
74/// - Returns [`HURRAY_OK`] and writes the number of bytes transferred, which MAY be fewer
75///   than `len`. A short read is normal: the remainder of the range is requested again.
76/// - Writing `0` with [`HURRAY_OK`] means the file is shorter than its own index claims,
77///   which Hurray reports as
78///   [`HURRAY_ERR_UNEXPECTED_EOF`][crate::HURRAY_ERR_UNEXPECTED_EOF]. It does not signal
79///   the end of the input: a file's end is known from its size, never discovered by
80///   reading.
81/// - On any other return value the callback MUST have transferred **no bytes** and MUST
82///   write `0` through `out_filled`, per the rule on [`HurrayTransportReadFn`]. Because
83///   the range is absolute, such a call is retryable: re-issuing it reads the same bytes.
84///
85/// A callback MUST NOT call any Hurray function on the handle that invoked it.
86pub type HurrayTransportReadAtFn = Option<
87    unsafe extern "C" fn(
88        ctx: *mut c_void,
89        offset: u64,
90        dst: *mut u8,
91        len: usize,
92        out_filled: *mut usize,
93    ) -> HurrayStatus,
94>;
95
96/// Reports the total size of the input in bytes.
97///
98/// Called exactly once, when a file reader is created — the container is located from its
99/// end, so the size is a precondition of opening it rather than something a read
100/// discovers. `fstat`, `GetFileSizeEx`, or the length of a memory span.
101///
102/// Returns [`HURRAY_OK`] and writes the size, or any other value to fail the open.
103pub type HurrayTransportSizeFn =
104    Option<unsafe extern "C" fn(ctx: *mut c_void, out_size: *mut u64) -> HurrayStatus>;
105
106/// Releases the transport's context pointer when the handle that owns it is destroyed.
107///
108/// Hurray never frees `ctx` itself — it belongs to the caller — so this callback is the
109/// only place a caller's transport object is torn down. Symmetric with the buffer release
110/// callbacks that already cross this ABI (ADR-009).
111pub type HurrayTransportReleaseFn = Option<unsafe extern "C" fn(ctx: *mut c_void)>;
112
113// ── The vtable ────────────────────────────────────────────────────────────────
114
115/// A caller-supplied byte transport: a context pointer plus the callbacks that move bytes.
116///
117/// Not a path and not a file descriptor. A vtable composes with a socket, a pipe, a memory
118/// span, or the caller's own object, and file descriptors are a portability wart of their
119/// own.
120///
121/// The struct is copied on construction, so the caller MAY free the `HurrayTransport`
122/// itself as soon as the handle is created; `ctx` must outlive the handle, and is released
123/// through [`release`][Self::release] when the handle is destroyed.
124///
125/// Thread affinity: **one handle, one thread at a time.** Hurray runs no threads of its
126/// own, so this is a rule rather than a threading model.
127#[repr(C)]
128pub struct HurrayTransport {
129    /// Opaque caller context, passed verbatim to every callback. MAY be null.
130    pub ctx: *mut c_void,
131    /// Required by a reader; MAY be null for a write-only transport.
132    pub read: HurrayTransportReadFn,
133    /// Required by a writer; MAY be null for a read-only transport.
134    pub write: HurrayTransportWriteFn,
135    /// Invoked once when the owning handle is destroyed. MAY be null.
136    pub release: HurrayTransportReleaseFn,
137    // Appended rather than grouped beside `read`: a caller using positional
138    // initialisation then gets null for the new fields, instead of its release callback
139    // landing where a positional read is expected.
140    /// Required by a file reader; MAY be null for a stream or write-only transport.
141    pub read_at: HurrayTransportReadAtFn,
142    /// Required by a file reader; MAY be null for a stream or write-only transport.
143    pub size: HurrayTransportSizeFn,
144}
145
146impl Default for HurrayTransport {
147    /// A vtable with no context and no callbacks — the `{0}` a C caller starts from,
148    /// spelled for Rust callers who fill in only the fields their transport has.
149    fn default() -> Self {
150        Self {
151            ctx: std::ptr::null_mut(),
152            read: None,
153            write: None,
154            release: None,
155            read_at: None,
156            size: None,
157        }
158    }
159}
160
161// ── Internal owned copy ───────────────────────────────────────────────────────
162
163/// A handle's own copy of the vtable, which releases `ctx` when the handle drops.
164pub(crate) struct Transport {
165    ctx: *mut c_void,
166    read: HurrayTransportReadFn,
167    write: HurrayTransportWriteFn,
168    release: HurrayTransportReleaseFn,
169    read_at: HurrayTransportReadAtFn,
170    size: HurrayTransportSizeFn,
171}
172
173impl Transport {
174    /// Copies a caller-supplied vtable.
175    ///
176    /// # Safety
177    ///
178    /// `vtable` MUST point to a readable [`HurrayTransport`] for the duration of the call.
179    pub(crate) unsafe fn from_c(vtable: *const HurrayTransport) -> Self {
180        Self {
181            ctx: (*vtable).ctx,
182            read: (*vtable).read,
183            write: (*vtable).write,
184            release: (*vtable).release,
185            read_at: (*vtable).read_at,
186            size: (*vtable).size,
187        }
188    }
189
190    /// Whether this transport can read.
191    pub(crate) fn can_read(&self) -> bool {
192        self.read.is_some()
193    }
194
195    /// Whether this transport can write.
196    pub(crate) fn can_write(&self) -> bool {
197        self.write.is_some()
198    }
199
200    /// Whether this transport can answer range requests, which a file reader needs.
201    pub(crate) fn can_read_at(&self) -> bool {
202        self.read_at.is_some() && self.size.is_some()
203    }
204
205    /// Fills up to `dst.len()` bytes from absolute offset `offset`, returning how many
206    /// arrived.
207    ///
208    /// `Err(status)` means no bytes moved. The range is absolute, so the same request MAY
209    /// simply be re-issued.
210    pub(crate) fn read_at(&self, offset: u64, dst: &mut [u8]) -> Result<usize, HurrayStatus> {
211        let Some(f) = self.read_at else {
212            return Err(HURRAY_ERR_NULL_POINTER);
213        };
214        let mut filled: usize = 0;
215        // SAFETY: `dst` is a live slice, so its pointer is valid and writable for
216        // `dst.len()` bytes; `filled` is a live local. The callback's own contract covers
217        // `ctx`, which Hurray only ever passes through.
218        let status = unsafe { f(self.ctx, offset, dst.as_mut_ptr(), dst.len(), &mut filled) };
219        if status != HURRAY_OK {
220            return Err(status);
221        }
222        // A count beyond what was offered is a broken callback, refused here for the same
223        // reason as on the sequential path.
224        if filled > dst.len() {
225            return Err(HURRAY_ERR_TRANSPORT);
226        }
227        Ok(filled)
228    }
229
230    /// Asks the transport for the total size of its input.
231    pub(crate) fn size(&self) -> Result<u64, HurrayStatus> {
232        let Some(f) = self.size else {
233            return Err(HURRAY_ERR_NULL_POINTER);
234        };
235        let mut size: u64 = 0;
236        // SAFETY: `size` is a live local; `ctx` is passed through untouched.
237        let status = unsafe { f(self.ctx, &mut size) };
238        if status != HURRAY_OK {
239            return Err(status);
240        }
241        Ok(size)
242    }
243
244    /// Fills up to `dst.len()` bytes, returning how many arrived.
245    ///
246    /// `Err(status)` means no bytes moved and the caller's stream position is intact.
247    pub(crate) fn read(&self, dst: &mut [u8]) -> Result<usize, HurrayStatus> {
248        let Some(f) = self.read else {
249            return Err(HURRAY_ERR_NULL_POINTER);
250        };
251        let mut filled: usize = 0;
252        // SAFETY: `dst` is a live slice, so its pointer is valid and writable for
253        // `dst.len()` bytes; `filled` is a live local. The callback's own contract covers
254        // `ctx`, which Hurray only ever passes through.
255        let status = unsafe { f(self.ctx, dst.as_mut_ptr(), dst.len(), &mut filled) };
256        if status != HURRAY_OK {
257            return Err(status);
258        }
259        // A count beyond what was offered is a broken callback. Refusing it here keeps the
260        // core's position provably correct instead of handing it a report it must reject.
261        if filled > dst.len() {
262            return Err(HURRAY_ERR_TRANSPORT);
263        }
264        Ok(filled)
265    }
266
267    /// Writes from `src`, returning how many bytes the sink accepted.
268    ///
269    /// `Err(status)` means no bytes reached the sink.
270    pub(crate) fn write(&self, src: &[u8]) -> Result<usize, HurrayStatus> {
271        let Some(f) = self.write else {
272            return Err(HURRAY_ERR_NULL_POINTER);
273        };
274        let mut written: usize = 0;
275        // SAFETY: `src` is a live slice, readable for `src.len()` bytes; `written` is a
276        // live local. `ctx` is passed through untouched.
277        let status = unsafe { f(self.ctx, src.as_ptr(), src.len(), &mut written) };
278        if status != HURRAY_OK {
279            return Err(status);
280        }
281        if written > src.len() {
282            return Err(HURRAY_ERR_TRANSPORT);
283        }
284        Ok(written)
285    }
286}
287
288impl Drop for Transport {
289    fn drop(&mut self) {
290        if let Some(f) = self.release.take() {
291            // SAFETY: invoked exactly once, when the owning handle is destroyed. The
292            // caller guaranteed `ctx` valid until this callback returns.
293            unsafe { f(self.ctx) };
294        }
295    }
296}