Skip to main content

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}