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}