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}