hurray_ffi/buffer_list.rs
1//! Opaque list of buffer handles for multi-buffer tensors (ADR-030).
2//!
3//! A tensor whose descriptor references more than one buffer — per-channel,
4//! NF4 or MXFP quantization, sparse layouts, block-paged, composite — needs all
5//! of its buffers to travel together. [`HurrayBufferList`] is that carrier: an
6//! ordered, owning collection of [`HurrayBuffer`] handles.
7//!
8//! ## Ownership
9//!
10//! The list **owns** every handle pushed into it. [`hurray_buffer_list_get`]
11//! returns a *borrowed* pointer — the caller MUST NOT destroy it.
12//! [`hurray_buffer_list_destroy`] destroys every owned handle exactly once and
13//! then frees the list.
14//!
15//! ## Order
16//!
17//! Element `i` of the list is the buffer at index `i` of the tensor
18//! descriptor's buffer table (ADR-030 § 3). Buffer indices appearing in
19//! quantization descriptors (`scale_buffer_index`, `zero_point_buffer_index`),
20//! layout descriptors, and composite members therefore index this list
21//! directly.
22
23use crate::{
24 buffer::{hurray_buffer_destroy, HurrayBuffer},
25 panic::{catch, null_check},
26 status::{HurrayStatus, HURRAY_ERR_INDEX_OUT_OF_BOUNDS, HURRAY_ERR_INTERNAL, HURRAY_OK},
27};
28
29// ── Debug sentinel constants ──────────────────────────────────────────────────
30
31/// Sentinel placed in `HurrayBufferList.sentinel` on allocation (debug builds only).
32#[cfg(debug_assertions)]
33const SENTINEL_LIVE: u64 = 0x1157_0000_11FE_0000;
34
35/// Sentinel written before the list allocation is freed, to detect a double
36/// destroy in debug builds.
37#[cfg(debug_assertions)]
38const SENTINEL_DEAD: u64 = 0x1157_DEAD_1157_DEAD;
39
40// ── HurrayBufferList ──────────────────────────────────────────────────────────
41
42/// Opaque handle to an ordered, owning list of [`HurrayBuffer`] handles.
43///
44/// Like [`HurrayBuffer`], this struct is **not** `#[repr(C)]`; its layout is an
45/// implementation detail and callers MUST treat it as a black-box pointer.
46/// Keeping it opaque is what allows [`hurray_buffer_list_get`] to bounds-check
47/// and to hand back a borrowed handle — a bare `HurrayBuffer**` array could do
48/// neither.
49pub struct HurrayBufferList {
50 /// Double-destroy sentinel (debug builds only).
51 #[cfg(debug_assertions)]
52 sentinel: u64,
53 /// Owned handles in descriptor-buffer-table order. A slot is nulled as soon
54 /// as its handle is destroyed, so a re-entrant destroy cannot free it twice.
55 buffers: Vec<*mut HurrayBuffer>,
56}
57
58// SAFETY: mirrors the `HurrayBuffer` contract — the C ABI is single-thread-at-a-time
59// per handle, so `Send` only permits moving the box across threads during
60// construction and destruction, never concurrent access.
61unsafe impl Send for HurrayBufferList {}
62
63impl HurrayBufferList {
64 /// Builds a list that already owns `buffers`, in descriptor buffer-table order.
65 ///
66 /// The push-one-at-a-time path exists so a C caller can fail halfway and keep
67 /// ownership of what it has not handed over; a list assembled inside this crate has no
68 /// such halfway state.
69 pub(crate) fn from_handles(buffers: Vec<*mut HurrayBuffer>) -> Box<Self> {
70 Box::new(Self {
71 #[cfg(debug_assertions)]
72 sentinel: SENTINEL_LIVE,
73 buffers,
74 })
75 }
76}
77
78// ── Constructor ───────────────────────────────────────────────────────────────
79
80/// Creates an empty [`HurrayBufferList`].
81///
82/// `capacity` is a hint only; the list grows as needed. Pass the tensor's
83/// buffer count to avoid reallocation.
84///
85/// The caller owns the returned list and MUST destroy it exactly once with
86/// [`hurray_buffer_list_destroy`].
87///
88/// # Safety
89///
90/// `out_list` MUST be a valid, non-null, writable pointer.
91///
92/// # Examples
93///
94/// ```
95/// use hurray_ffi::buffer_list::{hurray_buffer_list_destroy, hurray_buffer_list_new};
96/// use hurray_ffi::{HurrayBufferList, HURRAY_OK};
97///
98/// let mut list: *mut HurrayBufferList = std::ptr::null_mut();
99/// assert_eq!(unsafe { hurray_buffer_list_new(2, &mut list) }, HURRAY_OK);
100/// assert!(!list.is_null());
101///
102/// assert_eq!(unsafe { hurray_buffer_list_destroy(&mut list) }, HURRAY_OK);
103/// assert!(list.is_null()); // destroy nulls the caller's pointer
104/// ```
105#[no_mangle]
106pub unsafe extern "C" fn hurray_buffer_list_new(
107 capacity: u64,
108 out_list: *mut *mut HurrayBufferList,
109) -> HurrayStatus {
110 catch(|| {
111 null_check!(out_list);
112
113 // Cap the pre-allocation: `capacity` is an untrusted hint, and a bogus
114 // value must not let a caller request an enormous allocation up front.
115 let hint = capacity.min(64) as usize;
116
117 let boxed = Box::new(HurrayBufferList {
118 #[cfg(debug_assertions)]
119 sentinel: SENTINEL_LIVE,
120 buffers: Vec::with_capacity(hint),
121 });
122 // SAFETY: just allocated; ownership transfers to the caller, who must
123 // call hurray_buffer_list_destroy exactly once.
124 *out_list = Box::into_raw(boxed);
125 HURRAY_OK
126 })
127}
128
129// ── Push ──────────────────────────────────────────────────────────────────────
130
131/// Appends `buffer` to `list`, transferring ownership of the handle to the list.
132///
133/// On success the caller MUST NOT destroy `buffer` — destroying the list
134/// destroys it. On failure ownership stays with the caller.
135///
136/// Buffers MUST be pushed in descriptor buffer-table order: the first push is
137/// buffer index `0`, the second is index `1`, and so on.
138///
139/// # Safety
140///
141/// - `list` MUST be a live handle from [`hurray_buffer_list_new`].
142/// - `buffer` MUST be a live handle from `hurray_buffer_from_ptr` that has not
143/// been destroyed and is not already owned by another list.
144///
145/// # Examples
146///
147/// ```
148/// use hurray_ffi::buffer::hurray_buffer_from_ptr;
149/// use hurray_ffi::buffer_list::{
150/// hurray_buffer_list_destroy, hurray_buffer_list_len, hurray_buffer_list_new,
151/// hurray_buffer_list_push,
152/// };
153/// use hurray_ffi::{HurrayBuffer, HurrayBufferList, HURRAY_OK};
154///
155/// #[repr(align(64))]
156/// struct Aligned([u8; 64]);
157/// let mut data = Aligned([0u8; 64]);
158///
159/// let mut buffer: *mut HurrayBuffer = std::ptr::null_mut();
160/// assert_eq!(
161/// unsafe {
162/// hurray_buffer_from_ptr(
163/// data.0.as_mut_ptr().cast(), 64, 64, 0, 0, 0,
164/// None, std::ptr::null_mut(), &mut buffer,
165/// )
166/// },
167/// HURRAY_OK
168/// );
169///
170/// let mut list: *mut HurrayBufferList = std::ptr::null_mut();
171/// unsafe { hurray_buffer_list_new(1, &mut list) };
172/// assert_eq!(unsafe { hurray_buffer_list_push(list, buffer) }, HURRAY_OK);
173///
174/// let mut len: u64 = 0;
175/// unsafe { hurray_buffer_list_len(list, &mut len) };
176/// assert_eq!(len, 1);
177///
178/// // Destroying the list destroys the pushed buffer too.
179/// unsafe { hurray_buffer_list_destroy(&mut list) };
180/// ```
181#[no_mangle]
182pub unsafe extern "C" fn hurray_buffer_list_push(
183 list: *mut HurrayBufferList,
184 buffer: *mut HurrayBuffer,
185) -> HurrayStatus {
186 catch(|| {
187 null_check!(list, buffer);
188
189 #[cfg(debug_assertions)]
190 // SAFETY: list is non-null and points to a live HurrayBufferList.
191 if (*list).sentinel == SENTINEL_DEAD {
192 return HURRAY_ERR_INTERNAL;
193 }
194
195 // SAFETY: list is non-null and points to a live HurrayBufferList.
196 (*list).buffers.push(buffer);
197 HURRAY_OK
198 })
199}
200
201// ── Accessors ─────────────────────────────────────────────────────────────────
202
203/// Reads the number of buffers in `list`.
204///
205/// # Safety
206///
207/// - `list` MUST be a live handle from [`hurray_buffer_list_new`].
208/// - `out_len` MUST be a valid, non-null, writable pointer.
209///
210/// # Examples
211///
212/// ```
213/// use hurray_ffi::buffer_list::{
214/// hurray_buffer_list_destroy, hurray_buffer_list_len, hurray_buffer_list_new,
215/// };
216/// use hurray_ffi::{HurrayBufferList, HURRAY_OK};
217///
218/// let mut list: *mut HurrayBufferList = std::ptr::null_mut();
219/// unsafe { hurray_buffer_list_new(0, &mut list) };
220///
221/// let mut len: u64 = 7;
222/// assert_eq!(unsafe { hurray_buffer_list_len(list, &mut len) }, HURRAY_OK);
223/// assert_eq!(len, 0);
224///
225/// unsafe { hurray_buffer_list_destroy(&mut list) };
226/// ```
227#[no_mangle]
228pub unsafe extern "C" fn hurray_buffer_list_len(
229 list: *const HurrayBufferList,
230 out_len: *mut u64,
231) -> HurrayStatus {
232 catch(|| {
233 null_check!(list, out_len);
234
235 // SAFETY: list is non-null and points to a live HurrayBufferList.
236 *out_len = (*list).buffers.len() as u64;
237 HURRAY_OK
238 })
239}
240
241/// Borrows the [`HurrayBuffer`] at `index`.
242///
243/// The returned handle is **borrowed**: ownership stays with the list, and the
244/// caller MUST NOT call `hurray_buffer_destroy` on it. It stays valid until the
245/// list is destroyed.
246///
247/// Returns [`HURRAY_ERR_INDEX_OUT_OF_BOUNDS`] if `index` is not less than the
248/// list length.
249///
250/// # Safety
251///
252/// - `list` MUST be a live handle from [`hurray_buffer_list_new`].
253/// - `out_buffer` MUST be a valid, non-null, writable pointer.
254///
255/// # Examples
256///
257/// ```
258/// use hurray_ffi::buffer_list::{
259/// hurray_buffer_list_destroy, hurray_buffer_list_get, hurray_buffer_list_new,
260/// };
261/// use hurray_ffi::status::HURRAY_ERR_INDEX_OUT_OF_BOUNDS;
262/// use hurray_ffi::{HurrayBuffer, HurrayBufferList};
263///
264/// let mut list: *mut HurrayBufferList = std::ptr::null_mut();
265/// unsafe { hurray_buffer_list_new(0, &mut list) };
266///
267/// // An empty list has no index 0.
268/// let mut got: *mut HurrayBuffer = std::ptr::null_mut();
269/// assert_eq!(
270/// unsafe { hurray_buffer_list_get(list, 0, &mut got) },
271/// HURRAY_ERR_INDEX_OUT_OF_BOUNDS
272/// );
273///
274/// unsafe { hurray_buffer_list_destroy(&mut list) };
275/// ```
276#[no_mangle]
277pub unsafe extern "C" fn hurray_buffer_list_get(
278 list: *const HurrayBufferList,
279 index: u64,
280 out_buffer: *mut *mut HurrayBuffer,
281) -> HurrayStatus {
282 catch(|| {
283 null_check!(list, out_buffer);
284
285 // SAFETY: list is non-null and points to a live HurrayBufferList.
286 let buffers = &(*list).buffers;
287 let Ok(idx) = usize::try_from(index) else {
288 return HURRAY_ERR_INDEX_OUT_OF_BOUNDS;
289 };
290 match buffers.get(idx) {
291 // A nulled slot means the list is mid-destroy; treat it as absent
292 // rather than handing back a dangling handle.
293 Some(&b) if !b.is_null() => {
294 *out_buffer = b;
295 HURRAY_OK
296 }
297 _ => HURRAY_ERR_INDEX_OUT_OF_BOUNDS,
298 }
299 })
300}
301
302// ── Destructor ────────────────────────────────────────────────────────────────
303
304/// Destroys `*list` and every [`HurrayBuffer`] it owns, then writes null through
305/// `list`.
306///
307/// Nulling the caller's pointer is the sound half of Arrow's "release marks the
308/// structure released" discipline: the list allocation itself is freed here, so
309/// a marker written inside it could not be read back, but the caller's own
310/// variable can be invalidated.
311///
312/// Each owned slot is nulled as its handle is destroyed, so a release callback
313/// that panics or re-enters cannot cause a double free. A panicking callback
314/// leaks the remainder of the list rather than corrupting it.
315///
316/// Passing a pointer to a null pointer is a no-op and returns [`HURRAY_OK`],
317/// which makes cleanup paths idempotent.
318///
319/// # Safety
320///
321/// - `list` MUST be a valid, non-null, writable pointer to a `*mut HurrayBufferList`.
322/// - `*list` MUST be a live handle from [`hurray_buffer_list_new`], or null.
323///
324/// # Examples
325///
326/// ```
327/// use hurray_ffi::buffer_list::{hurray_buffer_list_destroy, hurray_buffer_list_new};
328/// use hurray_ffi::{HurrayBufferList, HURRAY_OK};
329///
330/// let mut list: *mut HurrayBufferList = std::ptr::null_mut();
331/// unsafe { hurray_buffer_list_new(0, &mut list) };
332///
333/// assert_eq!(unsafe { hurray_buffer_list_destroy(&mut list) }, HURRAY_OK);
334/// assert!(list.is_null());
335///
336/// // Idempotent: destroying an already-nulled pointer is a no-op.
337/// assert_eq!(unsafe { hurray_buffer_list_destroy(&mut list) }, HURRAY_OK);
338/// ```
339#[no_mangle]
340pub unsafe extern "C" fn hurray_buffer_list_destroy(
341 list: *mut *mut HurrayBufferList,
342) -> HurrayStatus {
343 catch(|| {
344 null_check!(list);
345
346 // SAFETY: list is a valid, writable pointer to a handle slot.
347 let ptr = *list;
348 if ptr.is_null() {
349 return HURRAY_OK;
350 }
351
352 #[cfg(debug_assertions)]
353 {
354 // SAFETY: ptr is non-null and points to a live HurrayBufferList.
355 if (*ptr).sentinel == SENTINEL_DEAD {
356 return HURRAY_ERR_INTERNAL;
357 }
358 // Written through the raw pointer before Box::from_raw so the freed
359 // slot carries the dead marker; writing after would only update the
360 // moved-out local copy.
361 (*ptr).sentinel = SENTINEL_DEAD;
362 }
363
364 // Destroy each owned handle, nulling its slot first so a re-entrant or
365 // panicking release callback can never see a destroyable handle twice.
366 // SAFETY: ptr is non-null and points to a live HurrayBufferList.
367 for slot in (*ptr).buffers.iter_mut() {
368 let buffer = std::mem::replace(slot, std::ptr::null_mut());
369 if !buffer.is_null() {
370 // SAFETY: the list owns this handle, it is non-null, and this is
371 // its first and only destroy.
372 hurray_buffer_destroy(buffer);
373 }
374 }
375
376 // SAFETY: created by hurray_buffer_list_new via Box::into_raw; this is
377 // the first and only destroy.
378 drop(Box::from_raw(ptr));
379
380 // The caller's variable is now observably dead.
381 *list = std::ptr::null_mut();
382 HURRAY_OK
383 })
384}
385
386// ── Tests ─────────────────────────────────────────────────────────────────────
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use crate::buffer::{hurray_buffer_byte_size, hurray_buffer_from_ptr};
392 use crate::status::HURRAY_ERR_NULL_POINTER;
393 use std::ffi::c_void;
394 use std::sync::atomic::{AtomicUsize, Ordering};
395
396 #[repr(align(64))]
397 struct Aligned([u8; 128]);
398
399 /// Counts releases through the caller-supplied context pointer rather than a
400 /// global: cargo runs tests in parallel, so a shared counter would be raced by
401 /// any other test that destroys a buffer.
402 unsafe extern "C" fn counting_release(_data: *mut c_void, ctx: *mut c_void) {
403 if !ctx.is_null() {
404 (*(ctx as *const AtomicUsize)).fetch_add(1, Ordering::SeqCst);
405 }
406 }
407
408 fn make_buffer(data: &mut Aligned, size: u64, releases: &AtomicUsize) -> *mut HurrayBuffer {
409 let mut buffer: *mut HurrayBuffer = std::ptr::null_mut();
410 let status = unsafe {
411 hurray_buffer_from_ptr(
412 data.0.as_mut_ptr().cast(),
413 size,
414 64,
415 0,
416 0,
417 0,
418 Some(counting_release),
419 releases as *const AtomicUsize as *mut c_void,
420 &mut buffer,
421 )
422 };
423 assert_eq!(status, HURRAY_OK);
424 buffer
425 }
426
427 #[test]
428 fn new_and_destroy_nulls_the_caller_pointer() {
429 let mut list: *mut HurrayBufferList = std::ptr::null_mut();
430 assert_eq!(unsafe { hurray_buffer_list_new(4, &mut list) }, HURRAY_OK);
431 assert!(!list.is_null());
432 assert_eq!(unsafe { hurray_buffer_list_destroy(&mut list) }, HURRAY_OK);
433 assert!(list.is_null());
434 }
435
436 #[test]
437 fn destroy_is_idempotent_on_a_null_slot() {
438 let mut list: *mut HurrayBufferList = std::ptr::null_mut();
439 assert_eq!(unsafe { hurray_buffer_list_destroy(&mut list) }, HURRAY_OK);
440 }
441
442 #[test]
443 fn null_arguments_are_rejected() {
444 assert_eq!(
445 unsafe { hurray_buffer_list_new(0, std::ptr::null_mut()) },
446 HURRAY_ERR_NULL_POINTER
447 );
448 assert_eq!(
449 unsafe { hurray_buffer_list_destroy(std::ptr::null_mut()) },
450 HURRAY_ERR_NULL_POINTER
451 );
452 let mut len = 0u64;
453 assert_eq!(
454 unsafe { hurray_buffer_list_len(std::ptr::null(), &mut len) },
455 HURRAY_ERR_NULL_POINTER
456 );
457 }
458
459 #[test]
460 fn push_then_len_and_get_round_trip_in_order() {
461 let mut a = Aligned([1u8; 128]);
462 let mut b = Aligned([2u8; 128]);
463 let releases = AtomicUsize::new(0);
464 let buf_a = make_buffer(&mut a, 128, &releases);
465 let buf_b = make_buffer(&mut b, 64, &releases);
466
467 let mut list: *mut HurrayBufferList = std::ptr::null_mut();
468 unsafe { hurray_buffer_list_new(2, &mut list) };
469 assert_eq!(unsafe { hurray_buffer_list_push(list, buf_a) }, HURRAY_OK);
470 assert_eq!(unsafe { hurray_buffer_list_push(list, buf_b) }, HURRAY_OK);
471
472 let mut len = 0u64;
473 unsafe { hurray_buffer_list_len(list, &mut len) };
474 assert_eq!(len, 2);
475
476 // Order is push order, i.e. descriptor buffer-table order: the 128-byte
477 // buffer is index 0 and the 64-byte one is index 1.
478 for (index, expected) in [(0u64, 128u64), (1, 64)] {
479 let mut got: *mut HurrayBuffer = std::ptr::null_mut();
480 assert_eq!(
481 unsafe { hurray_buffer_list_get(list, index, &mut got) },
482 HURRAY_OK
483 );
484 let mut size = 0u64;
485 unsafe { hurray_buffer_byte_size(got, &mut size) };
486 assert_eq!(size, expected);
487 }
488
489 unsafe { hurray_buffer_list_destroy(&mut list) };
490 }
491
492 #[test]
493 fn get_rejects_an_out_of_range_index() {
494 let mut a = Aligned([0u8; 128]);
495 let releases = AtomicUsize::new(0);
496 let buf = make_buffer(&mut a, 128, &releases);
497 let mut list: *mut HurrayBufferList = std::ptr::null_mut();
498 unsafe { hurray_buffer_list_new(1, &mut list) };
499 unsafe { hurray_buffer_list_push(list, buf) };
500
501 let mut got: *mut HurrayBuffer = std::ptr::null_mut();
502 assert_eq!(
503 unsafe { hurray_buffer_list_get(list, 1, &mut got) },
504 HURRAY_ERR_INDEX_OUT_OF_BOUNDS
505 );
506 assert_eq!(
507 unsafe { hurray_buffer_list_get(list, u64::MAX, &mut got) },
508 HURRAY_ERR_INDEX_OUT_OF_BOUNDS
509 );
510
511 unsafe { hurray_buffer_list_destroy(&mut list) };
512 }
513
514 #[test]
515 fn destroying_the_list_releases_every_owned_buffer_exactly_once() {
516 let releases = AtomicUsize::new(0);
517 let mut a = Aligned([0u8; 128]);
518 let mut b = Aligned([0u8; 128]);
519 let mut c = Aligned([0u8; 128]);
520
521 let mut list: *mut HurrayBufferList = std::ptr::null_mut();
522 unsafe { hurray_buffer_list_new(3, &mut list) };
523 for data in [&mut a, &mut b, &mut c] {
524 let buf = make_buffer(data, 128, &releases);
525 unsafe { hurray_buffer_list_push(list, buf) };
526 }
527
528 unsafe { hurray_buffer_list_destroy(&mut list) };
529 assert_eq!(releases.load(Ordering::SeqCst), 3);
530 assert!(list.is_null());
531 }
532}