Native Interchange Protocol

hurray.Tensor exposes a __hurray__() method and a matching hurray.from_hurray() constructor for in-process zero-copy tensor exchange between Hurray-aware Python extensions.

Unlike DLPack, the native protocol preserves the full Hurray descriptor — device tag, memory class, sync mode, element type, shape, and layout — without flattening to DLPack's DLDeviceType enum. It is available on all dtypes (including Tier 2 / quantized) and in both strict and relaxed modes.

Quick start

import struct, hurray

# Create a source tensor.
raw = struct.pack("6f", 1.0, 2.0, 3.0, 4.0, 5.0, 6.0)
source = hurray.Tensor(raw, hurray.float32, [2, 3])

# Zero-copy transfer via the native protocol.
target = hurray.from_hurray(source)

assert target.shape == source.shape   # (2, 3)
assert target.dtype == source.dtype   # hurray.float32

No data is copied. target borrows source's buffer; source is kept alive for as long as target exists.

Discovery

Probe support with hasattr before calling:

obj = source   # whatever you were handed

if hasattr(obj, "__hurray__"):
    tensor = hurray.from_hurray(obj)
else:
    # Fall back to DLPack or another protocol.
    tensor = hurray.from_dlpack(obj)

Why not DLPack?

DLPack is the right tool for interoperating with external libraries (PyTorch, JAX, NumPy). Use it when your consumers do not link hurray-ffi. The native protocol fills three gaps that DLPack v1.0 cannot express:

SituationDLPackNative protocol
ROCm UNIFIED memoryUnsupportedErrorSupported
PEER memory (any device)UnsupportedErrorSupported
Private device tags (0xF0–0xFE)UnsupportedErrorSupported
Tier 2 / quantized dtypesBufferErrorSupported

Tier 2 and quantized tensors

__hurray__ is available unconditionally — it is not gated on strict or relaxed mode and does not require the dtype to be an Array API Tier 1 type:

import hurray

q_tensor = hurray.Tensor(bytes(64), hurray.dtype.int4, [128])
q_copy = hurray.from_hurray(q_tensor)

assert q_copy.dtype == hurray.dtype.int4   # works in strict mode

Capsule lifecycle

__hurray__() returns a PyCapsule named "hurray_tensor". The capsule holds a HurrayBuffer pointer (from hurray-ffi) and a strong Python reference to the source Tensor.

hurray.from_hurray() renames the capsule to "used_hurray_tensor" before taking ownership — preventing double-free if the capsule is later GC'd. Attempting to consume the same capsule twice raises hurray.BufferError.

t = hurray.Tensor(bytes(8), hurray.float32, [2])
cap = t.__hurray__()          # fresh capsule

t2 = hurray.from_hurray(t)    # OK: calls __hurray__() internally
cap2 = t.__hurray__()         # OK: each call produces a new capsule

Error handling

import hurray

try:
    hurray.from_hurray(42)
    raise AssertionError("42 exposes no __hurray__")
except TypeError as exc:
    print(exc)

The other failure needs two builds to reproduce, so it cannot be shown here: a tensor produced by an extension built against a different hurray-ffi ABI version raises hurray.UnsupportedError, naming both versions. The capsule carries the producer's version precisely so the mismatch is caught at the boundary rather than read as whatever the consumer's layout happens to be.