Skip to main content

hurray_ffi/stream/
writer.rs

1//! Blocking stream writes over a caller-supplied transport (ADR-041 §§ 3, 8).
2//!
3//! The write core never assembles payloads: it hands the driver the caller's own slices,
4//! one at a time, and this module passes each straight to the write callback. The rule
5//! that the core must not retain a payload pointer past the item costs a blocking surface
6//! nothing — the whole operation lives and dies inside one call.
7
8use hurray_io::stream::{CompositeNode, StreamWriterCore, WriteOp, WriteStep};
9
10use crate::{
11    composite::{stream_tree, HurrayCompositeNode, TreeArena},
12    descriptor::HurrayDescriptor,
13    panic::{catch, null_check},
14    status::{
15        status_from_io_error, HurrayStatus, HURRAY_ERR_INTERNAL, HURRAY_ERR_NULL_POINTER,
16        HURRAY_ERR_SINK_CLOSED, HURRAY_OK,
17    },
18    stream::{transport::Transport, HurrayTransport},
19};
20
21// ── Options and payloads ──────────────────────────────────────────────────────
22
23/// Transport assumptions for a writer.
24///
25/// Pass null to [`hurray_writer_new`] for the defaults.
26#[repr(C)]
27pub struct HurrayWriterOptions {
28    /// Refuse to emit a buffer whose sync mode is not `ProducerSynced`. Set this when the
29    /// stream crosses a machine boundary, where a device-local synchronization handle
30    /// means nothing to the far end.
31    pub enforce_cross_machine_sync: bool,
32}
33
34/// One borrowed payload: a pointer and a length.
35///
36/// Borrowed for the duration of the [`hurray_writer_write_tensor`] call and not retained
37/// afterwards. A zero-length slice MAY carry a null `ptr`.
38#[repr(C)]
39pub struct HurrayIoSlice {
40    /// First byte of the payload.
41    pub ptr: *const u8,
42    /// Payload length in bytes. MUST equal the declared `byte_size` of the buffer handle
43    /// at the same index of the descriptor's buffer table.
44    pub len: usize,
45}
46
47// ── HurrayWriter ──────────────────────────────────────────────────────────────
48
49/// Opaque handle to a blocking stream writer.
50///
51/// Not `#[repr(C)]`; the layout is an implementation detail. Created by
52/// [`hurray_writer_new`] and released exactly once by [`hurray_writer_destroy`].
53///
54/// One handle, one thread at a time.
55pub struct HurrayWriter {
56    core: StreamWriterCore,
57    transport: Transport,
58}
59
60// ── Construction / destruction ────────────────────────────────────────────────
61
62/// Creates a writer over `transport`.
63///
64/// The vtable is copied, so the caller MAY free the [`HurrayTransport`] struct immediately;
65/// its `ctx` must stay valid until the writer is destroyed, at which point the vtable's
66/// release callback is invoked.
67///
68/// # Arguments
69///
70/// - `transport` — Non-null; its `write` callback MUST be non-null.
71/// - `options` — MAY be null for [`HurrayWriterOptions`]' documented defaults.
72/// - `out_handle` — Non-null pointer to a `*mut HurrayWriter`; set on success.
73///
74/// # Safety
75///
76/// - `transport` MUST point to a readable [`HurrayTransport`] for the duration of the call.
77/// - `options`, if non-null, MUST point to a readable [`HurrayWriterOptions`].
78/// - `out_handle` MUST be a valid, non-null, writable pointer.
79#[no_mangle]
80pub unsafe extern "C" fn hurray_writer_new(
81    transport: *const HurrayTransport,
82    options: *const HurrayWriterOptions,
83    out_handle: *mut *mut HurrayWriter,
84) -> HurrayStatus {
85    catch(|| {
86        // options may be null: that is the request for defaults.
87        null_check!(transport, out_handle);
88
89        // SAFETY: caller guarantees transport is readable for this call.
90        let transport = Transport::from_c(transport);
91        if !transport.can_write() {
92            return HURRAY_ERR_NULL_POINTER;
93        }
94
95        // SAFETY: options is either null or caller-guaranteed readable.
96        let cross_machine = !options.is_null() && (*options).enforce_cross_machine_sync;
97        let core = if cross_machine {
98            StreamWriterCore::cross_machine()
99        } else {
100            StreamWriterCore::new()
101        };
102
103        // SAFETY: Box::into_raw transfers ownership to the caller, who must call
104        // hurray_writer_destroy exactly once.
105        *out_handle = Box::into_raw(Box::new(HurrayWriter { core, transport }));
106        HURRAY_OK
107    })
108}
109
110/// Destroys a writer, releases its transport context, and writes null through `handle`.
111///
112/// There is nothing to flush: a tensor is fully on the wire, or the call that wrote it
113/// failed. The transport's own close, if it has one, belongs in its release callback.
114///
115/// # Safety
116///
117/// - `handle` MUST be a valid, non-null pointer to a `*mut HurrayWriter` from
118///   [`hurray_writer_new`].
119/// - That writer MUST NOT have been destroyed already.
120#[no_mangle]
121pub unsafe extern "C" fn hurray_writer_destroy(handle: *mut *mut HurrayWriter) -> HurrayStatus {
122    catch(|| {
123        null_check!(handle);
124        let writer = *handle;
125        if writer.is_null() {
126            return HURRAY_ERR_INTERNAL;
127        }
128        // SAFETY: created by hurray_writer_new via Box::into_raw; the caller guarantees
129        // this is the first and only destroy call. Dropping it runs the transport's
130        // release callback.
131        drop(Box::from_raw(writer));
132        *handle = std::ptr::null_mut();
133        HURRAY_OK
134    })
135}
136
137// ── Writing ───────────────────────────────────────────────────────────────────
138
139/// Writes one tensor — its descriptor then its payloads — blocking in the transport's
140/// write callback as needed.
141///
142/// `payloads` MUST have one entry per buffer in the descriptor's buffer table, in that
143/// order, each with the declared `byte_size`. The whole tensor is validated before a byte
144/// reaches the sink, so a mismatch is reported without putting a torn item on the wire.
145///
146/// Returns:
147///
148/// - [`HURRAY_OK`] — the tensor is entirely written and the payload pointers are no longer
149///   held.
150/// - [`HURRAY_ERR_SINK_CLOSED`] — the sink stopped accepting bytes mid-tensor, so a
151///   truncated item is on the wire. The writer is poisoned: every later call fails, and a
152///   fresh writer over the same connection is not a recovery.
153/// - Whatever status the write callback returned, if it failed **before any byte of this
154///   tensor reached the sink**. Nothing was emitted, so the call MAY be retried. A failure
155///   after the first byte is reported as [`HURRAY_ERR_SINK_CLOSED`] instead, because at
156///   that point the damage is the same.
157///
158/// A composite is written with [`hurray_writer_write_composite`]; a composite head offered
159/// here is refused, because a head alone is not a valid tensor.
160///
161/// # Safety
162///
163/// - `handle` MUST point to a live [`HurrayWriter`] and `descriptor` to a live
164///   [`HurrayDescriptor`].
165/// - If `count` is non-zero, `payloads` MUST point to `count` readable [`HurrayIoSlice`]
166///   values, each describing memory readable for its own `len` bytes.
167/// - All of that memory MUST stay valid and unmodified until this call returns.
168#[no_mangle]
169pub unsafe extern "C" fn hurray_writer_write_tensor(
170    handle: *mut HurrayWriter,
171    descriptor: *const HurrayDescriptor,
172    payloads: *const HurrayIoSlice,
173    count: usize,
174) -> HurrayStatus {
175    catch(|| {
176        null_check!(handle, descriptor);
177        if count > 0 {
178            null_check!(payloads);
179        }
180
181        // SAFETY: non-null and caller-guaranteed to describe `count` readable slices.
182        let slices = if count == 0 {
183            &[][..]
184        } else {
185            std::slice::from_raw_parts(payloads, count)
186        };
187
188        let mut borrowed: Vec<&[u8]> = Vec::with_capacity(count);
189        for slice in slices {
190            if slice.len == 0 {
191                // An empty slice needs no pointer, and from_raw_parts needs a valid one.
192                borrowed.push(&[]);
193            } else if slice.ptr.is_null() {
194                return HURRAY_ERR_NULL_POINTER;
195            } else {
196                // SAFETY: caller guarantees `len` readable bytes at `ptr`, unmodified for
197                // the duration of this call.
198                borrowed.push(std::slice::from_raw_parts(slice.ptr, slice.len));
199            }
200        }
201
202        // SAFETY: both pointers are non-null and point to live handles, which the C ABI's
203        // one-handle-one-thread rule says nothing else is touching.
204        let HurrayWriter { core, transport } = &mut *handle;
205        let descriptor = (*descriptor).inner();
206
207        let mut op = match core.tensor(descriptor, &borrowed) {
208            Ok(op) => op,
209            Err(e) => return status_from_io_error(&e),
210        };
211        drain(&mut op, transport)
212    })
213}
214
215// ── The driver ────────────────────────────────────────────────────────────────
216
217/// Writes every byte of one submitted item — a tensor or a whole composite — to the
218/// transport.
219///
220/// A free function rather than a method, so the core and the transport can be borrowed as
221/// disjoint fields: the op holds the core mutably for as long as it lives.
222fn drain(op: &mut WriteOp<'_>, transport: &Transport) -> HurrayStatus {
223    loop {
224        match op.step() {
225            Err(e) => return status_from_io_error(&e),
226            Ok(WriteStep::ItemWritten) => return HURRAY_OK,
227            Ok(WriteStep::Output { .. }) => {
228                let src = op.output_src();
229                match transport.write(src) {
230                    Ok(0) => {
231                        // § 4: zero accepted bytes is never progress.
232                        let e = op.report_sink_closed();
233                        return status_from_io_error(&e);
234                    }
235                    Ok(n) => {
236                        if let Err(e) = op.report_written(n) {
237                            return status_from_io_error(&e);
238                        }
239                    }
240                    Err(status) => {
241                        // No `report_sink_closed` here: the core poisoned itself at this
242                        // item's first accepted byte (§ 6b), and calling it would
243                        // overwrite that with `LostWriteCount` — claiming the count is
244                        // unknown when it is known exactly.
245                        if op.emitted() > 0 {
246                            // A torn item: the transport's own status understates it.
247                            return HURRAY_ERR_SINK_CLOSED;
248                        }
249                        // Nothing of this item is on the wire, so the callback's status is
250                        // the more useful of the two — it says why the stream stopped.
251                        return status;
252                    }
253                }
254            }
255        }
256    }
257}
258
259/// Writes a composite — its head, then every member, nested ones included — as one item.
260///
261/// `nodes` is the composite in preorder: `nodes[0]` is the head, and each node declares
262/// how many of the nodes after it are its direct members. [`crate::composite`] has the
263/// shape and an example layout; names are ignored here, because the stream wire carries
264/// none.
265///
266/// The whole tree is validated before a byte reaches the sink — member counts at every
267/// level, partition exact-cover, overlay base-first ordering — so an invalid composite is
268/// refused rather than half written. Payload slices stay borrowed throughout: nothing is
269/// copied or concatenated (ADR-041 § 3).
270///
271/// Returns:
272///
273/// - [`HURRAY_OK`] — the composite is entirely written and the payload pointers are no
274///   longer held.
275/// - [`HURRAY_ERR_NOT_COMPOSITE`][crate::HURRAY_ERR_NOT_COMPOSITE] — `count` is `0`, or
276///   `nodes[0]` is not a composite head.
277///   Use [`hurray_writer_write_tensor`] for a tensor.
278/// - [`HURRAY_ERR_INDEX_OUT_OF_BOUNDS`][crate::HURRAY_ERR_INDEX_OUT_OF_BOUNDS] — the
279///   member counts do not describe the array: a head declares members the array does not
280///   carry, or nodes trail after the head's subtree is complete.
281/// - [`HURRAY_ERR_INVALID_LAYOUT`][crate::HURRAY_ERR_INVALID_LAYOUT] — the tree is not a
282///   valid composite: a head that is not one, a member count that disagrees with the
283///   head, a partition that does not cover its index space, an overlay whose base is not
284///   first. Refused before a byte moved, so the writer stays usable.
285/// - [`HURRAY_ERR_SINK_CLOSED`] — the sink stopped accepting bytes mid-composite, so a
286///   truncated item is on the wire and the writer is poisoned. A composite is one item:
287///   there is no partially valid composite to resume from.
288/// - Whatever status the write callback returned, if it failed **before any byte of this
289///   composite reached the sink**. Nothing was emitted, so the call MAY be retried.
290///
291/// # Safety
292///
293/// - `handle` MUST point to a live [`HurrayWriter`].
294/// - If `count` is non-zero, `nodes` MUST point to `count` readable
295///   [`HurrayCompositeNode`] values, each satisfying that type's own pointer contract.
296/// - All of that memory MUST stay valid and unmodified until this call returns.
297#[no_mangle]
298pub unsafe extern "C" fn hurray_writer_write_composite(
299    handle: *mut HurrayWriter,
300    nodes: *const HurrayCompositeNode,
301    count: usize,
302) -> HurrayStatus {
303    catch(|| {
304        null_check!(handle);
305        if count > 0 {
306            null_check!(nodes);
307        }
308
309        // SAFETY: non-null and caller-guaranteed to describe `count` readable nodes.
310        let nodes = if count == 0 {
311            &[][..]
312        } else {
313            std::slice::from_raw_parts(nodes, count)
314        };
315
316        // Outlives the op below: the borrowed tree points into it.
317        let mut arena: TreeArena<CompositeNode<'static>> = TreeArena::default();
318        // SAFETY: delegated to this function's contract on `nodes`.
319        let (head, members) = match stream_tree(&mut arena, nodes) {
320            Ok(tree) => tree,
321            Err(status) => return status,
322        };
323
324        // SAFETY: handle is non-null and points to a live HurrayWriter, which the C ABI's
325        // one-handle-one-thread rule says nothing else is touching.
326        let HurrayWriter { core, transport } = &mut *handle;
327
328        let mut op = match core.composite(head, members) {
329            Ok(op) => op,
330            Err(e) => return status_from_io_error(&e),
331        };
332        drain(&mut op, transport)
333    })
334}