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
69impl HurrayBuffer {
70    /// Builds a handle around memory this crate already owns.
71    ///
72    /// Bypasses [`hurray_buffer_from_ptr`]'s wire-byte parsing and validation on purpose:
73    /// the caller here holds an already-validated [`BufferHandle`] decoded from a
74    /// descriptor, and re-deriving it from bytes would be able to fail where nothing can.
75    pub(crate) fn from_parts(
76        data: *mut c_void,
77        handle: BufferHandle,
78        release: HurrayReleaseCallback,
79        release_context: *mut c_void,
80    ) -> Box<Self> {
81        Box::new(Self {
82            #[cfg(debug_assertions)]
83            sentinel: SENTINEL_LIVE,
84            data,
85            handle,
86            release,
87            release_context,
88        })
89    }
90}
91
92// ── Constructor ───────────────────────────────────────────────────────────────
93
94/// Creates a new [`HurrayBuffer`] handle from a raw data pointer and metadata.
95///
96/// The caller retains ownership of `data` until the handle is destroyed via
97/// [`hurray_buffer_destroy`], at which point the `release` callback (if
98/// non-null) is invoked with `(data, release_context)`. If `release` is null,
99/// the caller is responsible for freeing `data` after destroying the handle.
100///
101/// # Arguments
102///
103/// - `data` — Non-null pointer to the first byte of the buffer. MUST be
104///   aligned to at least `alignment` bytes.
105/// - `byte_size` — Length of the buffer in bytes.
106/// - `alignment` — Declared alignment of `data` in bytes; MUST be a power of
107///   two and at least [`hurray_core::MIN_BUFFER_ALIGNMENT`] for non-empty
108///   buffers.
109/// - `device_tag` — Wire byte identifying the memory space (see
110///   `docs/spec/buffer-protocol.md § Device Tags`).
111/// - `sync_mode` — Wire byte for producer/consumer ordering (see
112///   `docs/spec/buffer-protocol.md § Synchronization Mode`).
113/// - `memory_class` — Wire byte for memory accessibility class.
114/// - `release` — Optional callback invoked on [`hurray_buffer_destroy`].
115/// - `release_context` — Opaque context pointer forwarded to `release`; MAY
116///   be null.
117/// - `out_handle` — Non-null pointer to a `*mut HurrayBuffer`; set to the
118///   newly allocated handle on success.
119///
120/// # Safety
121///
122/// - `data` MUST be a valid, non-null pointer to at least `byte_size` bytes.
123/// - `data` MUST remain valid until the release callback is called.
124/// - `out_handle` MUST be a valid, non-null, writable pointer.
125/// - `release_context` lifetime is caller-managed; it MUST remain valid until
126///   the release callback returns.
127#[no_mangle]
128pub unsafe extern "C" fn hurray_buffer_from_ptr(
129    data: *mut c_void,
130    byte_size: u64,
131    alignment: u32,
132    device_tag: u8,
133    sync_mode: u8,
134    memory_class: u8,
135    release: HurrayReleaseCallback,
136    release_context: *mut c_void,
137    out_handle: *mut *mut HurrayBuffer,
138) -> HurrayStatus {
139    catch(|| {
140        // release_context may be null — it is caller-controlled context.
141        null_check!(data, out_handle);
142
143        let device = match DeviceTag::from_byte(device_tag) {
144            Ok(d) => d,
145            Err(e) => return status_from_core_error(&e),
146        };
147        let mode = match SyncMode::from_byte(sync_mode) {
148            Ok(m) => m,
149            Err(e) => return status_from_core_error(&e),
150        };
151        let class = match MemoryClass::from_byte(memory_class) {
152            Ok(c) => c,
153            Err(e) => return status_from_core_error(&e),
154        };
155        let handle =
156            match BufferHandle::with_memory_class(byte_size, alignment, device, mode, class) {
157                Ok(h) => h,
158                Err(e) => return status_from_core_error(&e),
159            };
160
161        let boxed = Box::new(HurrayBuffer {
162            #[cfg(debug_assertions)]
163            sentinel: SENTINEL_LIVE,
164            data,
165            handle,
166            release,
167            release_context,
168        });
169        // SAFETY: we just allocated `boxed`; Box::into_raw transfers ownership
170        // to the caller, who must call hurray_buffer_destroy exactly once.
171        *out_handle = Box::into_raw(boxed);
172        HURRAY_OK
173    })
174}
175
176// ── Destructor ────────────────────────────────────────────────────────────────
177
178/// Destroys a [`HurrayBuffer`] handle and invokes the release callback.
179///
180/// After this call, `buffer` is no longer valid and MUST NOT be dereferenced.
181/// In debug builds, a double-free attempt returns [`HURRAY_ERR_INTERNAL`].
182///
183/// # Safety
184///
185/// - `buffer` MUST have been created by [`hurray_buffer_from_ptr`].
186/// - `buffer` MUST NOT have been previously destroyed.
187#[no_mangle]
188pub unsafe extern "C" fn hurray_buffer_destroy(buffer: *mut HurrayBuffer) -> HurrayStatus {
189    catch(|| {
190        null_check!(buffer);
191
192        #[cfg(debug_assertions)]
193        {
194            // SAFETY: buffer is non-null and points to a live HurrayBuffer;
195            // reading the sentinel before Box::from_raw for double-free detection.
196            if (*buffer).sentinel == SENTINEL_DEAD {
197                return HURRAY_ERR_INTERNAL;
198            }
199            // Write SENTINEL_DEAD through the raw pointer *before* Box::from_raw
200            // so the freed allocation slot carries the dead marker.  Writing
201            // after Box::from_raw only updates the moved-out local copy, not the
202            // original heap slot that the allocator may inspect on the second call.
203            (*buffer).sentinel = SENTINEL_DEAD;
204        }
205
206        // SAFETY: created by hurray_buffer_from_ptr via Box::into_raw; caller
207        // guarantees this is the first and only destroy call.
208        let mut b = Box::from_raw(buffer);
209
210        // SAFETY: caller provided `release` and `release_context` with valid
211        // lifetimes; we invoke the callback exactly once at destruction time.
212        if let Some(f) = b.release.take() {
213            f(b.data, b.release_context);
214        }
215
216        HURRAY_OK
217    })
218}
219
220// ── Accessors ─────────────────────────────────────────────────────────────────
221
222/// Reads the byte size of a buffer handle.
223///
224/// # Safety
225///
226/// `buffer` and `out_byte_size` MUST be valid, non-null pointers.
227#[no_mangle]
228pub unsafe extern "C" fn hurray_buffer_byte_size(
229    buffer: *const HurrayBuffer,
230    out_byte_size: *mut u64,
231) -> HurrayStatus {
232    catch(|| {
233        null_check!(buffer, out_byte_size);
234        // SAFETY: buffer is non-null and points to a live HurrayBuffer.
235        *out_byte_size = (*buffer).handle.byte_size();
236        HURRAY_OK
237    })
238}
239
240/// Reads the declared alignment of a buffer handle (in bytes).
241///
242/// # Safety
243///
244/// `buffer` and `out_alignment` MUST be valid, non-null pointers.
245#[no_mangle]
246pub unsafe extern "C" fn hurray_buffer_alignment(
247    buffer: *const HurrayBuffer,
248    out_alignment: *mut u32,
249) -> HurrayStatus {
250    catch(|| {
251        null_check!(buffer, out_alignment);
252        // SAFETY: buffer is non-null and points to a live HurrayBuffer.
253        *out_alignment = (*buffer).handle.alignment();
254        HURRAY_OK
255    })
256}
257
258/// Reads the device tag wire byte of a buffer handle.
259///
260/// # Safety
261///
262/// `buffer` and `out_device_tag` MUST be valid, non-null pointers.
263#[no_mangle]
264pub unsafe extern "C" fn hurray_buffer_device_tag(
265    buffer: *const HurrayBuffer,
266    out_device_tag: *mut u8,
267) -> HurrayStatus {
268    catch(|| {
269        null_check!(buffer, out_device_tag);
270        // SAFETY: buffer is non-null and points to a live HurrayBuffer.
271        *out_device_tag = (*buffer).handle.device_tag().to_byte();
272        HURRAY_OK
273    })
274}
275
276/// Reads the sync mode wire byte of a buffer handle.
277///
278/// # Safety
279///
280/// `buffer` and `out_sync_mode` MUST be valid, non-null pointers.
281#[no_mangle]
282pub unsafe extern "C" fn hurray_buffer_sync_mode(
283    buffer: *const HurrayBuffer,
284    out_sync_mode: *mut u8,
285) -> HurrayStatus {
286    catch(|| {
287        null_check!(buffer, out_sync_mode);
288        // SAFETY: buffer is non-null and points to a live HurrayBuffer.
289        *out_sync_mode = (*buffer).handle.sync_mode().to_byte();
290        HURRAY_OK
291    })
292}
293
294/// Reads the memory class wire byte of a buffer handle.
295///
296/// # Safety
297///
298/// `buffer` and `out_memory_class` MUST be valid, non-null pointers.
299#[no_mangle]
300pub unsafe extern "C" fn hurray_buffer_memory_class(
301    buffer: *const HurrayBuffer,
302    out_memory_class: *mut u8,
303) -> HurrayStatus {
304    catch(|| {
305        null_check!(buffer, out_memory_class);
306        // SAFETY: buffer is non-null and points to a live HurrayBuffer.
307        *out_memory_class = (*buffer).handle.memory_class().to_byte();
308        HURRAY_OK
309    })
310}
311
312/// Reads the raw data pointer stored in a buffer handle.
313///
314/// # Safety
315///
316/// `buffer` and `out_data` MUST be valid, non-null pointers.
317#[no_mangle]
318pub unsafe extern "C" fn hurray_buffer_data_ptr(
319    buffer: *const HurrayBuffer,
320    out_data: *mut *mut c_void,
321) -> HurrayStatus {
322    catch(|| {
323        null_check!(buffer, out_data);
324        // SAFETY: buffer is non-null and points to a live HurrayBuffer.
325        *out_data = (*buffer).data;
326        HURRAY_OK
327    })
328}