hurray_ffi/tensor_context.rs
1//! Opaque carrier for everything a native-protocol capsule holds beyond its
2//! buffers (ADR-034).
3//!
4//! A capsule carries two things: its pointer is a
5//! [`HurrayBufferList`](crate::HurrayBufferList), and its context is a
6//! [`HurrayTensorContext`]. The list holds the bytes; the context holds the
7//! encoded tensor descriptor that says what those bytes *are* — element type,
8//! shape, layout, quantization — plus the ABI version of the build that produced
9//! them.
10//!
11//! Before ADR-034 the context was a Rust struct private to `hurray-python`, so
12//! only `hurray-python` could read it. The buffers crossed the language boundary
13//! and the descriptor did not, which made the protocol's full-fidelity promise
14//! true between two Python peers and false for every other binding.
15//!
16//! ## Reading one
17//!
18//! Check the version first, always — see [`hurray_tensor_context_abi_version`].
19//! Every other accessor assumes a caller that has done so.
20//!
21//! ## The owner
22//!
23//! A context keeps its producer's tensor alive: the buffers point into memory
24//! something else owns. That something is passed in as an opaque `owner` pointer
25//! with an `owner_release` callback, and this crate never interprets either —
26//! which is what lets `hurray-python` park a Python reference there without any
27//! Python type reaching the C ABI.
28
29use std::ffi::c_void;
30
31use crate::{
32 panic::{catch, null_check},
33 status::{HurrayStatus, HURRAY_ERR_INTERNAL, HURRAY_ERR_NULL_POINTER, HURRAY_OK},
34};
35
36// ── Debug sentinel constants ──────────────────────────────────────────────────
37
38/// Sentinel placed in `HurrayTensorContext.sentinel` on allocation (debug builds only).
39#[cfg(debug_assertions)]
40const SENTINEL_LIVE: u64 = 0x1157_C7C0_0000_0000;
41
42/// Sentinel written before the allocation is freed, to detect a double destroy.
43#[cfg(debug_assertions)]
44const SENTINEL_DEAD: u64 = 0x1157_C7C0_DEAD_DEAD;
45
46// ── Callback type ─────────────────────────────────────────────────────────────
47
48/// Callback invoked exactly once by [`hurray_tensor_context_destroy`] to release
49/// whatever keeps the tensor's memory alive.
50///
51/// The argument is the `owner` pointer passed to [`hurray_tensor_context_new`],
52/// with the exact value provided at construction time.
53///
54/// Implementations MUST NOT call back into the Hurray C ABI from within this
55/// callback; doing so may cause deadlocks or use-after-free.
56pub type HurrayOwnerReleaseFn = Option<unsafe extern "C" fn(*mut c_void)>;
57
58// ── HurrayTensorContext ───────────────────────────────────────────────────────
59
60/// Opaque handle to a capsule's tensor context: descriptor bytes, ABI version,
61/// and an owner reference.
62///
63/// Like every other handle in this ABI, this struct is **not** `#[repr(C)]`; its
64/// layout is an implementation detail and callers MUST treat it as a black-box
65/// pointer. Publishing the layout would buy nothing — a caller holding a capsule
66/// already links this library to read its buffer list — and would freeze the
67/// layout for the life of the major version (ADR-034 § 2).
68pub struct HurrayTensorContext {
69 /// Double-destroy sentinel (debug builds only).
70 #[cfg(debug_assertions)]
71 sentinel: u64,
72 /// ABI version of the build that produced this context.
73 abi_version: u32,
74 /// Owned copy of the encoded tensor descriptor.
75 ///
76 /// Copied rather than borrowed: a borrow would tie this handle's validity to
77 /// a buffer the producer may drop, and a descriptor is small beside the
78 /// tensor it describes.
79 descriptor: Vec<u8>,
80 /// Opaque pointer to whatever owns the tensor's memory. Never interpreted.
81 owner: *mut c_void,
82 /// Callback that releases `owner`, invoked exactly once on destroy.
83 owner_release: HurrayOwnerReleaseFn,
84}
85
86// SAFETY: mirrors the `HurrayBuffer` contract — the C ABI is single-thread-at-a-time
87// per handle, so `Send` only permits moving the box across threads during
88// construction and destruction, never concurrent access.
89unsafe impl Send for HurrayTensorContext {}
90
91// ── Constructor ───────────────────────────────────────────────────────────────
92
93/// Creates a [`HurrayTensorContext`] owning a copy of `descriptor_bytes`.
94///
95/// `abi_version` MUST be the producing build's `HURRAY_C_ABI_VERSION`; a consumer
96/// compares it against its own before trusting anything else about the capsule.
97///
98/// `owner` and `owner_release` are optional and opaque. When `owner_release` is
99/// non-null it is invoked exactly once, with `owner`, during
100/// [`hurray_tensor_context_destroy`].
101///
102/// The caller owns the returned handle and MUST destroy it exactly once.
103///
104/// # Safety
105///
106/// - `out_ctx` MUST be a valid, non-null, writable pointer.
107/// - `descriptor_bytes` MUST point to at least `descriptor_len` readable bytes,
108/// unless `descriptor_len` is `0`, in which case it MAY be null.
109/// - `owner` MUST remain valid until `owner_release` is invoked.
110///
111/// # Examples
112///
113/// ```
114/// use hurray_ffi::tensor_context::{
115/// hurray_tensor_context_destroy, hurray_tensor_context_new,
116/// };
117/// use hurray_ffi::{HurrayTensorContext, HURRAY_C_ABI_VERSION, HURRAY_OK};
118///
119/// let descriptor = [0x48u8, 0x52, 0x52, 0x59]; // "HRRY"
120/// let mut ctx: *mut HurrayTensorContext = std::ptr::null_mut();
121/// assert_eq!(
122/// unsafe {
123/// hurray_tensor_context_new(
124/// HURRAY_C_ABI_VERSION,
125/// descriptor.as_ptr(),
126/// descriptor.len() as u64,
127/// std::ptr::null_mut(),
128/// None,
129/// &mut ctx,
130/// )
131/// },
132/// HURRAY_OK,
133/// );
134///
135/// assert_eq!(unsafe { hurray_tensor_context_destroy(&mut ctx) }, HURRAY_OK);
136/// assert!(ctx.is_null()); // destroy nulls the caller's pointer
137/// ```
138#[no_mangle]
139pub unsafe extern "C" fn hurray_tensor_context_new(
140 abi_version: u32,
141 descriptor_bytes: *const u8,
142 descriptor_len: u64,
143 owner: *mut c_void,
144 owner_release: HurrayOwnerReleaseFn,
145 out_ctx: *mut *mut HurrayTensorContext,
146) -> HurrayStatus {
147 catch(|| {
148 null_check!(out_ctx);
149
150 // A null pointer is only meaningful for an empty descriptor; anything
151 // else is a caller bug that would otherwise become an unsound read.
152 if descriptor_bytes.is_null() && descriptor_len != 0 {
153 return HURRAY_ERR_NULL_POINTER;
154 }
155
156 let descriptor = if descriptor_len == 0 {
157 Vec::new()
158 } else {
159 // SAFETY: caller guarantees descriptor_len readable bytes at descriptor_bytes.
160 std::slice::from_raw_parts(descriptor_bytes, descriptor_len as usize).to_vec()
161 };
162
163 let boxed = Box::new(HurrayTensorContext {
164 #[cfg(debug_assertions)]
165 sentinel: SENTINEL_LIVE,
166 abi_version,
167 descriptor,
168 owner,
169 owner_release,
170 });
171 // SAFETY: just allocated; ownership transfers to the caller, who must
172 // call hurray_tensor_context_destroy exactly once.
173 *out_ctx = Box::into_raw(boxed);
174 HURRAY_OK
175 })
176}
177
178// ── Accessors ─────────────────────────────────────────────────────────────────
179
180/// Reads the ABI version recorded by the producing build.
181///
182/// **Call this first.** It is the one accessor guaranteed to work on a context
183/// produced by any version of this ABI; every other accessor assumes a caller
184/// that has already compared this value against its own
185/// [`HURRAY_C_ABI_VERSION`](crate::HURRAY_C_ABI_VERSION) and found it
186/// compatible (ADR-034 § 4). That ordering is what lets later versions add
187/// accessors without breaking older consumers.
188///
189/// # Safety
190///
191/// `ctx` MUST be a live handle from [`hurray_tensor_context_new`], and `out`
192/// MUST be a valid, writable pointer.
193///
194/// # Examples
195///
196/// ```
197/// use hurray_ffi::tensor_context::{
198/// hurray_tensor_context_abi_version, hurray_tensor_context_destroy,
199/// hurray_tensor_context_new,
200/// };
201/// use hurray_ffi::{HurrayTensorContext, HURRAY_C_ABI_VERSION, HURRAY_OK};
202///
203/// let mut ctx: *mut HurrayTensorContext = std::ptr::null_mut();
204/// unsafe {
205/// hurray_tensor_context_new(
206/// HURRAY_C_ABI_VERSION, std::ptr::null(), 0, std::ptr::null_mut(), None, &mut ctx,
207/// );
208/// }
209///
210/// let mut version: u32 = 0;
211/// assert_eq!(
212/// unsafe { hurray_tensor_context_abi_version(ctx, &mut version) },
213/// HURRAY_OK,
214/// );
215/// assert_eq!(version, HURRAY_C_ABI_VERSION);
216///
217/// unsafe { hurray_tensor_context_destroy(&mut ctx) };
218/// ```
219#[no_mangle]
220pub unsafe extern "C" fn hurray_tensor_context_abi_version(
221 ctx: *const HurrayTensorContext,
222 out: *mut u32,
223) -> HurrayStatus {
224 catch(|| {
225 null_check!(ctx, out);
226 // SAFETY: ctx is non-null and points to a live HurrayTensorContext.
227 *out = (*ctx).abi_version;
228 HURRAY_OK
229 })
230}
231
232/// Borrows the encoded tensor descriptor.
233///
234/// The returned pointer is owned by `ctx` and is valid until the context is
235/// destroyed; the caller MUST NOT free it. Decode it with
236/// [`hurray_descriptor_decode`](crate::descriptor::hurray_descriptor_decode) to
237/// read the element type, shape, layout, and optional sections.
238///
239/// `out_len` is `0` — and `out_bytes` null — for a context created without a
240/// descriptor.
241///
242/// # Safety
243///
244/// `ctx` MUST be a live handle from [`hurray_tensor_context_new`], and both out
245/// pointers MUST be valid and writable.
246///
247/// # Examples
248///
249/// ```
250/// use hurray_ffi::tensor_context::{
251/// hurray_tensor_context_descriptor, hurray_tensor_context_destroy,
252/// hurray_tensor_context_new,
253/// };
254/// use hurray_ffi::{HurrayTensorContext, HURRAY_C_ABI_VERSION, HURRAY_OK};
255///
256/// let descriptor = [0x48u8, 0x52, 0x52, 0x59];
257/// let mut ctx: *mut HurrayTensorContext = std::ptr::null_mut();
258/// unsafe {
259/// hurray_tensor_context_new(
260/// HURRAY_C_ABI_VERSION,
261/// descriptor.as_ptr(),
262/// descriptor.len() as u64,
263/// std::ptr::null_mut(),
264/// None,
265/// &mut ctx,
266/// );
267/// }
268///
269/// let mut bytes: *const u8 = std::ptr::null();
270/// let mut len: u64 = 0;
271/// assert_eq!(
272/// unsafe { hurray_tensor_context_descriptor(ctx, &mut bytes, &mut len) },
273/// HURRAY_OK,
274/// );
275/// assert_eq!(len, 4);
276/// assert_eq!(unsafe { std::slice::from_raw_parts(bytes, len as usize) }, &descriptor);
277///
278/// unsafe { hurray_tensor_context_destroy(&mut ctx) };
279/// ```
280#[no_mangle]
281pub unsafe extern "C" fn hurray_tensor_context_descriptor(
282 ctx: *const HurrayTensorContext,
283 out_bytes: *mut *const u8,
284 out_len: *mut u64,
285) -> HurrayStatus {
286 catch(|| {
287 null_check!(ctx, out_bytes, out_len);
288 // SAFETY: ctx is non-null and points to a live HurrayTensorContext.
289 let descriptor = &(*ctx).descriptor;
290 // An empty descriptor reports a null pointer rather than a dangling
291 // one-past-the-end address, so a caller that ignores the length cannot
292 // read from it.
293 *out_bytes = if descriptor.is_empty() {
294 std::ptr::null()
295 } else {
296 descriptor.as_ptr()
297 };
298 *out_len = descriptor.len() as u64;
299 HURRAY_OK
300 })
301}
302
303// ── Destructor ────────────────────────────────────────────────────────────────
304
305/// Destroys a [`HurrayTensorContext`], invoking its owner-release callback, and
306/// nulls the caller's pointer.
307///
308/// Destroying a null handle is a no-op that returns [`HURRAY_OK`], so the call
309/// is safe to make unconditionally on a cleanup path.
310///
311/// # Safety
312///
313/// - `ctx` MUST be a valid, writable pointer to a handle slot.
314/// - The handle it points to MUST come from [`hurray_tensor_context_new`] and
315/// MUST NOT have been destroyed already.
316///
317/// # Examples
318///
319/// ```
320/// use hurray_ffi::tensor_context::{hurray_tensor_context_destroy, hurray_tensor_context_new};
321/// use hurray_ffi::{HurrayTensorContext, HURRAY_C_ABI_VERSION, HURRAY_OK};
322///
323/// let mut ctx: *mut HurrayTensorContext = std::ptr::null_mut();
324/// unsafe {
325/// hurray_tensor_context_new(
326/// HURRAY_C_ABI_VERSION, std::ptr::null(), 0, std::ptr::null_mut(), None, &mut ctx,
327/// );
328/// }
329///
330/// assert_eq!(unsafe { hurray_tensor_context_destroy(&mut ctx) }, HURRAY_OK);
331/// assert!(ctx.is_null());
332/// // Destroying again is a no-op, not a double free.
333/// assert_eq!(unsafe { hurray_tensor_context_destroy(&mut ctx) }, HURRAY_OK);
334/// ```
335#[no_mangle]
336pub unsafe extern "C" fn hurray_tensor_context_destroy(
337 ctx: *mut *mut HurrayTensorContext,
338) -> HurrayStatus {
339 catch(|| {
340 null_check!(ctx);
341
342 // SAFETY: ctx is a valid, writable pointer to a handle slot.
343 let ptr = *ctx;
344 if ptr.is_null() {
345 return HURRAY_OK;
346 }
347
348 #[cfg(debug_assertions)]
349 {
350 // SAFETY: ptr is non-null and points to a live HurrayTensorContext.
351 if (*ptr).sentinel == SENTINEL_DEAD {
352 return HURRAY_ERR_INTERNAL;
353 }
354 // Written through the raw pointer before Box::from_raw so the freed
355 // slot carries the dead marker; writing after would only update the
356 // moved-out local copy.
357 (*ptr).sentinel = SENTINEL_DEAD;
358 }
359
360 // Take the callback and its argument before freeing, then null the
361 // caller's slot, so a re-entrant or panicking callback cannot reach a
362 // handle that is mid-destruction.
363 // SAFETY: ptr is non-null and points to a live HurrayTensorContext.
364 let release = (*ptr).owner_release.take();
365 let owner = std::mem::replace(&mut (*ptr).owner, std::ptr::null_mut());
366
367 // SAFETY: created by hurray_tensor_context_new via Box::into_raw; this is
368 // the first and only destroy.
369 drop(Box::from_raw(ptr));
370 *ctx = std::ptr::null_mut();
371
372 if let Some(release) = release {
373 // SAFETY: the caller guaranteed at construction time that owner stays
374 // valid until this call, and this is the only call.
375 release(owner);
376 }
377 HURRAY_OK
378 })
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 /// Builds a context over `bytes`, runs `f`, and destroys it.
386 fn with_context(bytes: &[u8], f: impl FnOnce(*mut HurrayTensorContext)) {
387 let mut ctx: *mut HurrayTensorContext = std::ptr::null_mut();
388 let status = unsafe {
389 hurray_tensor_context_new(
390 crate::HURRAY_C_ABI_VERSION,
391 bytes.as_ptr(),
392 bytes.len() as u64,
393 std::ptr::null_mut(),
394 None,
395 &mut ctx,
396 )
397 };
398 assert_eq!(status, HURRAY_OK);
399 f(ctx);
400 assert_eq!(
401 unsafe { hurray_tensor_context_destroy(&mut ctx) },
402 HURRAY_OK
403 );
404 assert!(ctx.is_null());
405 }
406
407 #[test]
408 fn descriptor_survives_the_round_trip() {
409 let bytes = [1u8, 2, 3, 4, 5];
410 with_context(&bytes, |ctx| {
411 let mut out: *const u8 = std::ptr::null();
412 let mut len: u64 = 0;
413 assert_eq!(
414 unsafe { hurray_tensor_context_descriptor(ctx, &mut out, &mut len) },
415 HURRAY_OK
416 );
417 assert_eq!(len, 5);
418 // SAFETY: the accessor returned a live borrow of the context's copy.
419 assert_eq!(
420 unsafe { std::slice::from_raw_parts(out, len as usize) },
421 &bytes
422 );
423 });
424 }
425
426 #[test]
427 fn the_descriptor_is_copied_not_borrowed() {
428 let mut ctx: *mut HurrayTensorContext = std::ptr::null_mut();
429 {
430 // Dropped before the context is read: a borrow would dangle here.
431 // Heap, not an array: freeing the allocation is what makes a borrow a
432 // real use-after-free rather than a stale stack read that may pass.
433 #[allow(clippy::useless_vec)]
434 let transient = vec![9u8, 8, 7];
435 unsafe {
436 hurray_tensor_context_new(
437 crate::HURRAY_C_ABI_VERSION,
438 transient.as_ptr(),
439 transient.len() as u64,
440 std::ptr::null_mut(),
441 None,
442 &mut ctx,
443 );
444 }
445 }
446
447 let mut out: *const u8 = std::ptr::null();
448 let mut len: u64 = 0;
449 unsafe { hurray_tensor_context_descriptor(ctx, &mut out, &mut len) };
450 // SAFETY: the context owns its copy, which outlives `transient`.
451 assert_eq!(
452 unsafe { std::slice::from_raw_parts(out, len as usize) },
453 &[9, 8, 7]
454 );
455 unsafe { hurray_tensor_context_destroy(&mut ctx) };
456 }
457
458 #[test]
459 fn abi_version_round_trips() {
460 with_context(&[0u8], |ctx| {
461 let mut version = 0u32;
462 assert_eq!(
463 unsafe { hurray_tensor_context_abi_version(ctx, &mut version) },
464 HURRAY_OK
465 );
466 assert_eq!(version, crate::HURRAY_C_ABI_VERSION);
467 });
468 }
469
470 #[test]
471 fn an_empty_descriptor_reports_null_and_zero() {
472 let mut ctx: *mut HurrayTensorContext = std::ptr::null_mut();
473 assert_eq!(
474 unsafe {
475 hurray_tensor_context_new(
476 crate::HURRAY_C_ABI_VERSION,
477 std::ptr::null(),
478 0,
479 std::ptr::null_mut(),
480 None,
481 &mut ctx,
482 )
483 },
484 HURRAY_OK
485 );
486
487 let mut out: *const u8 = std::ptr::null();
488 let mut len: u64 = 1;
489 unsafe { hurray_tensor_context_descriptor(ctx, &mut out, &mut len) };
490 assert!(out.is_null(), "no dangling one-past-the-end pointer");
491 assert_eq!(len, 0);
492 unsafe { hurray_tensor_context_destroy(&mut ctx) };
493 }
494
495 #[test]
496 fn a_null_descriptor_with_a_nonzero_length_is_rejected() {
497 let mut ctx: *mut HurrayTensorContext = std::ptr::null_mut();
498 assert_eq!(
499 unsafe {
500 hurray_tensor_context_new(
501 crate::HURRAY_C_ABI_VERSION,
502 std::ptr::null(),
503 8,
504 std::ptr::null_mut(),
505 None,
506 &mut ctx,
507 )
508 },
509 HURRAY_ERR_NULL_POINTER
510 );
511 assert!(ctx.is_null());
512 }
513
514 #[test]
515 fn null_arguments_are_rejected_rather_than_dereferenced() {
516 assert_eq!(
517 unsafe {
518 hurray_tensor_context_new(
519 crate::HURRAY_C_ABI_VERSION,
520 std::ptr::null(),
521 0,
522 std::ptr::null_mut(),
523 None,
524 std::ptr::null_mut(),
525 )
526 },
527 HURRAY_ERR_NULL_POINTER
528 );
529 let mut version = 0u32;
530 assert_eq!(
531 unsafe { hurray_tensor_context_abi_version(std::ptr::null(), &mut version) },
532 HURRAY_ERR_NULL_POINTER
533 );
534 assert_eq!(
535 unsafe { hurray_tensor_context_destroy(std::ptr::null_mut()) },
536 HURRAY_ERR_NULL_POINTER
537 );
538 }
539
540 // ── Owner release ─────────────────────────────────────────────────────────
541
542 static RELEASE_COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
543
544 unsafe extern "C" fn count_release(owner: *mut c_void) {
545 assert_eq!(owner as usize, 0xABCD, "owner arrives unchanged");
546 RELEASE_COUNT.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
547 }
548
549 #[test]
550 fn the_owner_is_released_exactly_once() {
551 RELEASE_COUNT.store(0, std::sync::atomic::Ordering::SeqCst);
552 let mut ctx: *mut HurrayTensorContext = std::ptr::null_mut();
553 unsafe {
554 hurray_tensor_context_new(
555 crate::HURRAY_C_ABI_VERSION,
556 std::ptr::null(),
557 0,
558 0xABCD as *mut c_void,
559 Some(count_release),
560 &mut ctx,
561 );
562 }
563 assert_eq!(RELEASE_COUNT.load(std::sync::atomic::Ordering::SeqCst), 0);
564
565 unsafe { hurray_tensor_context_destroy(&mut ctx) };
566 assert_eq!(RELEASE_COUNT.load(std::sync::atomic::Ordering::SeqCst), 1);
567
568 // The handle is null now, so a second destroy must not release again.
569 unsafe { hurray_tensor_context_destroy(&mut ctx) };
570 assert_eq!(RELEASE_COUNT.load(std::sync::atomic::Ordering::SeqCst), 1);
571 }
572}