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}