hurray_ffi/tensor.rs
1//! A tensor handle: a descriptor and its buffers, paired structurally.
2//!
3//! A tensor *is* a descriptor plus its buffers, and the C ABI used to make callers keep
4//! the two halves paired by convention — a consumer that fetched one without the other
5//! got either bytes it could not interpret or a description of bytes it did not have
6//! ([#174](https://github.com/pgillet/hurray/issues/174)). [`HurrayTensor`] owns both, so
7//! the pairing is structural: a caller holding the handle holds the description of the
8//! bytes it points at.
9//!
10//! Every path that produces a tensor produces this handle (ADR-043): a stream read, a
11//! file read, and a native-protocol capsule, which carries one `HurrayTensor *` and no
12//! context. One destroy call, and its internal order cannot be got wrong.
13//!
14//! ## Reading one
15//!
16//! Check the version first, always — see [`hurray_tensor_abi_version`]. Every other
17//! accessor assumes a caller that has done so.
18//!
19//! ## The owner
20//!
21//! A producer's buffers point into memory something else owns, and that something is
22//! passed in as an opaque `owner` pointer with an `owner_release` callback. This crate
23//! never interprets either, which is what lets `hurray-python` park a Python reference
24//! there without any Python type reaching the C ABI. A tensor from a read carries
25//! neither: it owns its buffers outright.
26
27use std::ffi::c_void;
28
29use bytes::Bytes;
30use hurray_core::TensorDescriptor;
31
32use crate::{
33 buffer::HurrayBuffer,
34 buffer_list::{hurray_buffer_list_destroy, HurrayBufferList},
35 descriptor::{hurray_descriptor_destroy, HurrayDescriptor},
36 panic::{catch, null_check},
37 status::{
38 status_from_core_error, HurrayStatus, HURRAY_ERR_INTERNAL, HURRAY_ERR_NULL_POINTER,
39 HURRAY_OK,
40 },
41};
42
43// ── Debug sentinel constants ──────────────────────────────────────────────────
44
45/// Sentinel placed in `HurrayTensor.sentinel` on allocation (debug builds only).
46#[cfg(debug_assertions)]
47const SENTINEL_LIVE: u64 = 0x7E43_0000_7E43_0000;
48
49/// Sentinel written before the allocation is freed, to detect a double destroy in debug
50/// builds.
51#[cfg(debug_assertions)]
52const SENTINEL_DEAD: u64 = 0x7E43_DEAD_7E43_DEAD;
53
54// ── Callback type ─────────────────────────────────────────────────────────────
55
56/// Callback invoked exactly once by [`hurray_tensor_destroy`] to release whatever keeps
57/// the tensor's memory alive.
58///
59/// The argument is the `owner` pointer passed to [`hurray_tensor_new`], with the exact
60/// value provided at construction time.
61///
62/// Implementations MUST NOT call back into the Hurray C ABI from within this callback;
63/// doing so may cause deadlocks or use-after-free.
64pub type HurrayOwnerReleaseFn = Option<unsafe extern "C" fn(*mut c_void)>;
65
66// ── HurrayTensor ──────────────────────────────────────────────────────────────
67
68/// Opaque handle to a tensor: its descriptor and its buffers.
69///
70/// Not `#[repr(C)]`; the layout is an implementation detail. The handle **owns** both
71/// halves. [`hurray_tensor_descriptor`] and [`hurray_tensor_buffers`] hand out *borrowed*
72/// pointers — valid until [`hurray_tensor_destroy`], and never to be destroyed
73/// individually.
74pub struct HurrayTensor {
75 /// Double-destroy sentinel (debug builds only).
76 #[cfg(debug_assertions)]
77 sentinel: u64,
78 /// ABI version of the build that produced this tensor.
79 abi_version: u32,
80 /// Owned; nulled as soon as it is destroyed so a re-entrant destroy cannot free twice.
81 descriptor: *mut HurrayDescriptor,
82 /// Owned, in descriptor buffer-table order.
83 buffers: *mut HurrayBufferList,
84 /// Opaque pointer to whatever owns the tensor's memory. Never interpreted.
85 owner: *mut c_void,
86 /// Callback that releases `owner`, invoked exactly once on destroy.
87 owner_release: HurrayOwnerReleaseFn,
88}
89
90// SAFETY: mirrors the HurrayBuffer and HurrayBufferList contract — the C ABI is
91// single-thread-at-a-time per handle, so Send only permits moving the box across threads
92// during construction and destruction, never concurrent access.
93unsafe impl Send for HurrayTensor {}
94
95impl HurrayTensor {
96 /// Takes ownership of a descriptor handle and a buffer list.
97 pub(crate) fn from_parts(
98 abi_version: u32,
99 descriptor: *mut HurrayDescriptor,
100 buffers: *mut HurrayBufferList,
101 owner: *mut c_void,
102 owner_release: HurrayOwnerReleaseFn,
103 ) -> Box<Self> {
104 Box::new(Self {
105 #[cfg(debug_assertions)]
106 sentinel: SENTINEL_LIVE,
107 abi_version,
108 descriptor,
109 buffers,
110 owner,
111 owner_release,
112 })
113 }
114
115 /// Builds a handle from an already-decoded descriptor, for a Rust producer.
116 ///
117 /// The Rust-side twin of [`hurray_tensor_new`], which decodes bytes a C caller
118 /// supplied. `hurray-python` holds a `TensorDescriptor` already, so encoding it only
119 /// for the constructor to decode it straight back would be a round trip through bytes
120 /// neither side wants (ADR-043).
121 ///
122 /// # Safety
123 ///
124 /// - `buffers` MUST be a live list from
125 /// [`hurray_buffer_list_new`](crate::hurray_buffer_list_new) that nothing else owns;
126 /// ownership transfers to the returned handle.
127 /// - `owner` MUST remain valid until `owner_release` is invoked.
128 /// - The returned pointer MUST be destroyed exactly once, with
129 /// [`hurray_tensor_destroy`].
130 pub unsafe fn from_core(
131 abi_version: u32,
132 descriptor: TensorDescriptor,
133 buffers: *mut HurrayBufferList,
134 owner: *mut c_void,
135 owner_release: HurrayOwnerReleaseFn,
136 ) -> *mut HurrayTensor {
137 let descriptor = Box::into_raw(HurrayDescriptor::from_descriptor(descriptor));
138 Box::into_raw(Self::from_parts(
139 abi_version,
140 descriptor,
141 buffers,
142 owner,
143 owner_release,
144 ))
145 }
146
147 /// Borrows the decoded descriptor as the `hurray-core` type, for a Rust consumer.
148 ///
149 /// The Rust-side twin of [`hurray_tensor_descriptor`], which hands a C caller the
150 /// opaque handle to read fields through. A Rust consumer — `hurray-python` rebuilding
151 /// its own tensor — wants the type itself, and re-encoding it to decode it again
152 /// would be the round trip [`HurrayTensor::from_core`] exists to avoid.
153 ///
154 /// Returns `None` only for a handle mid-destruction, which a caller holding a live
155 /// tensor never has.
156 pub fn core_descriptor(&self) -> Option<&TensorDescriptor> {
157 if self.descriptor.is_null() {
158 return None;
159 }
160 // SAFETY: non-null and owned by this handle, which the caller holds alive.
161 Some(unsafe { (*self.descriptor).inner() })
162 }
163}
164
165// ── Constructor ───────────────────────────────────────────────────────────────
166
167/// Creates a [`HurrayTensor`] from an encoded descriptor and a finished buffer list.
168///
169/// `abi_version` MUST be the producing build's
170/// [`HURRAY_C_ABI_VERSION`](crate::HURRAY_C_ABI_VERSION); a consumer compares it against
171/// its own before trusting anything else about the handle.
172///
173/// `descriptor_bytes` is **decoded during this call** and only the decoded form is kept,
174/// so a producer cannot export a tensor whose descriptor does not parse: bytes that fail
175/// to decode are rejected with the status the decode failed with. A null pointer or a
176/// `descriptor_len` of `0` is rejected with
177/// [`HURRAY_ERR_NULL_POINTER`] — a tensor with no
178/// descriptor is a carrier for bytes nothing can interpret.
179///
180/// On success `buffers` is **owned by the tensor**, and the caller MUST NOT push into it,
181/// destroy it, or use the handle at all. On failure ownership stays with the caller, who
182/// MUST destroy it — the discipline
183/// [`hurray_buffer_list_push`](crate::buffer_list::hurray_buffer_list_push) already
184/// follows.
185///
186/// `owner` and `owner_release` are optional and opaque. When `owner_release` is non-null
187/// it is invoked exactly once, with `owner`, during [`hurray_tensor_destroy`].
188///
189/// # Safety
190///
191/// - `out_tensor` MUST be a valid, non-null, writable pointer.
192/// - `descriptor_bytes` MUST point to at least `descriptor_len` readable bytes.
193/// - `buffers` MUST be a live list from
194/// [`hurray_buffer_list_new`](crate::hurray_buffer_list_new) that nothing else owns.
195/// - `owner` MUST remain valid until `owner_release` is invoked.
196///
197/// # Examples
198///
199/// ```
200/// use hurray_core::{
201/// BufferHandle, DeviceTag, ElementType, LayoutDescriptor, Shape, SyncMode,
202/// TensorDescriptor, MIN_BUFFER_ALIGNMENT,
203/// };
204/// use hurray_ffi::tensor::{hurray_tensor_destroy, hurray_tensor_new};
205/// use hurray_ffi::{
206/// hurray_buffer_list_new, HurrayBufferList, HurrayTensor, HURRAY_C_ABI_VERSION, HURRAY_OK,
207/// };
208///
209/// let handle = BufferHandle::new(
210/// 16,
211/// MIN_BUFFER_ALIGNMENT,
212/// DeviceTag::Cpu,
213/// SyncMode::ProducerSynced,
214/// )?;
215/// let descriptor = TensorDescriptor::new(
216/// 1,
217/// 0,
218/// ElementType::Float32,
219/// Shape::new(vec![4])?,
220/// 0,
221/// LayoutDescriptor::RowMajor,
222/// vec![handle],
223/// None,
224/// None,
225/// None,
226/// None,
227/// )?;
228/// let encoded = descriptor.encode()?;
229///
230/// // An empty list is enough to show the ownership transfer; a real producer pushes
231/// // one buffer per descriptor buffer-table entry first.
232/// let mut buffers: *mut HurrayBufferList = std::ptr::null_mut();
233/// assert_eq!(unsafe { hurray_buffer_list_new(1, &mut buffers) }, HURRAY_OK);
234///
235/// let mut tensor: *mut HurrayTensor = std::ptr::null_mut();
236/// assert_eq!(
237/// unsafe {
238/// hurray_tensor_new(
239/// HURRAY_C_ABI_VERSION,
240/// encoded.as_ptr(),
241/// encoded.len() as u64,
242/// buffers, // ownership transfers on success
243/// std::ptr::null_mut(),
244/// None,
245/// &mut tensor,
246/// )
247/// },
248/// HURRAY_OK,
249/// );
250///
251/// // One call ends the whole tensor: descriptor, buffers, and the owner reference.
252/// assert_eq!(unsafe { hurray_tensor_destroy(&mut tensor) }, HURRAY_OK);
253/// assert!(tensor.is_null()); // destroy nulls the caller's pointer
254/// # Ok::<(), Box<dyn std::error::Error>>(())
255/// ```
256#[no_mangle]
257pub unsafe extern "C" fn hurray_tensor_new(
258 abi_version: u32,
259 descriptor_bytes: *const u8,
260 descriptor_len: u64,
261 buffers: *mut HurrayBufferList,
262 owner: *mut c_void,
263 owner_release: HurrayOwnerReleaseFn,
264 out_tensor: *mut *mut HurrayTensor,
265) -> HurrayStatus {
266 catch(|| {
267 null_check!(out_tensor, descriptor_bytes, buffers);
268
269 // A zero-length descriptor is refused rather than stored: the handle keeps only
270 // the decoded form, and there is nothing to decode (ADR-043 § 6).
271 if descriptor_len == 0 {
272 return HURRAY_ERR_NULL_POINTER;
273 }
274
275 // SAFETY: caller guarantees descriptor_len readable bytes at descriptor_bytes.
276 let slice = std::slice::from_raw_parts(descriptor_bytes, descriptor_len as usize);
277 let descriptor = match TensorDescriptor::decode(slice) {
278 Ok(descriptor) => descriptor,
279 // The list is not destroyed here: ownership only transfers on success, so a
280 // producer that got the descriptor wrong still holds its buffers.
281 Err(e) => return status_from_core_error(&e),
282 };
283
284 // SAFETY: the caller's contract on `buffers` and `owner` is this function's own.
285 *out_tensor =
286 HurrayTensor::from_core(abi_version, descriptor, buffers, owner, owner_release);
287 HURRAY_OK
288 })
289}
290
291// ── Accessors ─────────────────────────────────────────────────────────────────
292
293/// Reads the ABI version recorded by the producing build.
294///
295/// **Call this first.** It is the one accessor guaranteed to work on a tensor produced by
296/// any version of this ABI, and every other accessor assumes a caller that has already
297/// compared this value against its own
298/// [`HURRAY_C_ABI_VERSION`](crate::HURRAY_C_ABI_VERSION) and found it equal (ADR-043
299/// § 5). That ordering is what lets later versions add accessors without breaking
300/// consumers compiled against an earlier header.
301///
302/// A tensor from a stream or file read reports this library's version; one from
303/// [`hurray_tensor_new`] reports what its producer passed.
304///
305/// # Safety
306///
307/// `handle` and `out` MUST be valid, non-null pointers.
308///
309/// # Examples
310///
311/// ```
312/// # use hurray_core::{
313/// # BufferHandle, DeviceTag, ElementType, LayoutDescriptor, Shape, SyncMode,
314/// # TensorDescriptor, MIN_BUFFER_ALIGNMENT,
315/// # };
316/// use hurray_ffi::tensor::{
317/// hurray_tensor_abi_version, hurray_tensor_destroy, hurray_tensor_new,
318/// };
319/// use hurray_ffi::{
320/// hurray_buffer_list_new, HurrayBufferList, HurrayTensor, HURRAY_C_ABI_VERSION, HURRAY_OK,
321/// };
322/// # let handle = BufferHandle::new(16, MIN_BUFFER_ALIGNMENT, DeviceTag::Cpu, SyncMode::ProducerSynced)?;
323/// # let descriptor = TensorDescriptor::new(
324/// # 1, 0, ElementType::Float32, Shape::new(vec![4])?, 0, LayoutDescriptor::RowMajor,
325/// # vec![handle], None, None, None, None,
326/// # )?;
327/// # let encoded = descriptor.encode()?;
328/// # let mut buffers: *mut HurrayBufferList = std::ptr::null_mut();
329/// # unsafe { hurray_buffer_list_new(1, &mut buffers) };
330/// # let mut tensor: *mut HurrayTensor = std::ptr::null_mut();
331/// # unsafe {
332/// # hurray_tensor_new(
333/// # HURRAY_C_ABI_VERSION, encoded.as_ptr(), encoded.len() as u64, buffers,
334/// # std::ptr::null_mut(), None, &mut tensor,
335/// # )
336/// # };
337/// let mut version: u32 = 0;
338/// assert_eq!(
339/// unsafe { hurray_tensor_abi_version(tensor, &mut version) },
340/// HURRAY_OK,
341/// );
342/// if version != HURRAY_C_ABI_VERSION {
343/// // Producer and consumer disagree — refuse rather than dereference further.
344/// return Ok(());
345/// }
346/// # unsafe { hurray_tensor_destroy(&mut tensor) };
347/// # Ok::<(), Box<dyn std::error::Error>>(())
348/// ```
349#[no_mangle]
350pub unsafe extern "C" fn hurray_tensor_abi_version(
351 handle: *const HurrayTensor,
352 out: *mut u32,
353) -> HurrayStatus {
354 catch(|| {
355 null_check!(handle, out);
356 // SAFETY: handle is non-null and points to a live HurrayTensor.
357 *out = (*handle).abi_version;
358 HURRAY_OK
359 })
360}
361
362/// Borrows the tensor's descriptor handle.
363///
364/// The returned handle carries every accessor in [`crate::descriptor`] and
365/// [`crate::field`]. It is **borrowed**: the caller MUST NOT call
366/// [`hurray_descriptor_destroy`] on it, and MUST keep the tensor alive while it reads.
367///
368/// # Safety
369///
370/// `handle` and `out_descriptor` MUST be valid, non-null pointers.
371///
372/// # Examples
373///
374/// ```
375/// # use hurray_core::{
376/// # BufferHandle, DeviceTag, ElementType, LayoutDescriptor, Shape, SyncMode,
377/// # TensorDescriptor, MIN_BUFFER_ALIGNMENT,
378/// # };
379/// use hurray_ffi::descriptor::hurray_descriptor_rank;
380/// use hurray_ffi::tensor::{hurray_tensor_descriptor, hurray_tensor_destroy, hurray_tensor_new};
381/// use hurray_ffi::{
382/// hurray_buffer_list_new, HurrayBufferList, HurrayDescriptor, HurrayTensor,
383/// HURRAY_C_ABI_VERSION, HURRAY_OK,
384/// };
385/// # let handle = BufferHandle::new(16, MIN_BUFFER_ALIGNMENT, DeviceTag::Cpu, SyncMode::ProducerSynced)?;
386/// # let descriptor = TensorDescriptor::new(
387/// # 1, 0, ElementType::Float32, Shape::new(vec![4])?, 0, LayoutDescriptor::RowMajor,
388/// # vec![handle], None, None, None, None,
389/// # )?;
390/// # let encoded = descriptor.encode()?;
391/// # let mut buffers: *mut HurrayBufferList = std::ptr::null_mut();
392/// # unsafe { hurray_buffer_list_new(1, &mut buffers) };
393/// # let mut tensor: *mut HurrayTensor = std::ptr::null_mut();
394/// # unsafe {
395/// # hurray_tensor_new(
396/// # HURRAY_C_ABI_VERSION, encoded.as_ptr(), encoded.len() as u64, buffers,
397/// # std::ptr::null_mut(), None, &mut tensor,
398/// # )
399/// # };
400/// let mut descriptor: *mut HurrayDescriptor = std::ptr::null_mut();
401/// assert_eq!(
402/// unsafe { hurray_tensor_descriptor(tensor, &mut descriptor) },
403/// HURRAY_OK,
404/// );
405///
406/// let mut rank: u32 = 0;
407/// unsafe { hurray_descriptor_rank(descriptor, &mut rank) };
408/// assert_eq!(rank, 1);
409/// # unsafe { hurray_tensor_destroy(&mut tensor) };
410/// # Ok::<(), Box<dyn std::error::Error>>(())
411/// ```
412#[no_mangle]
413pub unsafe extern "C" fn hurray_tensor_descriptor(
414 handle: *const HurrayTensor,
415 out_descriptor: *mut *mut HurrayDescriptor,
416) -> HurrayStatus {
417 catch(|| {
418 null_check!(handle, out_descriptor);
419 // SAFETY: handle is non-null and points to a live HurrayTensor.
420 *out_descriptor = (*handle).descriptor;
421 HURRAY_OK
422 })
423}
424
425/// Borrows the tensor's buffer list.
426///
427/// Element `i` of the list is buffer `i` of the descriptor's buffer table. The list is
428/// **borrowed**: the caller MUST NOT call [`hurray_buffer_list_destroy`] on it, and MUST
429/// keep the tensor alive while it reads the buffers.
430///
431/// # Safety
432///
433/// `handle` and `out_buffers` MUST be valid, non-null pointers.
434///
435/// # Examples
436///
437/// ```
438/// # use hurray_core::{
439/// # BufferHandle, DeviceTag, ElementType, LayoutDescriptor, Shape, SyncMode,
440/// # TensorDescriptor, MIN_BUFFER_ALIGNMENT,
441/// # };
442/// use hurray_ffi::buffer_list::hurray_buffer_list_len;
443/// use hurray_ffi::tensor::{hurray_tensor_buffers, hurray_tensor_destroy, hurray_tensor_new};
444/// use hurray_ffi::{
445/// hurray_buffer_list_new, HurrayBufferList, HurrayTensor, HURRAY_C_ABI_VERSION, HURRAY_OK,
446/// };
447/// # let handle = BufferHandle::new(16, MIN_BUFFER_ALIGNMENT, DeviceTag::Cpu, SyncMode::ProducerSynced)?;
448/// # let descriptor = TensorDescriptor::new(
449/// # 1, 0, ElementType::Float32, Shape::new(vec![4])?, 0, LayoutDescriptor::RowMajor,
450/// # vec![handle], None, None, None, None,
451/// # )?;
452/// # let encoded = descriptor.encode()?;
453/// # let mut buffers: *mut HurrayBufferList = std::ptr::null_mut();
454/// # unsafe { hurray_buffer_list_new(1, &mut buffers) };
455/// # let mut tensor: *mut HurrayTensor = std::ptr::null_mut();
456/// # unsafe {
457/// # hurray_tensor_new(
458/// # HURRAY_C_ABI_VERSION, encoded.as_ptr(), encoded.len() as u64, buffers,
459/// # std::ptr::null_mut(), None, &mut tensor,
460/// # )
461/// # };
462/// let mut borrowed: *mut HurrayBufferList = std::ptr::null_mut();
463/// assert_eq!(
464/// unsafe { hurray_tensor_buffers(tensor, &mut borrowed) },
465/// HURRAY_OK,
466/// );
467///
468/// let mut len: u64 = 0;
469/// unsafe { hurray_buffer_list_len(borrowed, &mut len) };
470/// # unsafe { hurray_tensor_destroy(&mut tensor) };
471/// # Ok::<(), Box<dyn std::error::Error>>(())
472/// ```
473#[no_mangle]
474pub unsafe extern "C" fn hurray_tensor_buffers(
475 handle: *const HurrayTensor,
476 out_buffers: *mut *mut HurrayBufferList,
477) -> HurrayStatus {
478 catch(|| {
479 null_check!(handle, out_buffers);
480 // SAFETY: handle is non-null and points to a live HurrayTensor.
481 *out_buffers = (*handle).buffers;
482 HURRAY_OK
483 })
484}
485
486// ── Destructor ────────────────────────────────────────────────────────────────
487
488/// Destroys a tensor handle — its descriptor, every buffer it owns, and its owner
489/// reference — then writes null through `handle`.
490///
491/// One call rather than two, so the teardown order is not the caller's problem. A
492/// non-null `owner_release` is invoked exactly once, with the `owner` pointer given at
493/// construction.
494///
495/// # Safety
496///
497/// - `handle` MUST be a valid, non-null pointer to a `*mut HurrayTensor` produced by
498/// [`hurray_tensor_new`], by [`hurray_reader_next_tensor`][crate::hurray_reader_next_tensor],
499/// by [`hurray_file_reader_read_tensor`][crate::hurray_file_reader_read_tensor], or
500/// taken out of a native-protocol capsule.
501/// - That tensor MUST NOT have been destroyed already.
502///
503/// # Examples
504///
505/// ```
506/// # use hurray_core::{
507/// # BufferHandle, DeviceTag, ElementType, LayoutDescriptor, Shape, SyncMode,
508/// # TensorDescriptor, MIN_BUFFER_ALIGNMENT,
509/// # };
510/// use hurray_ffi::tensor::{hurray_tensor_destroy, hurray_tensor_new};
511/// use hurray_ffi::{
512/// hurray_buffer_list_new, HurrayBufferList, HurrayTensor, HURRAY_C_ABI_VERSION,
513/// HURRAY_ERR_INTERNAL, HURRAY_OK,
514/// };
515/// # let handle = BufferHandle::new(16, MIN_BUFFER_ALIGNMENT, DeviceTag::Cpu, SyncMode::ProducerSynced)?;
516/// # let descriptor = TensorDescriptor::new(
517/// # 1, 0, ElementType::Float32, Shape::new(vec![4])?, 0, LayoutDescriptor::RowMajor,
518/// # vec![handle], None, None, None, None,
519/// # )?;
520/// # let encoded = descriptor.encode()?;
521/// # let mut buffers: *mut HurrayBufferList = std::ptr::null_mut();
522/// # unsafe { hurray_buffer_list_new(1, &mut buffers) };
523/// # let mut tensor: *mut HurrayTensor = std::ptr::null_mut();
524/// # unsafe {
525/// # hurray_tensor_new(
526/// # HURRAY_C_ABI_VERSION, encoded.as_ptr(), encoded.len() as u64, buffers,
527/// # std::ptr::null_mut(), None, &mut tensor,
528/// # )
529/// # };
530/// assert_eq!(unsafe { hurray_tensor_destroy(&mut tensor) }, HURRAY_OK);
531/// assert!(tensor.is_null());
532///
533/// // A second destroy finds the caller's variable dead and says so, rather than
534/// // freeing anything twice.
535/// assert_eq!(
536/// unsafe { hurray_tensor_destroy(&mut tensor) },
537/// HURRAY_ERR_INTERNAL,
538/// );
539/// # Ok::<(), Box<dyn std::error::Error>>(())
540/// ```
541#[no_mangle]
542pub unsafe extern "C" fn hurray_tensor_destroy(handle: *mut *mut HurrayTensor) -> HurrayStatus {
543 catch(|| {
544 null_check!(handle);
545 let tensor = *handle;
546 if tensor.is_null() {
547 return HURRAY_ERR_INTERNAL;
548 }
549
550 #[cfg(debug_assertions)]
551 {
552 // SAFETY: tensor is non-null and points to a live HurrayTensor. Read and mark
553 // through the raw pointer before Box::from_raw, so the dead marker lands in
554 // the heap slot a second destroy would inspect.
555 if (*tensor).sentinel == SENTINEL_DEAD {
556 return HURRAY_ERR_INTERNAL;
557 }
558 (*tensor).sentinel = SENTINEL_DEAD;
559 }
560
561 // SAFETY: produced by HurrayTensor::from_parts via Box::into_raw; the caller
562 // guarantees this is the first and only destroy call.
563 let mut boxed = Box::from_raw(tensor);
564
565 // Taken before anything is freed, so a re-entrant or panicking release callback
566 // cannot reach a handle that is mid-destruction.
567 let release = boxed.owner_release.take();
568 let owner = std::mem::replace(&mut boxed.owner, std::ptr::null_mut());
569
570 let descriptor = std::mem::replace(&mut boxed.descriptor, std::ptr::null_mut());
571 if !descriptor.is_null() {
572 // SAFETY: owned by this tensor and destroyed exactly once.
573 hurray_descriptor_destroy(descriptor);
574 }
575 let mut buffers = std::mem::replace(&mut boxed.buffers, std::ptr::null_mut());
576 if !buffers.is_null() {
577 // SAFETY: owned by this tensor and destroyed exactly once; the list destroys
578 // every handle it owns.
579 hurray_buffer_list_destroy(&mut buffers);
580 }
581
582 drop(boxed);
583 *handle = std::ptr::null_mut();
584
585 // Last, and after the buffers: a producer's buffers point into memory the owner
586 // keeps alive, so releasing the owner first would hand the buffer release
587 // callbacks freed memory.
588 if let Some(release) = release {
589 // SAFETY: the caller guaranteed at construction time that owner stays valid
590 // until this call, and this is the only call.
591 release(owner);
592 }
593 HURRAY_OK
594 })
595}
596
597// ── Handing a decoded tensor to C ─────────────────────────────────────────────
598
599/// Releases one buffer's backing `Bytes` when its [`HurrayBuffer`] is destroyed.
600///
601/// # Safety
602///
603/// `context` MUST be the `Box<Bytes>` leaked by [`tensor_handle`] for this buffer, and
604/// MUST NOT have been released already.
605unsafe extern "C" fn release_bytes(_data: *mut c_void, context: *mut c_void) {
606 if !context.is_null() {
607 // SAFETY: context is the Box<Bytes> leaked for this buffer; hurray_buffer_destroy
608 // invokes a release callback exactly once.
609 drop(Box::from_raw(context as *mut Bytes));
610 }
611}
612
613/// Wraps a decoded tensor in the handles a C caller can hold.
614///
615/// No payload is copied: each `HurrayBuffer` points into the destination the core
616/// allocated and the transport filled, and owns a reference to it. Shared by the stream
617/// and file readers — a decoded tensor is the same thing whichever core produced it.
618///
619/// A read-produced tensor reports this build's ABI version and carries no owner: it owns
620/// its buffers outright, so there is nothing external to keep alive (ADR-043 § 1).
621pub(crate) fn tensor_handle(
622 descriptor: TensorDescriptor,
623 buffers: Vec<Bytes>,
624) -> Box<HurrayTensor> {
625 let mut handles: Vec<*mut HurrayBuffer> = Vec::with_capacity(buffers.len());
626 for (bytes, handle) in buffers.into_iter().zip(descriptor.buffers.iter().copied()) {
627 let boxed = Box::new(bytes);
628 let data = boxed.as_ptr() as *mut c_void;
629 let context = Box::into_raw(boxed) as *mut c_void;
630 handles.push(Box::into_raw(HurrayBuffer::from_parts(
631 data,
632 handle,
633 Some(release_bytes),
634 context,
635 )));
636 }
637
638 let list = Box::into_raw(HurrayBufferList::from_handles(handles));
639 let descriptor = Box::into_raw(HurrayDescriptor::from_descriptor(descriptor));
640 HurrayTensor::from_parts(
641 crate::HURRAY_C_ABI_VERSION,
642 descriptor,
643 list,
644 std::ptr::null_mut(),
645 None,
646 )
647}