Skip to main content

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}