Skip to main content

hurray_ffi/
buffer.rs

1//! Opaque buffer handle for the Hurray C ABI.
2//!
3//! [`HurrayBuffer`] is an opaque heap-allocated handle that wraps a
4//! [`hurray_core::BufferHandle`] together with the raw data pointer and an
5//! optional release callback. Callers create handles via
6//! [`hurray_buffer_from_ptr`] and MUST destroy them exactly once via
7//! [`hurray_buffer_destroy`].
8
9use std::ffi::c_void;
10
11use hurray_core::{BufferHandle, DeviceTag, MemoryClass, SyncMode};
12
13use crate::{
14    panic::{catch, null_check},
15    status::{status_from_core_error, HurrayStatus, HURRAY_ERR_INTERNAL, HURRAY_OK},
16};
17
18// ── Debug sentinel constants ──────────────────────────────────────────────────
19
20/// Sentinel placed in `HurrayBuffer.sentinel` on allocation (debug builds only).
21#[cfg(debug_assertions)]
22const SENTINEL_LIVE: u64 = 0xDEAD_BEEF_CAFE_BABE;
23
24/// Sentinel placed in `HurrayBuffer.sentinel` after `hurray_buffer_destroy`
25/// to detect double-free in debug builds.
26#[cfg(debug_assertions)]
27const SENTINEL_DEAD: u64 = 0x0000_DEAD_0000_DEAD;
28
29// ── Release callback type ─────────────────────────────────────────────────────
30
31/// Optional release callback invoked by [`hurray_buffer_destroy`].
32///
33/// The first argument is the `data` pointer passed to [`hurray_buffer_from_ptr`].
34/// The second argument is the `release_context` pointer passed to the same
35/// function. Both arguments carry the exact values provided at construction time.
36///
37/// Implementations MUST NOT call back into the Hurray C ABI from within this
38/// callback; doing so may cause deadlocks or use-after-free.
39pub type HurrayReleaseCallback = Option<unsafe extern "C" fn(*mut c_void, *mut c_void)>;
40
41// ── HurrayBuffer ─────────────────────────────────────────────────────────────
42
43/// Opaque handle to a buffer registered with the Hurray C ABI.
44///
45/// This struct is **not** `#[repr(C)]`; its internal layout is an
46/// implementation detail. Callers MUST treat it as a black-box opaque
47/// pointer. The handle is heap-allocated by [`hurray_buffer_from_ptr`] and
48/// MUST be released exactly once by [`hurray_buffer_destroy`].
49pub struct HurrayBuffer {
50    /// Double-free sentinel (debug builds only).
51    #[cfg(debug_assertions)]
52    sentinel: u64,
53    /// Raw pointer to the buffer's first byte.
54    data: *mut c_void,
55    /// Metadata about the buffer's size, alignment, device, and sync mode.
56    pub(crate) handle: BufferHandle,
57    /// Optional callback invoked during [`hurray_buffer_destroy`] to release
58    /// the underlying memory.
59    release: HurrayReleaseCallback,
60    /// Caller-supplied context pointer forwarded to `release`.
61    release_context: *mut c_void,
62}
63
64// SAFETY: Single-thread-at-a-time per the C ABI contract; Send allows
65// Box<HurrayBuffer> to move across threads during destruction without any
66// concurrent access — the C API has no shared-state concurrency model.
67unsafe impl Send for HurrayBuffer {}
68
69// ── Constructor ───────────────────────────────────────────────────────────────
70
71/// Creates a new [`HurrayBuffer`] handle from a raw data pointer and metadata.
72///
73/// The caller retains ownership of `data` until the handle is destroyed via
74/// [`hurray_buffer_destroy`], at which point the `release` callback (if
75/// non-null) is invoked with `(data, release_context)`. If `release` is null,
76/// the caller is responsible for freeing `data` after destroying the handle.
77///
78/// # Arguments
79///
80/// - `data` — Non-null pointer to the first byte of the buffer. MUST be
81///   aligned to at least `alignment` bytes.
82/// - `byte_size` — Length of the buffer in bytes.
83/// - `alignment` — Declared alignment of `data` in bytes; MUST be a power of
84///   two and at least [`hurray_core::MIN_BUFFER_ALIGNMENT`] for non-empty
85///   buffers.
86/// - `device_tag` — Wire byte identifying the memory space (see
87///   `docs/spec/buffer-protocol.md § Device Tags`).
88/// - `sync_mode` — Wire byte for producer/consumer ordering (see
89///   `docs/spec/buffer-protocol.md § Synchronization Mode`).
90/// - `memory_class` — Wire byte for memory accessibility class.
91/// - `release` — Optional callback invoked on [`hurray_buffer_destroy`].
92/// - `release_context` — Opaque context pointer forwarded to `release`; MAY
93///   be null.
94/// - `out_handle` — Non-null pointer to a `*mut HurrayBuffer`; set to the
95///   newly allocated handle on success.
96///
97/// # Safety
98///
99/// - `data` MUST be a valid, non-null pointer to at least `byte_size` bytes.
100/// - `data` MUST remain valid until the release callback is called.
101/// - `out_handle` MUST be a valid, non-null, writable pointer.
102/// - `release_context` lifetime is caller-managed; it MUST remain valid until
103///   the release callback returns.
104#[no_mangle]
105pub unsafe extern "C" fn hurray_buffer_from_ptr(
106    data: *mut c_void,
107    byte_size: u64,
108    alignment: u32,
109    device_tag: u8,
110    sync_mode: u8,
111    memory_class: u8,
112    release: HurrayReleaseCallback,
113    release_context: *mut c_void,
114    out_handle: *mut *mut HurrayBuffer,
115) -> HurrayStatus {
116    catch(|| {
117        // release_context may be null — it is caller-controlled context.
118        null_check!(data, out_handle);
119
120        let device = match DeviceTag::from_byte(device_tag) {
121            Ok(d) => d,
122            Err(e) => return status_from_core_error(&e),
123        };
124        let mode = match SyncMode::from_byte(sync_mode) {
125            Ok(m) => m,
126            Err(e) => return status_from_core_error(&e),
127        };
128        let class = match MemoryClass::from_byte(memory_class) {
129            Ok(c) => c,
130            Err(e) => return status_from_core_error(&e),
131        };
132        let handle =
133            match BufferHandle::with_memory_class(byte_size, alignment, device, mode, class) {
134                Ok(h) => h,
135                Err(e) => return status_from_core_error(&e),
136            };
137
138        let boxed = Box::new(HurrayBuffer {
139            #[cfg(debug_assertions)]
140            sentinel: SENTINEL_LIVE,
141            data,
142            handle,
143            release,
144            release_context,
145        });
146        // SAFETY: we just allocated `boxed`; Box::into_raw transfers ownership
147        // to the caller, who must call hurray_buffer_destroy exactly once.
148        *out_handle = Box::into_raw(boxed);
149        HURRAY_OK
150    })
151}
152
153// ── Destructor ────────────────────────────────────────────────────────────────
154
155/// Destroys a [`HurrayBuffer`] handle and invokes the release callback.
156///
157/// After this call, `buffer` is no longer valid and MUST NOT be dereferenced.
158/// In debug builds, a double-free attempt returns [`HURRAY_ERR_INTERNAL`].
159///
160/// # Safety
161///
162/// - `buffer` MUST have been created by [`hurray_buffer_from_ptr`].
163/// - `buffer` MUST NOT have been previously destroyed.
164#[no_mangle]
165pub unsafe extern "C" fn hurray_buffer_destroy(buffer: *mut HurrayBuffer) -> HurrayStatus {
166    catch(|| {
167        null_check!(buffer);
168
169        #[cfg(debug_assertions)]
170        {
171            // SAFETY: buffer is non-null and points to a live HurrayBuffer;
172            // reading the sentinel before Box::from_raw for double-free detection.
173            if (*buffer).sentinel == SENTINEL_DEAD {
174                return HURRAY_ERR_INTERNAL;
175            }
176            // Write SENTINEL_DEAD through the raw pointer *before* Box::from_raw
177            // so the freed allocation slot carries the dead marker.  Writing
178            // after Box::from_raw only updates the moved-out local copy, not the
179            // original heap slot that the allocator may inspect on the second call.
180            (*buffer).sentinel = SENTINEL_DEAD;
181        }
182
183        // SAFETY: created by hurray_buffer_from_ptr via Box::into_raw; caller
184        // guarantees this is the first and only destroy call.
185        let mut b = Box::from_raw(buffer);
186
187        // SAFETY: caller provided `release` and `release_context` with valid
188        // lifetimes; we invoke the callback exactly once at destruction time.
189        if let Some(f) = b.release.take() {
190            f(b.data, b.release_context);
191        }
192
193        HURRAY_OK
194    })
195}
196
197// ── Accessors ─────────────────────────────────────────────────────────────────
198
199/// Reads the byte size of a buffer handle.
200///
201/// # Safety
202///
203/// `buffer` and `out_byte_size` MUST be valid, non-null pointers.
204#[no_mangle]
205pub unsafe extern "C" fn hurray_buffer_byte_size(
206    buffer: *const HurrayBuffer,
207    out_byte_size: *mut u64,
208) -> HurrayStatus {
209    catch(|| {
210        null_check!(buffer, out_byte_size);
211        // SAFETY: buffer is non-null and points to a live HurrayBuffer.
212        *out_byte_size = (*buffer).handle.byte_size();
213        HURRAY_OK
214    })
215}
216
217/// Reads the declared alignment of a buffer handle (in bytes).
218///
219/// # Safety
220///
221/// `buffer` and `out_alignment` MUST be valid, non-null pointers.
222#[no_mangle]
223pub unsafe extern "C" fn hurray_buffer_alignment(
224    buffer: *const HurrayBuffer,
225    out_alignment: *mut u32,
226) -> HurrayStatus {
227    catch(|| {
228        null_check!(buffer, out_alignment);
229        // SAFETY: buffer is non-null and points to a live HurrayBuffer.
230        *out_alignment = (*buffer).handle.alignment();
231        HURRAY_OK
232    })
233}
234
235/// Reads the device tag wire byte of a buffer handle.
236///
237/// # Safety
238///
239/// `buffer` and `out_device_tag` MUST be valid, non-null pointers.
240#[no_mangle]
241pub unsafe extern "C" fn hurray_buffer_device_tag(
242    buffer: *const HurrayBuffer,
243    out_device_tag: *mut u8,
244) -> HurrayStatus {
245    catch(|| {
246        null_check!(buffer, out_device_tag);
247        // SAFETY: buffer is non-null and points to a live HurrayBuffer.
248        *out_device_tag = (*buffer).handle.device_tag().to_byte();
249        HURRAY_OK
250    })
251}
252
253/// Reads the sync mode wire byte of a buffer handle.
254///
255/// # Safety
256///
257/// `buffer` and `out_sync_mode` MUST be valid, non-null pointers.
258#[no_mangle]
259pub unsafe extern "C" fn hurray_buffer_sync_mode(
260    buffer: *const HurrayBuffer,
261    out_sync_mode: *mut u8,
262) -> HurrayStatus {
263    catch(|| {
264        null_check!(buffer, out_sync_mode);
265        // SAFETY: buffer is non-null and points to a live HurrayBuffer.
266        *out_sync_mode = (*buffer).handle.sync_mode().to_byte();
267        HURRAY_OK
268    })
269}
270
271/// Reads the memory class wire byte of a buffer handle.
272///
273/// # Safety
274///
275/// `buffer` and `out_memory_class` MUST be valid, non-null pointers.
276#[no_mangle]
277pub unsafe extern "C" fn hurray_buffer_memory_class(
278    buffer: *const HurrayBuffer,
279    out_memory_class: *mut u8,
280) -> HurrayStatus {
281    catch(|| {
282        null_check!(buffer, out_memory_class);
283        // SAFETY: buffer is non-null and points to a live HurrayBuffer.
284        *out_memory_class = (*buffer).handle.memory_class().to_byte();
285        HURRAY_OK
286    })
287}
288
289/// Reads the raw data pointer stored in a buffer handle.
290///
291/// # Safety
292///
293/// `buffer` and `out_data` MUST be valid, non-null pointers.
294#[no_mangle]
295pub unsafe extern "C" fn hurray_buffer_data_ptr(
296    buffer: *const HurrayBuffer,
297    out_data: *mut *mut c_void,
298) -> HurrayStatus {
299    catch(|| {
300        null_check!(buffer, out_data);
301        // SAFETY: buffer is non-null and points to a live HurrayBuffer.
302        *out_data = (*buffer).data;
303        HURRAY_OK
304    })
305}