use std::ffi::{c_void, CStr};
use std::ptr::NonNull;
use pyo3::exceptions::{PyRuntimeError, PyTypeError, PyValueError};
use pyo3::prelude::*;
use pyo3::types::{PyCapsule, PyCapsuleMethods};
use scirs2_numpy::dlpack::{
DLDataType, DLDataTypeCode, DLDevice, DLDeviceType, DLManagedTensor, DLTensor,
};
const DLTENSOR_NAME: &CStr = c"dltensor";
const USED_DLTENSOR_NAME: &CStr = c"used_dltensor";
#[repr(C)]
struct BackingStore {
managed: DLManagedTensor,
data: Vec<f64>,
shape: Vec<i64>,
strides: Vec<i64>,
}
impl BackingStore {
unsafe fn drop_raw(ptr: *mut BackingStore) {
if !ptr.is_null() {
drop(unsafe { Box::from_raw(ptr) });
}
}
}
unsafe extern "C" fn backing_store_deleter(managed: *mut DLManagedTensor) {
if managed.is_null() {
return;
}
let backing = managed as *mut BackingStore;
unsafe { BackingStore::drop_raw(backing) };
}
unsafe extern "C" fn capsule_destructor(capsule: *mut pyo3::ffi::PyObject) {
let is_live_dltensor =
unsafe { pyo3::ffi::PyCapsule_IsValid(capsule, DLTENSOR_NAME.as_ptr()) } != 0;
if !is_live_dltensor {
return;
}
let ptr = unsafe { pyo3::ffi::PyCapsule_GetPointer(capsule, DLTENSOR_NAME.as_ptr()) };
if !ptr.is_null() {
let managed_ptr = ptr as *mut DLManagedTensor;
if let Some(deleter) = unsafe { (*managed_ptr).deleter } {
unsafe { deleter(managed_ptr) };
}
}
}
#[derive(Debug)]
enum ExtractError {
UnsupportedDtype {
code: u8,
bits: u8,
},
Arithmetic(String),
}
unsafe fn read_strided_elements<T: Copy>(
base_ptr: *const u8,
shape: &[i64],
strides: &[i64],
) -> Result<Vec<T>, String> {
if shape.len() != strides.len() {
return Err(format!(
"shape/strides length mismatch: {} dims vs {} strides",
shape.len(),
strides.len()
));
}
if shape.is_empty() {
let v = unsafe { std::ptr::read_unaligned(base_ptr as *const T) };
return Ok(vec![v]);
}
let elem_size = std::mem::size_of::<T>() as i64;
let ndim = shape.len();
let mut total: usize = 1;
for &d in shape {
if d < 0 {
return Err(format!("negative dimension {d} in tensor shape"));
}
total = total
.checked_mul(d as usize)
.ok_or_else(|| "tensor element count overflows usize".to_string())?;
}
let mut result: Vec<T> = Vec::with_capacity(total);
let mut idx = vec![0i64; ndim];
for _ in 0..total {
let mut offset_elems: i64 = 0;
for (&i, &s) in idx.iter().zip(strides.iter()) {
let term = i
.checked_mul(s)
.ok_or_else(|| "stride offset overflow".to_string())?;
offset_elems = offset_elems
.checked_add(term)
.ok_or_else(|| "stride offset overflow".to_string())?;
}
let byte_offset = offset_elems
.checked_mul(elem_size)
.ok_or_else(|| "byte offset overflow".to_string())?;
let ptr = base_ptr.wrapping_offset(byte_offset as isize) as *const T;
let v = unsafe { std::ptr::read_unaligned(ptr) };
result.push(v);
for (i, &dim) in idx.iter_mut().zip(shape.iter()).rev() {
*i += 1;
if *i < dim {
break;
}
*i = 0;
}
}
Ok(result)
}
unsafe fn extract_dlpack_data(
dl_tensor: &DLTensor,
) -> Result<(Vec<f64>, Vec<usize>), ExtractError> {
let is_scalar = dl_tensor.ndim <= 0 || dl_tensor.shape.is_null();
let shape_i64: Vec<i64> = if is_scalar {
Vec::new()
} else {
unsafe {
std::slice::from_raw_parts(dl_tensor.shape as *const i64, dl_tensor.ndim as usize)
}
.to_vec()
};
let elem_strides: Vec<i64> = if is_scalar {
Vec::new()
} else if dl_tensor.strides.is_null() {
compute_c_strides(&shape_i64)
} else {
unsafe {
std::slice::from_raw_parts(dl_tensor.strides as *const i64, dl_tensor.ndim as usize)
}
.to_vec()
};
let base_ptr = unsafe { (dl_tensor.data as *const u8).add(dl_tensor.byte_offset as usize) };
let dtype = dl_tensor.dtype;
let flat_vec: Vec<f64> = match (dtype.code, dtype.bits, dtype.lanes) {
(2, 32, 1) => {
let raw: Vec<f32> =
unsafe { read_strided_elements(base_ptr, &shape_i64, &elem_strides) }
.map_err(ExtractError::Arithmetic)?;
raw.into_iter().map(|v| v as f64).collect()
}
(2, 64, 1) => unsafe { read_strided_elements::<f64>(base_ptr, &shape_i64, &elem_strides) }
.map_err(ExtractError::Arithmetic)?,
(0, 8, 1) => {
let raw: Vec<i8> =
unsafe { read_strided_elements(base_ptr, &shape_i64, &elem_strides) }
.map_err(ExtractError::Arithmetic)?;
raw.into_iter().map(|v| v as f64).collect()
}
(0, 16, 1) => {
let raw: Vec<i16> =
unsafe { read_strided_elements(base_ptr, &shape_i64, &elem_strides) }
.map_err(ExtractError::Arithmetic)?;
raw.into_iter().map(|v| v as f64).collect()
}
(0, 32, 1) => {
let raw: Vec<i32> =
unsafe { read_strided_elements(base_ptr, &shape_i64, &elem_strides) }
.map_err(ExtractError::Arithmetic)?;
raw.into_iter().map(|v| v as f64).collect()
}
(0, 64, 1) => {
let raw: Vec<i64> =
unsafe { read_strided_elements(base_ptr, &shape_i64, &elem_strides) }
.map_err(ExtractError::Arithmetic)?;
raw.into_iter().map(|v| v as f64).collect()
}
(1, 8, 1) => {
let raw: Vec<u8> =
unsafe { read_strided_elements(base_ptr, &shape_i64, &elem_strides) }
.map_err(ExtractError::Arithmetic)?;
raw.into_iter().map(|v| v as f64).collect()
}
(1, 16, 1) => {
let raw: Vec<u16> =
unsafe { read_strided_elements(base_ptr, &shape_i64, &elem_strides) }
.map_err(ExtractError::Arithmetic)?;
raw.into_iter().map(|v| v as f64).collect()
}
(1, 32, 1) => {
let raw: Vec<u32> =
unsafe { read_strided_elements(base_ptr, &shape_i64, &elem_strides) }
.map_err(ExtractError::Arithmetic)?;
raw.into_iter().map(|v| v as f64).collect()
}
(1, 64, 1) => {
let raw: Vec<u64> =
unsafe { read_strided_elements(base_ptr, &shape_i64, &elem_strides) }
.map_err(ExtractError::Arithmetic)?;
raw.into_iter().map(|v| v as f64).collect()
}
(code, bits, _) => {
return Err(ExtractError::UnsupportedDtype { code, bits });
}
};
let shape_vec: Vec<usize> = if is_scalar {
vec![1]
} else {
shape_i64.iter().map(|&d| d.max(0) as usize).collect()
};
Ok((flat_vec, shape_vec))
}
#[pyfunction]
pub fn from_dlpack(py: Python<'_>, capsule: &Bound<'_, PyAny>) -> PyResult<Py<PyAny>> {
let cap = capsule.cast::<PyCapsule>().map_err(|_| {
PyTypeError::new_err(
"from_dlpack: argument must be a PyCapsule (the result of tensor.__dlpack__()). \
Got a non-capsule object instead.",
)
})?;
let name_opt = cap.name().map_err(|e| {
PyValueError::new_err(format!("from_dlpack: could not read capsule name: {e}"))
})?;
let name_matches = match name_opt {
None => false,
Some(cn) => {
let name_cstr = unsafe { cn.as_cstr() };
name_cstr == DLTENSOR_NAME
}
};
if !name_matches {
return Err(PyValueError::new_err(
"from_dlpack: expected a PyCapsule named 'dltensor'. \
Pass the result of tensor.__dlpack__() directly.",
));
}
let nn_ptr: NonNull<c_void> = cap
.pointer_checked(Some(DLTENSOR_NAME))
.map_err(|e| PyRuntimeError::new_err(format!("from_dlpack: null capsule pointer: {e}")))?;
let managed_ptr = nn_ptr.as_ptr() as *mut DLManagedTensor;
let dl_tensor: &DLTensor = unsafe { &(*managed_ptr).dl_tensor };
if dl_tensor.device.device_type != DLDeviceType::Cpu as i32 {
return Err(PyTypeError::new_err(format!(
"from_dlpack: only CPU tensors are supported (got device type {}). \
Copy the tensor to CPU before calling from_dlpack.",
dl_tensor.device.device_type
)));
}
if dl_tensor.data.is_null() {
return Err(PyValueError::new_err(
"from_dlpack: tensor has a null data pointer.",
));
}
let (flat_vec, shape_vec) = unsafe { extract_dlpack_data(dl_tensor) }.map_err(|e| match e {
ExtractError::UnsupportedDtype { code, bits } => PyTypeError::new_err(format!(
"from_dlpack: unsupported dtype (code={code}, bits={bits}). \
Supported: int8/16/32/64, uint8/16/32/64, float32, float64.",
)),
ExtractError::Arithmetic(msg) => PyValueError::new_err(format!("from_dlpack: {msg}")),
})?;
let rename_result =
unsafe { pyo3::ffi::PyCapsule_SetName(cap.as_ptr(), USED_DLTENSOR_NAME.as_ptr()) };
let _ = rename_result;
if let Some(deleter) = unsafe { (*managed_ptr).deleter } {
unsafe { deleter(managed_ptr) };
}
let numpy = py.import("numpy").map_err(|e| {
PyRuntimeError::new_err(format!("from_dlpack: could not import numpy: {e}"))
})?;
let arr = numpy.getattr("array")?.call1((flat_vec,))?;
let shaped = arr.call_method1("reshape", (shape_vec,))?;
Ok(shaped.into())
}
#[pyfunction]
pub fn to_dlpack(py: Python<'_>, array: &Bound<'_, PyAny>) -> PyResult<Py<PyAny>> {
let numpy = py
.import("numpy")
.map_err(|e| PyRuntimeError::new_err(format!("to_dlpack: could not import numpy: {e}")))?;
let arr = numpy.getattr("asarray")?.call1((array,))?;
let arr_f64 = numpy
.getattr("ascontiguousarray")?
.call((arr,), Some(&pyo3::types::PyDict::new(py)))?;
let shape_obj = arr_f64.getattr("shape")?;
let shape_tuple: Vec<i64> = shape_obj.extract::<Vec<i64>>().map_err(|e| {
PyTypeError::new_err(format!("to_dlpack: could not extract array shape: {e}"))
})?;
let flat_list = arr_f64.call_method0("flatten")?;
let data_vec: Vec<f64> = flat_list.extract::<Vec<f64>>().map_err(|e| {
PyTypeError::new_err(format!(
"to_dlpack: array must be convertible to float64: {e}"
))
})?;
let strides_vec: Vec<i64> = compute_c_strides(&shape_tuple);
let n = shape_tuple.len();
let mut store = Box::new(BackingStore {
managed: DLManagedTensor {
dl_tensor: DLTensor {
data: std::ptr::null_mut(), device: DLDevice {
device_type: DLDeviceType::Cpu as i32,
device_id: 0,
},
ndim: n as i32,
dtype: DLDataType {
code: DLDataTypeCode::Float as u8,
bits: 64,
lanes: 1,
},
shape: std::ptr::null_mut(), strides: std::ptr::null_mut(), byte_offset: 0,
},
manager_ctx: std::ptr::null_mut(),
deleter: Some(backing_store_deleter),
},
data: data_vec,
shape: shape_tuple,
strides: strides_vec,
});
store.managed.dl_tensor.data = store.data.as_mut_ptr() as *mut c_void;
store.managed.dl_tensor.shape = store.shape.as_mut_ptr();
store.managed.dl_tensor.strides = store.strides.as_mut_ptr();
let raw_store: *mut BackingStore = Box::into_raw(store);
let managed_nn = NonNull::new(raw_store as *mut c_void)
.ok_or_else(|| PyRuntimeError::new_err("to_dlpack: null BackingStore pointer"))?;
let capsule = unsafe {
PyCapsule::new_with_pointer_and_destructor(
py,
managed_nn,
DLTENSOR_NAME,
Some(capsule_destructor),
)
}
.map_err(|e| PyRuntimeError::new_err(format!("to_dlpack: failed to create capsule: {e}")))?;
Ok(capsule.into())
}
fn compute_c_strides(shape: &[i64]) -> Vec<i64> {
let n = shape.len();
if n == 0 {
return Vec::new();
}
let mut strides = vec![1i64; n];
for i in (0..n - 1).rev() {
strides[i] = strides[i + 1] * shape[i + 1];
}
strides
}
pub fn register_dlpack_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(from_dlpack, m)?)?;
m.add_function(wrap_pyfunction!(to_dlpack, m)?)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dlpack_module_symbol_exists() {
let _msg = "dlpack module compiled successfully";
}
#[test]
fn compute_c_strides_1d() {
assert_eq!(compute_c_strides(&[5]), vec![1]);
}
#[test]
fn compute_c_strides_2d() {
assert_eq!(compute_c_strides(&[2, 3]), vec![3, 1]);
}
#[test]
fn compute_c_strides_3d() {
assert_eq!(compute_c_strides(&[2, 3, 4]), vec![12, 4, 1]);
}
#[test]
fn compute_c_strides_empty() {
assert_eq!(compute_c_strides(&[]), Vec::<i64>::new());
}
#[test]
fn read_strided_elements_contiguous_matches_plain_read() {
let data: [f64; 6] = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let shape = [2_i64, 3];
let strides = compute_c_strides(&shape);
let base_ptr = data.as_ptr() as *const u8;
let out: Vec<f64> = unsafe { read_strided_elements(base_ptr, &shape, &strides) }
.expect("contiguous read should not fail");
assert_eq!(out, data.to_vec());
}
#[test]
fn read_strided_elements_transposed_2d_reads_logical_order() {
let data: [f64; 6] = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let shape = [3_i64, 2];
let strides = [1_i64, 3];
let base_ptr = data.as_ptr() as *const u8;
let out: Vec<f64> = unsafe { read_strided_elements(base_ptr, &shape, &strides) }
.expect("transposed strided read should succeed");
assert_eq!(out, vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
assert_ne!(out, data.to_vec());
}
#[test]
fn read_strided_elements_negative_stride_reversed_view() {
let data: [f64; 3] = [10.0, 20.0, 30.0];
let shape = [3_i64];
let strides = [-1_i64];
let base_ptr = unsafe { data.as_ptr().add(2) } as *const u8;
let out: Vec<f64> = unsafe { read_strided_elements(base_ptr, &shape, &strides) }
.expect("negative-stride read should succeed");
assert_eq!(out, vec![30.0, 20.0, 10.0]);
}
#[test]
fn read_strided_elements_length_mismatch_errors_without_reading() {
let x = 0.0_f64;
let base_ptr = &x as *const f64 as *const u8;
let result: Result<Vec<f64>, String> =
unsafe { read_strided_elements(base_ptr, &[2, 2], &[1]) };
assert!(result.is_err());
}
#[test]
fn read_strided_elements_stride_overflow_errors_cleanly() {
let data = [0.0_f64];
let shape = [2_i64, 2];
let strides = [i64::MAX, i64::MAX];
let base_ptr = data.as_ptr() as *const u8;
let result: Result<Vec<f64>, String> =
unsafe { read_strided_elements(base_ptr, &shape, &strides) };
assert!(result.is_err(), "expected overflow to be caught as Err");
}
#[test]
fn read_strided_elements_scalar_zero_dim() {
let data = [42.0_f64];
let base_ptr = data.as_ptr() as *const u8;
let out: Vec<f64> =
unsafe { read_strided_elements(base_ptr, &[], &[]) }.expect("scalar read should work");
assert_eq!(out, vec![42.0]);
}
fn make_tensor(
data_ptr: *mut c_void,
shape: &[i64],
strides: *mut i64,
dtype: DLDataType,
) -> DLTensor {
DLTensor {
data: data_ptr,
device: DLDevice {
device_type: DLDeviceType::Cpu as i32,
device_id: 0,
},
ndim: shape.len() as i32,
dtype,
shape: shape.as_ptr() as *mut i64,
strides,
byte_offset: 0,
}
}
#[test]
fn extract_dlpack_data_null_strides_is_treated_as_c_contiguous() {
let mut data: [f64; 6] = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let shape = [2_i64, 3];
let dtype = DLDataType {
code: DLDataTypeCode::Float as u8,
bits: 64,
lanes: 1,
};
let tensor = make_tensor(
data.as_mut_ptr() as *mut c_void,
&shape,
std::ptr::null_mut(),
dtype,
);
let (flat, shape_vec) =
unsafe { extract_dlpack_data(&tensor) }.expect("contiguous extraction should succeed");
assert_eq!(flat, data.to_vec());
assert_eq!(shape_vec, vec![2, 3]);
}
#[test]
fn extract_dlpack_data_non_null_strides_transposed_view_reads_correctly() {
let mut data: [f64; 6] = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let mut strides = [1_i64, 3];
let shape = [3_i64, 2];
let dtype = DLDataType {
code: DLDataTypeCode::Float as u8,
bits: 64,
lanes: 1,
};
let tensor = make_tensor(
data.as_mut_ptr() as *mut c_void,
&shape,
strides.as_mut_ptr(),
dtype,
);
let (flat, shape_vec) = unsafe { extract_dlpack_data(&tensor) }
.expect("transposed extraction should succeed, not silently misread");
assert_eq!(flat, vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
assert_eq!(shape_vec, vec![3, 2]);
assert_ne!(flat, data.to_vec());
}
#[test]
fn extract_dlpack_data_float32_transposed_widens_correctly() {
let mut data: [f32; 6] = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let mut strides = [1_i64, 3];
let shape = [3_i64, 2];
let dtype = DLDataType {
code: DLDataTypeCode::Float as u8,
bits: 32,
lanes: 1,
};
let tensor = make_tensor(
data.as_mut_ptr() as *mut c_void,
&shape,
strides.as_mut_ptr(),
dtype,
);
let (flat, shape_vec) =
unsafe { extract_dlpack_data(&tensor) }.expect("f32 transposed extraction failed");
assert_eq!(flat, vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
assert_eq!(shape_vec, vec![3, 2]);
}
#[test]
fn extract_dlpack_data_unsupported_dtype_errors() {
let mut data: [f64; 1] = [0.0];
let shape = [1_i64];
let dtype = DLDataType {
code: 99, bits: 64,
lanes: 1,
};
let tensor = make_tensor(
data.as_mut_ptr() as *mut c_void,
&shape,
std::ptr::null_mut(),
dtype,
);
let result = unsafe { extract_dlpack_data(&tensor) };
assert!(matches!(
result,
Err(ExtractError::UnsupportedDtype { code: 99, bits: 64 })
));
}
}