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}