use pyo3::exceptions::{PyAttributeError, PyBufferError, PyRuntimeError};
use pyo3::{Borrowed, Bound, PyAny, PyErr, PyTypeInfo, Python};
use std::ffi::CStr;
use std::ptr::NonNull;
use crate::{
ManagedBox,
ffi::{
DLDevice, DLManagedTensorVersioned, DLPACK_MAJOR_VERSION, DLPackExchangeAPI,
DLPackExchangeAPIHeader, DLTensor,
},
};
const DLPACK_EXCHANGE_API: &CStr = c"dlpack_exchange_api";
pub struct DlpackExchangeApiRef {
api: NonNull<DLPackExchangeAPI>,
}
impl DlpackExchangeApiRef {
pub fn from_object(obj: Borrowed<'_, '_, PyAny>) -> pyo3::PyResult<Option<Self>> {
let capsule = unsafe {
let ty = pyo3::ffi::Py_TYPE(obj.as_ptr()) as *mut pyo3::ffi::PyObject;
let attr = pyo3::intern!(obj.py(), "__dlpack_c_exchange_api__");
let capsule = pyo3::ffi::PyObject_GetAttr(ty, attr.as_ptr());
if capsule.is_null() {
let attr_error =
PyAttributeError::type_object_raw(pyo3::Python::assume_attached()).cast();
if pyo3::ffi::PyErr_ExceptionMatches(attr_error) != 0 {
pyo3::ffi::PyErr_Clear();
return Ok(None);
}
return Err(fetch_python_error());
}
capsule
};
let api_ptr = unsafe {
let ptr = pyo3::ffi::PyCapsule_GetPointer(capsule, DLPACK_EXCHANGE_API.as_ptr());
pyo3::ffi::Py_DecRef(capsule);
if ptr.is_null() {
return Err(fetch_python_error());
}
ptr.cast::<DLPackExchangeAPI>()
};
let Some(api) = compatible_api(api_ptr) else {
return Err(PyRuntimeError::new_err(
"no compatible DLPackExchangeAPI version found",
));
};
Ok(Some(Self { api }))
}
pub fn managed_tensor_from_py_object_no_sync(
&self,
obj: Borrowed<'_, '_, PyAny>,
) -> pyo3::PyResult<ManagedBox<DLManagedTensorVersioned>> {
let api = unsafe { self.api.as_ref() };
let Some(from_py_object) = api.managed_tensor_from_py_object_no_sync else {
return Err(PyRuntimeError::new_err(
"DLPackExchangeAPI managed_tensor_from_py_object_no_sync is null",
));
};
let mut out = std::ptr::null_mut();
let rc = unsafe { from_py_object(obj.as_ptr().cast(), &mut out) };
if rc != 0 {
return Err(fetch_python_error());
}
if out.is_null() {
return Err(PyBufferError::new_err(
"DLPackExchangeAPI returned a null managed tensor",
));
}
Ok(unsafe { ManagedBox::new_unchecked(out) })
}
pub fn managed_tensor_to_py_object_no_sync<'py>(
&self,
tensor: ManagedBox<DLManagedTensorVersioned>,
py: Python<'py>,
) -> pyo3::PyResult<Bound<'py, PyAny>> {
let api = unsafe { self.api.as_ref() };
let Some(to_py_object) = api.managed_tensor_to_py_object_no_sync else {
return Err(PyRuntimeError::new_err(
"DLPackExchangeAPI managed_tensor_to_py_object_no_sync is null",
));
};
let raw = tensor.into_raw();
let mut out = std::ptr::null_mut();
let rc = unsafe { to_py_object(raw, &mut out) };
if rc != 0 {
return Err(fetch_python_error());
}
if out.is_null() {
return Err(PyRuntimeError::new_err(
"DLPackExchangeAPI returned a null Python object",
));
}
unsafe { Bound::from_owned_ptr_or_err(py, out.cast()) }
}
pub fn current_work_stream(&self, device: DLDevice) -> pyo3::PyResult<*mut std::ffi::c_void> {
let api = unsafe { self.api.as_ref() };
let Some(current_work_stream) = api.current_work_stream else {
return Err(PyRuntimeError::new_err(
"DLPackExchangeAPI current_work_stream is null",
));
};
let mut stream = std::ptr::null_mut();
let rc = unsafe { current_work_stream(device.device_type, device.device_id, &mut stream) };
if rc != 0 {
return Err(fetch_python_error());
}
Ok(stream)
}
pub fn with_dltensor_view_no_sync<R>(
&self,
obj: Borrowed<'_, '_, PyAny>,
f: impl FnOnce(&DLTensor) -> R,
) -> pyo3::PyResult<R> {
let api = unsafe { self.api.as_ref() };
let Some(from_py_object) = api.dltensor_from_py_object_no_sync else {
return Err(PyRuntimeError::new_err(
"DLPackExchangeAPI dltensor_from_py_object_no_sync is null",
));
};
let mut tensor = DLTensor::default();
let rc = unsafe { from_py_object(obj.as_ptr().cast(), &mut tensor) };
if rc != 0 {
return Err(fetch_python_error());
}
Ok(f(&tensor))
}
}
fn compatible_api(api: *mut DLPackExchangeAPI) -> Option<NonNull<DLPackExchangeAPI>> {
let mut header = api.cast::<DLPackExchangeAPIHeader>();
while let Some(current_header) = NonNull::new(header) {
let current = unsafe { current_header.as_ref() };
if current.version.major == DLPACK_MAJOR_VERSION {
return NonNull::new(header.cast::<DLPackExchangeAPI>());
}
header = current.prev_api;
}
None
}
fn fetch_python_error() -> PyErr {
unsafe { PyErr::fetch(pyo3::Python::assume_attached()) }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
Builder, ManagedTensorBase,
ffi::{DLDataType, DLDevice, DLDeviceType, DLPACK_MINOR_VERSION, DLPackVersion},
};
use pyo3::conversion::FromPyObject;
use pyo3::types::{PyAnyMethods, PyModule};
use std::ffi::c_void;
use std::os::raw::{c_char, c_int};
unsafe extern "C" fn mock_allocator(
_prototype: *mut DLTensor,
_out: *mut *mut DLManagedTensorVersioned,
_error_ctx: *mut c_void,
_set_error: Option<unsafe extern "C" fn(*mut c_void, *const c_char, *const c_char)>,
) -> c_int {
-1
}
unsafe extern "C" fn mock_managed_from_py_object(
_py_object: *mut c_void,
out: *mut *mut DLManagedTensorVersioned,
) -> c_int {
let data = Box::new(vec![7i32, 8, 9]);
let data_ptr = data.as_ptr() as *mut c_void;
let raw = unsafe {
Builder::new(data, crate::metadata::CopiedArray::new([3i64], [1i64])).data(data_ptr)
}
.dtype(DLDataType::of::<i32>())
.build_raw::<DLManagedTensorVersioned>();
unsafe {
*out = raw;
}
0
}
static VIEW_DATA: [i32; 3] = [1, 2, 3];
static VIEW_SHAPE: [i64; 1] = [3];
static VIEW_STRIDES: [i64; 1] = [1];
unsafe extern "C" fn mock_dltensor_from_py_object(
_py_object: *mut c_void,
out: *mut DLTensor,
) -> c_int {
unsafe {
*out = DLTensor {
data: VIEW_DATA.as_ptr() as *mut c_void,
device: DLDevice::CPU,
ndim: 1,
dtype: DLDataType::of::<i32>(),
shape: VIEW_SHAPE.as_ptr() as *mut i64,
strides: VIEW_STRIDES.as_ptr() as *mut i64,
byte_offset: 0,
};
}
0
}
unsafe extern "C" fn mock_current_work_stream(
device_type: DLDeviceType,
_device_id: i32,
out_current_stream: *mut *mut c_void,
) -> c_int {
unsafe {
*out_current_stream = if device_type == DLDeviceType::CPU {
std::ptr::null_mut()
} else {
std::ptr::dangling_mut::<c_void>()
};
}
0
}
unsafe extern "C" fn mock_tensor_to_py_object(
tensor: *mut DLManagedTensorVersioned,
out_py_object: *mut *mut c_void,
) -> c_int {
unsafe {
DLManagedTensorVersioned::drop_raw(tensor);
*out_py_object = pyo3::ffi::PyLong_FromLong(42).cast();
}
0
}
fn leak_mock_api() -> *mut DLPackExchangeAPI {
Box::leak(Box::new(DLPackExchangeAPI {
header: DLPackExchangeAPIHeader {
version: DLPackVersion {
major: DLPACK_MAJOR_VERSION,
minor: DLPACK_MINOR_VERSION,
},
prev_api: std::ptr::null_mut(),
},
managed_tensor_allocator: Some(mock_allocator),
managed_tensor_from_py_object_no_sync: Some(mock_managed_from_py_object),
managed_tensor_to_py_object_no_sync: Some(mock_tensor_to_py_object),
dltensor_from_py_object_no_sync: Some(mock_dltensor_from_py_object),
current_work_stream: Some(mock_current_work_stream),
}))
}
#[test]
fn exchange_api_fast_path_extracts_versioned_tensor() {
pyo3::Python::initialize();
pyo3::Python::attach(|py| -> pyo3::PyResult<()> {
let module = PyModule::from_code(
py,
cr#"class MockTensor:
pass
"#,
c"mock_tensor.py",
c"mock_tensor",
)?;
let cls = module.getattr("MockTensor")?;
let obj = cls.call0()?;
let api = leak_mock_api();
let capsule =
unsafe { pyo3::ffi::PyCapsule_New(api.cast(), DLPACK_EXCHANGE_API.as_ptr(), None) };
let capsule = unsafe { pyo3::Bound::from_owned_ptr(py, capsule) };
cls.setattr("__dlpack_c_exchange_api__", capsule)?;
let api_ref = DlpackExchangeApiRef::from_object(obj.as_borrowed())?.unwrap();
assert!(api_ref.current_work_stream(DLDevice::CPU)?.is_null());
api_ref.with_dltensor_view_no_sync(obj.as_borrowed(), |tensor| {
assert_eq!(tensor.ndim, 1);
assert_eq!(unsafe { tensor.num_elements() }.unwrap(), 3);
})?;
let dlpack = ManagedBox::<DLManagedTensorVersioned>::extract(obj.as_borrowed())?;
let tensor = dlpack.tensor();
assert_eq!(tensor.ndim, 1);
assert_eq!(unsafe { tensor.shape() }.unwrap(), &[3]);
assert_eq!(
unsafe { tensor.cpu_data_slice::<i32>() }.unwrap(),
&[7, 8, 9]
);
Ok(())
})
.unwrap();
}
#[test]
fn exchange_api_lookup_preserves_non_attribute_errors() {
pyo3::Python::initialize();
pyo3::Python::attach(|py| -> pyo3::PyResult<()> {
let module = PyModule::from_code(
py,
cr#"class BrokenDescriptor:
def __get__(self, instance, owner):
raise RuntimeError("boom")
class MockTensor:
__dlpack_c_exchange_api__ = BrokenDescriptor()
"#,
c"broken_exchange.py",
c"broken_exchange",
)?;
let obj = module.getattr("MockTensor")?.call0()?;
let err = match DlpackExchangeApiRef::from_object(obj.as_borrowed()) {
Ok(_) => panic!("non-AttributeError exchange API lookup failure must propagate"),
Err(err) => err,
};
assert!(err.is_instance_of::<PyRuntimeError>(py));
Ok(())
})
.unwrap();
}
#[test]
fn exchange_api_exports_without_capsule() {
pyo3::Python::initialize();
pyo3::Python::attach(|py| -> pyo3::PyResult<()> {
let api = NonNull::new(leak_mock_api()).unwrap();
let api = DlpackExchangeApiRef { api };
let data = Box::new(vec![7i32, 8, 9]);
let data_ptr = data.as_ptr() as *mut c_void;
let tensor = unsafe {
Builder::new(data, crate::metadata::CopiedArray::new([3i64], [1i64])).data(data_ptr)
}
.dtype(DLDataType::of::<i32>())
.build::<DLManagedTensorVersioned>();
let object = api.managed_tensor_to_py_object_no_sync(tensor, py)?;
assert_eq!(object.extract::<i64>()?, 42);
Ok(())
})
.unwrap();
}
}