This is an automated email from the ASF dual-hosted git repository.
tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git
The following commit(s) were added to refs/heads/main by this push:
new ab87e916 [Rust] Data and ABI soundness fixes: open DLDeviceType,
checked tensor sizes, unsafe AsDLDataType (#957)
ab87e916 is described below
commit ab87e91655f172b16d888f84b2c16c40fddbab7d
Author: Tianqi Chen <[email protected]>
AuthorDate: Sat Nov 7 08:04:37 2026 -0500
[Rust] Data and ABI soundness fixes: open DLDeviceType, checked tensor
sizes, unsafe AsDLDataType (#957)
This PR fixes data and ABI soundness issues in the Rust bindings, where
safe code could create invalid values or read out of bounds.
- **`DLDevice::device_type` is an `i32`.** Device type codes are an open
set, so a code read from a foreign `DLDevice` may not be a
`DLDeviceType` variant. The field is now an `i32`, as `DLDataType::code`
is a `u8`, and `DLDeviceType` stays the enum of known codes.
- **Checked tensor sizes.** The element count and byte size of a tensor
wrapped on overflow, and a negative extent became a huge count, so
`Tensor::from_nd_alloc(CPUNDAlloc, &[(1 << 61) + 1], f64, cpu)`
allocated 8 bytes behind a 2^61-element slice. Both now use checked
arithmetic: `from_nd_alloc` panics on such a shape before allocating, as
`Vec::with_capacity` does on capacity overflow, and `Tensor::from_slice`
returns a `ValueError`.
- **`AsDLDataType` is an `unsafe trait`.** `Tensor::data_as_slice::<T>`
reads data of dtype `T::DL_DATA_TYPE` as `T`, so a safe implementation
naming the dtype of an unrelated type let safe code read arbitrary bits
as `T`. The trait now states the layout contract that `data_as_slice`
relies on.
- **`TVMFFIByteArray::as_str` is `unsafe`.** It views a raw pointer and
length as text. Converting a raw C string argument that is not UTF-8 to
a `String` now fails as a type mismatch instead of panicking.
`String::as_str` stays unchecked, since an `ffi.String` holds UTF-8 text
by the ABI.
- **Null pointers with zero length.** C passes an empty array as a null
pointer with length zero, which a slice cannot hold. A crate-internal
`util::slice_from_nullable_raw_parts` allows that case and replaces the
hand-written checks for packed call arguments, tensor shapes and data,
`Shape`, `List` cells, `Bytes` from a byte array, and type field tables.
- **`Map` accepts an `ffi.Dict`.** As C++ `Map<K, V>` does, converting
an `ffi.Dict` to a `Map` copies its entries into a new map, converting
each to `K` and `V`.
Source-breaking changes:
- `DLDevice::device_type` is an `i32`: compare it with
`DLDeviceType::kDLCPU as i32`, and drop `as i32` where it is passed on
as a code.
- Implementations of `AsDLDataType` need `unsafe impl`.
- Calls to `TVMFFIByteArray::as_str` need an `unsafe` block.
---
rust/tvm-ffi-sys/src/c_api.rs | 6 +-
rust/tvm-ffi-sys/src/dlpack.rs | 5 +-
rust/tvm-ffi/src/collections/list.rs | 5 +-
rust/tvm-ffi/src/collections/map.rs | 74 +++++++++++++------
rust/tvm-ffi/src/collections/shape.rs | 6 +-
rust/tvm-ffi/src/collections/tensor.rs | 102 +++++++++++++++------------
rust/tvm-ffi/src/device.rs | 10 +--
rust/tvm-ffi/src/dtype.rs | 9 ++-
rust/tvm-ffi/src/error.rs | 9 ++-
rust/tvm-ffi/src/extra/structural_common.rs | 5 +-
rust/tvm-ffi/src/extra/structural_mutate.rs | 3 +-
rust/tvm-ffi/src/function.rs | 2 +-
rust/tvm-ffi/src/function_internal.rs | 11 +++
rust/tvm-ffi/src/lib.rs | 1 +
rust/tvm-ffi/src/macros.rs | 3 +-
rust/tvm-ffi/src/reflection/accessor.rs | 19 +++--
rust/tvm-ffi/src/reflection/unsafe_init.rs | 4 +-
rust/tvm-ffi/src/string.rs | 5 +-
rust/tvm-ffi/src/type_traits.rs | 4 +-
rust/tvm-ffi/src/util.rs | 47 ++++++++++++
rust/tvm-ffi/tests/test_any.rs | 11 +--
rust/tvm-ffi/tests/test_map.rs | 12 ++++
rust/tvm-ffi/tests/test_object_reflection.rs | 37 ++++++----
rust/tvm-ffi/tests/test_rs_object.rs | 13 ++--
rust/tvm-ffi/tests/test_tensor.rs | 2 +-
25 files changed, 273 insertions(+), 132 deletions(-)
diff --git a/rust/tvm-ffi-sys/src/c_api.rs b/rust/tvm-ffi-sys/src/c_api.rs
index 199ee58c..0eebb92f 100644
--- a/rust/tvm-ffi-sys/src/c_api.rs
+++ b/rust/tvm-ffi-sys/src/c_api.rs
@@ -246,7 +246,11 @@ impl TVMFFIByteArray {
///
/// # Returns
/// * `&str` - The converted str view.
- pub fn as_str(&self) -> &str {
+ ///
+ /// # Safety
+ /// `data` must be null or point to `size` bytes of UTF-8 text that stay
+ /// valid and unchanged while the result is used.
+ pub unsafe fn as_str(&self) -> &str {
// An empty array may hold a null pointer, as the type table stores an
// empty doc string.
if self.data.is_null() {
diff --git a/rust/tvm-ffi-sys/src/dlpack.rs b/rust/tvm-ffi-sys/src/dlpack.rs
index 2037b43f..96d0b71f 100644
--- a/rust/tvm-ffi-sys/src/dlpack.rs
+++ b/rust/tvm-ffi-sys/src/dlpack.rs
@@ -49,7 +49,8 @@ pub enum DLDeviceType {
#[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct DLDevice {
- pub device_type: DLDeviceType,
+ /// A [`DLDeviceType`] code, an `i32` so the codes stay an open set.
+ pub device_type: i32,
pub device_id: i32,
}
@@ -111,7 +112,7 @@ pub struct DLTensor {
impl DLDevice {
pub fn new(device_type: DLDeviceType, device_id: i32) -> Self {
Self {
- device_type,
+ device_type: device_type as i32,
device_id,
}
}
diff --git a/rust/tvm-ffi/src/collections/list.rs
b/rust/tvm-ffi/src/collections/list.rs
index e1650779..799c725f 100644
--- a/rust/tvm-ffi/src/collections/list.rs
+++ b/rust/tvm-ffi/src/collections/list.rs
@@ -231,10 +231,7 @@ unsafe impl<T: ContainerElement> ObjectRefCore for List<T>
{
/// mutated while the slice is alive.
pub(crate) unsafe fn seq_cells(data: &TVMFFIAny) -> &[TVMFFIAny] {
let seq = &*(data.data_union.v_obj as *const ListObj);
- if *seq.size.get() == 0 {
- return &[];
- }
- std::slice::from_raw_parts(
+ crate::util::slice_from_nullable_raw_parts(
*seq.data.get() as *const TVMFFIAny,
*seq.size.get() as usize,
)
diff --git a/rust/tvm-ffi/src/collections/map.rs
b/rust/tvm-ffi/src/collections/map.rs
index 94278fdb..2ba7c449 100644
--- a/rust/tvm-ffi/src/collections/map.rs
+++ b/rust/tvm-ffi/src/collections/map.rs
@@ -350,24 +350,47 @@ where
/// `try_cast_from_any_view`, which run during argument decoding where a
/// panic could unwind across the C ABI.
fn try_raw_entries(&self) -> Result<Vec<(Any, Any)>> {
- let mut entries = Vec::with_capacity(self.len());
- let mut remaining = self.len();
+ forward_entries(
+ crate::cached_global_func!("ffi.MapForwardIterFunctor"),
+ AnyView::from(self),
+ self.len(),
+ )
+ }
+
+ /// Converts raw entries one by one into a new map.
+ fn from_raw_entries(entries: Vec<(Any, Any)>) -> Result<Self> {
+ let mut pairs = Vec::with_capacity(entries.len());
+ for (k, v) in entries {
+ pairs.push((element_from_any::<K>(k)?, element_from_any::<V>(v)?));
+ }
+ Self::from_pairs(&pairs)
+ }
+}
+
+/// The `len` entries of the map or dict `container`, read with the iterator
+/// functor that `iter_functor` makes for it.
+fn forward_entries(
+ iter_functor: &Function,
+ container: AnyView,
+ len: usize,
+) -> Result<Vec<(Any, Any)>> {
+ let mut entries = Vec::with_capacity(len);
+ let mut remaining = len;
+ if remaining == 0 {
+ return Ok(entries);
+ }
+ let functor = iter_functor
+ .call_packed(&[container])
+ .and_then(Function::try_from)?;
+ loop {
+ let k = functor.call_packed(&[AnyView::from(&0i64)])?;
+ let v = functor.call_packed(&[AnyView::from(&1i64)])?;
+ entries.push((k, v));
+ remaining -= 1;
if remaining == 0 {
return Ok(entries);
}
- let functor = crate::cached_global_func!("ffi.MapForwardIterFunctor")
- .call_packed(&[AnyView::from(self)])
- .and_then(Function::try_from)?;
- loop {
- let k = functor.call_packed(&[AnyView::from(&0i64)])?;
- let v = functor.call_packed(&[AnyView::from(&1i64)])?;
- entries.push((k, v));
- remaining -= 1;
- if remaining == 0 {
- return Ok(entries);
- }
- functor.call_packed(&[AnyView::from(&2i64)])?;
- }
+ functor.call_packed(&[AnyView::from(&2i64)])?;
}
}
@@ -649,6 +672,18 @@ where
}
unsafe fn try_cast_from_any_view(data: &TVMFFIAny) -> Result<Self, ()> {
+ if data.type_index == TypeIndex::kTVMFFIDict as i32 {
+ // A dict is mutable, so it converts by copy, as C++ `Map` accepts
it.
+ // `ffi.Dict` shares the `MapBaseObj` header.
+ let len = (*(data.data_union.v_obj as *const MapObj)).size as
usize;
+ let entries = forward_entries(
+ crate::cached_global_func!("ffi.DictForwardIterFunctor"),
+ AnyView::from_raw_ffi_any(*data),
+ len,
+ )
+ .map_err(|_| ())?;
+ return Self::from_raw_entries(entries).map_err(|_| ());
+ }
if data.type_index != TypeIndex::kTVMFFIMap as i32 {
return Err(());
}
@@ -663,13 +698,8 @@ where
// Slow path: try to convert entry by entry into a new map, as C++
// `TryCastFromAnyView` does.
let src = <Self as
AnyCompatible>::copy_from_any_view_after_check(data);
- let mut pairs = Vec::with_capacity(src.len());
- for (k, v) in src.try_raw_entries().map_err(|_| ())? {
- let k = element_from_any::<K>(k).map_err(|_| ())?;
- let v = element_from_any::<V>(v).map_err(|_| ())?;
- pairs.push((k, v));
- }
- Self::from_pairs(&pairs).map_err(|_| ())
+ let entries = src.try_raw_entries().map_err(|_| ())?;
+ Self::from_raw_entries(entries).map_err(|_| ())
}
}
diff --git a/rust/tvm-ffi/src/collections/shape.rs
b/rust/tvm-ffi/src/collections/shape.rs
index ef0eb3b0..f7214c0c 100644
--- a/rust/tvm-ffi/src/collections/shape.rs
+++ b/rust/tvm-ffi/src/collections/shape.rs
@@ -86,11 +86,9 @@ impl Shape {
/// The dimensions as a slice.
pub fn as_slice(&self) -> &[i64] {
- // An empty shape may have a null data pointer, which a slice cannot.
- if self.data.data.size == 0 {
- return &[];
+ unsafe {
+ crate::util::slice_from_nullable_raw_parts(self.data.data.data,
self.data.data.size)
}
- unsafe { std::slice::from_raw_parts(self.data.data.data,
self.data.data.size) }
}
/// Fill `strides` with the row-major strides of `shape`.
diff --git a/rust/tvm-ffi/src/collections/tensor.rs
b/rust/tvm-ffi/src/collections/tensor.rs
index 1dd1b2da..83b82dfd 100644
--- a/rust/tvm-ffi/src/collections/tensor.rs
+++ b/rust/tvm-ffi/src/collections/tensor.rs
@@ -68,6 +68,9 @@ pub unsafe trait NDAllocator: 'static {
/// Size queries on a `DLTensor`.
pub trait DLTensorExt {
/// The number of elements: the product of the shape, 1 for a scalar.
+ ///
+ /// # Panics
+ /// If an extent is negative or the product overflows `usize`.
fn numel(&self) -> usize;
/// The bytes of one element: `bits * lanes`, rounded up to whole bytes.
fn item_size(&self) -> usize;
@@ -76,14 +79,9 @@ pub trait DLTensorExt {
impl DLTensorExt for DLTensor {
fn numel(&self) -> usize {
// A scalar (`ndim == 0`) has one element and may have a null shape.
- if self.ndim <= 0 {
- return 1;
- }
- unsafe {
- std::slice::from_raw_parts(self.shape, self.ndim as usize)
- .iter()
- .product::<i64>() as usize
- }
+ let shape =
+ unsafe { crate::util::slice_from_nullable_raw_parts(self.shape,
self.ndim as usize) };
+ checked_numel(shape).expect("tensor shape has a negative extent or
overflows usize")
}
fn item_size(&self) -> usize {
@@ -91,6 +89,22 @@ impl DLTensorExt for DLTensor {
}
}
+/// The product of `shape`; `None` if an extent is negative or it overflows.
+fn checked_numel(shape: &[i64]) -> Option<usize> {
+ shape.iter().try_fold(1usize, |numel, &extent| {
+ numel.checked_mul(usize::try_from(extent).ok()?)
+ })
+}
+
+/// The bytes of `tensor`'s data; it fits in `isize`, as an allocation must.
+fn data_size(tensor: &DLTensor) -> usize {
+ tensor
+ .numel()
+ .checked_mul(tensor.item_size())
+ .filter(|&size| size <= isize::MAX as usize)
+ .expect("tensor byte size overflows isize")
+}
+
//-----------------------------------------------------
// Tensor
//-----------------------------------------------------
@@ -161,7 +175,7 @@ impl Tensor {
dtype.to_string()
);
}
- if self.device().device_type != DLDeviceType::kDLCPU {
+ if self.device().device_type != DLDeviceType::kDLCPU as i32 {
crate::bail!(crate::error::RUNTIME_ERROR, "Tensor is not on CPU");
}
crate::ensure!(
@@ -171,11 +185,7 @@ impl Tensor {
);
unsafe {
- // An empty tensor may have a null data pointer, which a slice
cannot.
- if self.numel() == 0 {
- return Ok(&[]);
- }
- Ok(std::slice::from_raw_parts(
+ Ok(crate::util::slice_from_nullable_raw_parts(
self.data.dltensor.data as *const T,
self.numel(),
))
@@ -208,7 +218,7 @@ impl Tensor {
self.dtype().to_string()
);
}
- if self.device().device_type != DLDeviceType::kDLCPU {
+ if self.device().device_type != DLDeviceType::kDLCPU as i32 {
crate::bail!(crate::error::RUNTIME_ERROR, "Tensor is not on CPU");
}
crate::ensure!(
@@ -217,10 +227,7 @@ impl Tensor {
"Tensor is not contiguous"
);
unsafe {
- if self.numel() == 0 {
- return Ok(&mut []);
- }
- Ok(std::slice::from_raw_parts_mut(
+ Ok(crate::util::slice_from_nullable_raw_parts_mut(
self.data.dltensor.data as *mut T,
self.numel(),
))
@@ -230,11 +237,7 @@ impl Tensor {
/// The extent of each dimension.
pub fn shape(&self) -> &[i64] {
let t = &self.data.dltensor;
- // A scalar may have a null shape, which a slice cannot.
- if t.ndim == 0 {
- return &[];
- }
- unsafe { std::slice::from_raw_parts(t.shape, t.ndim as usize) }
+ unsafe { crate::util::slice_from_nullable_raw_parts(t.shape, t.ndim as
usize) }
}
/// The number of dimensions.
@@ -243,6 +246,9 @@ impl Tensor {
}
/// The number of elements.
+ ///
+ /// # Panics
+ /// As [`DLTensorExt::numel`].
pub fn numel(&self) -> usize {
self.data.dltensor.numel()
}
@@ -320,6 +326,9 @@ impl Tensor {
///
/// # Returns
/// The new tensor, with row-major strides.
+ ///
+ /// # Panics
+ /// If an extent of `shape` is negative or the byte size overflows `isize`.
pub fn from_nd_alloc<TNDAlloc>(
alloc: TNDAlloc,
shape: &[i64],
@@ -329,17 +338,23 @@ impl Tensor {
where
TNDAlloc: NDAllocator,
{
+ let dltensor = DLTensor {
+ data: std::ptr::null_mut(),
+ device,
+ ndim: shape.len() as i32,
+ dtype,
+ shape: shape.as_ptr() as *mut i64,
+ strides: std::ptr::null_mut(),
+ byte_offset: 0,
+ };
+ // Allocators size the data from the shape, so check it before
allocating.
+ data_size(&dltensor);
let tensor_obj = TensorObjFromNDAlloc {
base_: TensorObj {
object: Object::new(),
dltensor: DLTensor {
- data: std::ptr::null_mut(),
- device,
- ndim: shape.len() as i32,
- dtype,
shape: std::ptr::null_mut(),
- strides: std::ptr::null_mut(),
- byte_offset: 0,
+ ..dltensor
},
},
alloc,
@@ -409,14 +424,15 @@ impl Tensor {
/// The new tensor, allocated with [`CPUNDAlloc`].
///
/// # Errors
- /// A `ValueError` if `slice` does not have as many elements as `shape`.
+ /// A `ValueError` if `slice` does not have as many elements as `shape`,
+ /// or `shape` has a negative extent.
pub fn from_slice<T: AsDLDataType>(slice: &[T], shape: &[i64]) ->
Result<Self> {
+ if checked_numel(shape) != Some(slice.len()) {
+ crate::bail!(crate::error::VALUE_ERROR, "Slice length mismatch");
+ }
let dtype = T::DL_DATA_TYPE;
let device = DLDevice::new(DLDeviceType::kDLCPU, 0);
let tensor = Tensor::from_nd_alloc(CPUNDAlloc, shape, dtype, device);
- if tensor.numel() != slice.len() {
- crate::bail!(crate::error::VALUE_ERROR, "Slice length mismatch");
- }
// SAFETY: the tensor was just allocated, so nothing else can access
it.
unsafe { tensor.data_as_slice_mut::<T>()? }.copy_from_slice(slice);
Ok(tensor)
@@ -432,9 +448,7 @@ unsafe impl NDAllocator for CPUNDAlloc {
const MIN_ALIGN: usize = 64;
unsafe fn alloc_data(&mut self, prototype: &DLTensor) -> *mut
core::ffi::c_void {
- let numel = prototype.numel();
- let item_size = prototype.item_size();
- let size = numel * item_size;
+ let size = data_size(prototype);
// A zero-size allocation is undefined behavior; an empty tensor gets
an
// aligned dangling pointer, which `free_data` does not release.
if size == 0 {
@@ -449,9 +463,7 @@ unsafe impl NDAllocator for CPUNDAlloc {
}
unsafe fn free_data(&mut self, tensor: &DLTensor) {
- let numel = tensor.numel();
- let item_size = tensor.item_size();
- let size = numel * item_size;
+ let size = data_size(tensor);
if size == 0 {
return;
}
@@ -539,15 +551,15 @@ impl<'a> TensorView<'a> {
/// The extent of each dimension.
pub fn shape(&self) -> &'a [i64] {
let t = self.dltensor();
- if t.ndim == 0 {
- return &[];
- }
- unsafe { std::slice::from_raw_parts(t.shape, t.ndim as usize) }
+ unsafe { crate::util::slice_from_nullable_raw_parts(t.shape, t.ndim as
usize) }
}
/// The number of elements.
+ ///
+ /// # Panics
+ /// As [`DLTensorExt::numel`].
pub fn numel(&self) -> usize {
- self.shape().iter().product::<i64>() as usize
+ self.dltensor().numel()
}
/// The strides, in elements.
diff --git a/rust/tvm-ffi/src/device.rs b/rust/tvm-ffi/src/device.rs
index a7513d23..cd39363a 100644
--- a/rust/tvm-ffi/src/device.rs
+++ b/rust/tvm-ffi/src/device.rs
@@ -27,7 +27,7 @@ use tvm_ffi_sys::{TVMFFIEnvGetStream, TVMFFIEnvSetStream,
TVMFFIStreamHandle};
/// The current stream of `device`, as the caller or [`with_stream`] set it;
/// null if none is set.
pub fn current_stream(device: &DLDevice) -> TVMFFIStreamHandle {
- unsafe { TVMFFIEnvGetStream(device.device_type as i32, device.device_id) }
+ unsafe { TVMFFIEnvGetStream(device.device_type, device.device_id) }
}
/// Call `f` with the device stream temporarily set to `stream`.
@@ -46,7 +46,7 @@ pub unsafe fn with_stream<T>(
let mut prev_stream: TVMFFIStreamHandle = std::ptr::null_mut();
unsafe {
crate::check_safe_call!(TVMFFIEnvSetStream(
- device.device_type as i32,
+ device.device_type,
device.device_id,
stream,
&mut prev_stream as *mut TVMFFIStreamHandle
@@ -74,7 +74,7 @@ impl RestoreStream {
fn set(&self) -> i32 {
unsafe {
TVMFFIEnvSetStream(
- self.device.device_type as i32,
+ self.device.device_type,
self.device.device_id,
self.stream,
std::ptr::null_mut(),
@@ -99,8 +99,8 @@ impl Drop for RestoreStream {
unsafe impl AnyCompatible for DLDevice {
#[inline]
unsafe fn is_undefined(field: *const Self) -> bool {
- // No device type is zero; read as an integer, since zero is no
`DLDeviceType`.
- *std::ptr::addr_of!((*field).device_type).cast::<u32>() == 0
+ // No device type is zero.
+ *std::ptr::addr_of!((*field).device_type) == 0
}
fn type_str() -> String {
diff --git a/rust/tvm-ffi/src/dtype.rs b/rust/tvm-ffi/src/dtype.rs
index 8aa77ca3..d926fd6e 100644
--- a/rust/tvm-ffi/src/dtype.rs
+++ b/rust/tvm-ffi/src/dtype.rs
@@ -122,14 +122,19 @@ unsafe impl AnyCompatible for DLDataType {
}
/// A Rust element type of a tensor, such as `f32`, and its `DLDataType`.
-pub trait AsDLDataType: Copy {
+///
+/// # Safety
+/// Tensor data of `DL_DATA_TYPE` is read as `Self` without checks, so `Self`
+/// must have that data type's item size, be no more aligned than the item
+/// size, and accept every bit pattern of it.
+pub unsafe trait AsDLDataType: Copy {
/// The data type of `Self`.
const DL_DATA_TYPE: DLDataType;
}
macro_rules! impl_as_dl_data_type {
($type: ty, $code: expr, $bits: expr) => {
- impl AsDLDataType for $type {
+ unsafe impl AsDLDataType for $type {
const DL_DATA_TYPE: DLDataType = DLDataType {
code: $code as u8,
bits: $bits as u8,
diff --git a/rust/tvm-ffi/src/error.rs b/rust/tvm-ffi/src/error.rs
index 48f6cf58..2cf64ae5 100644
--- a/rust/tvm-ffi/src/error.rs
+++ b/rust/tvm-ffi/src/error.rs
@@ -281,7 +281,8 @@ impl Error {
///
/// The kind of the error, such as [`VALUE_ERROR`].
pub fn kind(&self) -> ErrorKind<'_> {
- ErrorKind(self.data.cell.kind.as_str())
+ // SAFETY: the error cell holds UTF-8 text that lives as long as the
error.
+ ErrorKind(unsafe { self.data.cell.kind.as_str() })
}
/// Get the message of the error.
@@ -290,7 +291,8 @@ impl Error {
///
/// The message of the error.
pub fn message(&self) -> &str {
- self.data.cell.message.as_str()
+ // SAFETY: the error cell holds UTF-8 text that lives as long as the
error.
+ unsafe { self.data.cell.message.as_str() }
}
/// Get the backtrace of the error.
@@ -299,7 +301,8 @@ impl Error {
///
/// The backtrace, most recent call first.
pub fn backtrace(&self) -> &str {
- self.data.cell.backtrace.as_str()
+ // SAFETY: the error cell holds UTF-8 text that lives as long as the
error.
+ unsafe { self.data.cell.backtrace.as_str() }
}
/// The error that caused this one, the next link of the cause chain; the
diff --git a/rust/tvm-ffi/src/extra/structural_common.rs
b/rust/tvm-ffi/src/extra/structural_common.rs
index 441fe017..79afcae5 100644
--- a/rust/tvm-ffi/src/extra/structural_common.rs
+++ b/rust/tvm-ffi/src/extra/structural_common.rs
@@ -225,10 +225,11 @@ unsafe fn visit_field_level<B>(
info: *const TVMFFITypeInfo,
callback: &mut impl FnMut(&'static TVMFFIFieldInfo) -> ControlFlow<B>,
) -> Option<B> {
- if info.is_null() || (*info).fields.is_null() {
+ if info.is_null() {
return None;
}
- let fields = std::slice::from_raw_parts((*info).fields, (*info).num_fields
as usize);
+ let fields =
+ crate::util::slice_from_nullable_raw_parts((*info).fields,
(*info).num_fields as usize);
for field in fields {
// C reflection tables are immortal once registered.
let field: &'static TVMFFIFieldInfo = &*(field as *const
TVMFFIFieldInfo);
diff --git a/rust/tvm-ffi/src/extra/structural_mutate.rs
b/rust/tvm-ffi/src/extra/structural_mutate.rs
index 2d205453..ec207551 100644
--- a/rust/tvm-ffi/src/extra/structural_mutate.rs
+++ b/rust/tvm-ffi/src/extra/structural_mutate.rs
@@ -1583,7 +1583,8 @@ fn call_field_setter(
value: &TVMFFIAny,
) -> Result<()> {
if field.setter.is_null() {
- return Err(missing_field_setter_error(field.name.as_str()));
+ // SAFETY: registered field names are UTF-8 text that lives as long as
the process.
+ return Err(missing_field_setter_error(unsafe { field.name.as_str() }));
}
let return_code = unsafe {
if field.flags & FLAG_SETTER_IS_FUNCTION == 0 {
diff --git a/rust/tvm-ffi/src/function.rs b/rust/tvm-ffi/src/function.rs
index b8dd4e0b..009f58a6 100644
--- a/rust/tvm-ffi/src/function.rs
+++ b/rust/tvm-ffi/src/function.rs
@@ -116,7 +116,7 @@ impl<F: Fn(&[AnyView]) -> Result<Any> + Send + Sync +
'static> CallbackFunctionO
result: *mut TVMFFIAny,
) -> i32 {
let this = &*(handle as *mut Self);
- let packed_args = std::slice::from_raw_parts(args as *const AnyView,
num_args as usize);
+ let packed_args = crate::function_internal::packed_args(args,
num_args);
let ret_value = match
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
(this.callback)(packed_args)
})) {
diff --git a/rust/tvm-ffi/src/function_internal.rs
b/rust/tvm-ffi/src/function_internal.rs
index c992d7ea..f9de7fd6 100644
--- a/rust/tvm-ffi/src/function_internal.rs
+++ b/rust/tvm-ffi/src/function_internal.rs
@@ -35,6 +35,17 @@ pub fn panic_to_error(payload: Box<dyn std::any::Any +
Send>) -> Error {
Error::from_panic(payload)
}
+/// The arguments of a packed call from C.
+///
+/// # Safety
+/// If `num_args` is positive, `args` must point to `num_args` valid
+/// arguments that outlive `'a`.
+#[doc(hidden)]
+#[inline(always)]
+pub unsafe fn packed_args<'a>(args: *const TVMFFIAny, num_args: i32) -> &'a
[AnyView<'a>] {
+ crate::util::slice_from_nullable_raw_parts(args as *const AnyView,
num_args as usize)
+}
+
pub trait AsPackedCallable<I, O> {
// Call the function in packed convention
fn call_packed(&self, packed_args: &[AnyView]) -> Result<Any>;
diff --git a/rust/tvm-ffi/src/lib.rs b/rust/tvm-ffi/src/lib.rs
index 7012023d..2e027dd8 100644
--- a/rust/tvm-ffi/src/lib.rs
+++ b/rust/tvm-ffi/src/lib.rs
@@ -66,6 +66,7 @@ pub mod rvalue_ref;
pub mod static_init;
pub mod string;
pub mod type_traits;
+mod util;
pub use tvm_ffi_sys;
pub use crate::any::{Any, AnyView};
diff --git a/rust/tvm-ffi/src/macros.rs b/rust/tvm-ffi/src/macros.rs
index 4b59ecb4..a37ece0d 100644
--- a/rust/tvm-ffi/src/macros.rs
+++ b/rust/tvm-ffi/src/macros.rs
@@ -641,8 +641,7 @@ macro_rules! tvm_ffi_dll_export_typed_func {
num_args: i32,
result: *mut $crate::tvm_ffi_sys::TVMFFIAny,
) -> i32 {
- let packed_args =
- std::slice::from_raw_parts(args as *const
$crate::any::AnyView, num_args as usize);
+ let packed_args = $crate::function_internal::packed_args(args,
num_args);
let ret_value = match
::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| {
$crate::function_internal::call_packed_callable($func,
packed_args)
})) {
diff --git a/rust/tvm-ffi/src/reflection/accessor.rs
b/rust/tvm-ffi/src/reflection/accessor.rs
index 2b1f111b..40d10a7d 100644
--- a/rust/tvm-ffi/src/reflection/accessor.rs
+++ b/rust/tvm-ffi/src/reflection/accessor.rs
@@ -101,7 +101,8 @@ pub fn type_key(type_index: i32) -> Option<&'static str> {
// SAFETY: the index is registered, and type information lives as long as
// the process.
let info: &'static TVMFFITypeInfo = unsafe {
&*TVMFFIGetTypeInfo(type_index) };
- Some(info.type_key.as_str())
+ // SAFETY: a type key is UTF-8 text.
+ Some(unsafe { info.type_key.as_str() })
}
/// The default value `T` registers for its field `name`, or `None` if `T`
@@ -113,12 +114,14 @@ pub fn field_default<T: ObjectCore>(name: &str) ->
Option<Any> {
// SAFETY: `ObjectCore` types are registered, and type information lives
// as long as the process.
let info: &'static TVMFFITypeInfo = unsafe {
&*TVMFFIGetTypeInfo(T::type_index()) };
- if info.fields.is_null() {
- return None;
- }
// SAFETY: a registered type lists `num_fields` initialized fields.
- let fields = unsafe { std::slice::from_raw_parts(info.fields,
info.num_fields as usize) };
- let field = fields.iter().find(|field| field.name.as_str() == name)?;
+ let fields = unsafe {
+ crate::util::slice_from_nullable_raw_parts(info.fields,
info.num_fields as usize)
+ };
+ // SAFETY: a field name is UTF-8 text.
+ let field = fields
+ .iter()
+ .find(|field| unsafe { field.name.as_str() } == name)?;
let has_default =
TVMFFIFieldFlagBitMask::kTVMFFIFieldFlagBitMaskHasDefault as i64;
let from_factory =
TVMFFIFieldFlagBitMask::kTVMFFIFieldFlagBitMaskDefaultFromFactory as i64;
if field.flags & has_default == 0 || field.flags & from_factory != 0 {
@@ -285,9 +288,11 @@ impl FieldGetter {
})?;
let field = unsafe { field.as_ref() };
let getter = field.getter.ok_or_else(|| {
+ // SAFETY: a field name is UTF-8 text.
+ let name = unsafe { field.name.as_str() };
Error::new(
TYPE_ERROR,
- &format!("Reflected field `{}` has no getter",
field.name.as_str()),
+ &format!("Reflected field `{name}` has no getter"),
"",
)
})?;
diff --git a/rust/tvm-ffi/src/reflection/unsafe_init.rs
b/rust/tvm-ffi/src/reflection/unsafe_init.rs
index ba4493c7..31b3fce4 100644
--- a/rust/tvm-ffi/src/reflection/unsafe_init.rs
+++ b/rust/tvm-ffi/src/reflection/unsafe_init.rs
@@ -188,7 +188,7 @@ mod tests {
assert!(!zero_is_undefined::<i64>());
assert!(!zero_is_undefined::<bool>());
- // `None`, through the niche of a reference or a device, or through a
tag.
+ // `None`, through the niche of a reference or through a tag.
assert!(!zero_is_undefined::<Option<Array<i64>>>());
assert!(!zero_is_undefined::<Option<DLDevice>>());
assert!(!zero_is_undefined::<Option<String>>());
@@ -197,7 +197,7 @@ mod tests {
#[test]
fn values_are_defined() {
let cpu = DLDevice {
- device_type: DLDeviceType::kDLCPU,
+ device_type: DLDeviceType::kDLCPU as i32,
device_id: 0,
};
assert!(!is_undefined(Array::<i64>::new()));
diff --git a/rust/tvm-ffi/src/string.rs b/rust/tvm-ffi/src/string.rs
index 247f1649..c91b33a3 100644
--- a/rust/tvm-ffi/src/string.rs
+++ b/rust/tvm-ffi/src/string.rs
@@ -524,7 +524,7 @@ unsafe impl AnyCompatible for Bytes {
if data.type_index == TypeIndex::kTVMFFIByteArrayPtr as i32 {
// deep copy from bytearray ptr
let bytes = &*(data.data_union.v_ptr as *const TVMFFIByteArray);
- Ok(Self::from(std::slice::from_raw_parts(
+ Ok(Self::from(crate::util::slice_from_nullable_raw_parts(
bytes.data, bytes.size,
)))
} else if data.type_index == TypeIndex::kTVMFFISmallBytes as i32 {
@@ -576,10 +576,9 @@ unsafe impl AnyCompatible for String {
unsafe fn try_cast_from_any_view(data: &TVMFFIAny) -> Result<Self, ()> {
if data.type_index == TypeIndex::kTVMFFIRawStr as i32 {
- // 1. Create a CStr wrapper from the raw pointer.
let c_str =
std::ffi::CStr::from_ptr(data.data_union.v_c_str as *const
std::os::raw::c_char);
- Ok(Self::from(c_str.to_str().expect("Invalid UTF-8")))
+ c_str.to_str().map(Self::from).map_err(|_| ())
} else if data.type_index == TypeIndex::kTVMFFISmallStr as i32 {
Ok(Self { data: *data })
} else if data.type_index == TypeIndex::kTVMFFIStr as i32 {
diff --git a/rust/tvm-ffi/src/type_traits.rs b/rust/tvm-ffi/src/type_traits.rs
index 4495a103..f6aba47c 100644
--- a/rust/tvm-ffi/src/type_traits.rs
+++ b/rust/tvm-ffi/src/type_traits.rs
@@ -785,8 +785,8 @@ unsafe impl<T: AnyCompatible> AnyCompatible for Option<T> {
unsafe fn is_undefined(field: *const Self) -> bool {
// With a tag, all-zero bytes are `None`. A niche-optimized `Option<T>`
// keeps `T`'s bytes, where all-zero bytes are `None` if `None` looks
- // undefined to `T` (a reference's null pointer, a device's zero type),
- // and undefined if `None` sits elsewhere in the niche.
+ // undefined to `T` (a reference's null pointer), and undefined if
+ // `None` sits elsewhere in the niche.
if std::mem::size_of::<Self>() != std::mem::size_of::<T>() {
return false;
}
diff --git a/rust/tvm-ffi/src/util.rs b/rust/tvm-ffi/src/util.rs
new file mode 100644
index 00000000..515e2b20
--- /dev/null
+++ b/rust/tvm-ffi/src/util.rs
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+//! Crate-internal helpers.
+
+/// [`std::slice::from_raw_parts`] that allows a null `data` when `len` is 0.
+///
+/// # Safety
+/// As for [`std::slice::from_raw_parts`] when `len` is not 0.
+#[inline(always)]
+pub(crate) unsafe fn slice_from_nullable_raw_parts<'a, T>(data: *const T, len:
usize) -> &'a [T] {
+ if len == 0 {
+ return &[];
+ }
+ std::slice::from_raw_parts(data, len)
+}
+
+/// [`std::slice::from_raw_parts_mut`] that allows a null `data` when `len` is
0.
+///
+/// # Safety
+/// As for [`std::slice::from_raw_parts_mut`] when `len` is not 0.
+#[inline(always)]
+pub(crate) unsafe fn slice_from_nullable_raw_parts_mut<'a, T>(
+ data: *mut T,
+ len: usize,
+) -> &'a mut [T] {
+ if len == 0 {
+ return &mut [];
+ }
+ std::slice::from_raw_parts_mut(data, len)
+}
diff --git a/rust/tvm-ffi/tests/test_any.rs b/rust/tvm-ffi/tests/test_any.rs
index fa91998b..c73bed03 100644
--- a/rust/tvm-ffi/tests/test_any.rs
+++ b/rust/tvm-ffi/tests/test_any.rs
@@ -326,13 +326,13 @@ fn test_any_dl_device() {
// Test Any with CPU device
assert_eq!(any_cpu.type_index(), TypeIndex::kTVMFFIDevice as i32);
let converted_cpu = DLDevice::try_from(any_cpu).unwrap();
- assert_eq!(converted_cpu.device_type, DLDeviceType::kDLCPU);
+ assert_eq!(converted_cpu.device_type, DLDeviceType::kDLCPU as i32);
assert_eq!(converted_cpu.device_id, 0);
// Test AnyView with CPU device
assert_eq!(any_view_cpu.type_index(), TypeIndex::kTVMFFIDevice as i32);
let converted_cpu_view = DLDevice::try_from(any_view_cpu).unwrap();
- assert_eq!(converted_cpu_view.device_type, DLDeviceType::kDLCPU);
+ assert_eq!(converted_cpu_view.device_type, DLDeviceType::kDLCPU as i32);
assert_eq!(converted_cpu_view.device_id, 0);
// Test DLDevice with CUDA device
@@ -343,13 +343,16 @@ fn test_any_dl_device() {
// Test Any with CUDA device
assert_eq!(any_cuda.type_index(), TypeIndex::kTVMFFIDevice as i32);
let converted_cuda = DLDevice::try_from(any_cuda).unwrap();
- assert_eq!(converted_cuda.device_type, DLDeviceType::kDLCUDA);
+ assert_eq!(converted_cuda.device_type, DLDeviceType::kDLCUDA as i32);
assert_eq!(converted_cuda.device_id, 1);
// Test AnyView with CUDA device
assert_eq!(any_view_cuda.type_index(), TypeIndex::kTVMFFIDevice as i32);
let converted_cuda_view = DLDevice::try_from(any_view_cuda).unwrap();
- assert_eq!(converted_cuda_view.device_type, DLDeviceType::kDLCUDA);
+ assert_eq!(
+ converted_cuda_view.device_type,
+ DLDeviceType::kDLCUDA as i32
+ );
assert_eq!(converted_cuda_view.device_id, 1);
}
diff --git a/rust/tvm-ffi/tests/test_map.rs b/rust/tvm-ffi/tests/test_map.rs
index 950c8f64..dcc37344 100644
--- a/rust/tvm-ffi/tests/test_map.rs
+++ b/rust/tvm-ffi/tests/test_map.rs
@@ -382,3 +382,15 @@ fn test_map_any_cast_slow_path_converts() {
assert_eq!(AnyView::from(&real).debug_strong_count(), Some(1));
assert_eq!(AnyView::from(&converted).debug_strong_count(), Some(1));
}
+
+#[test]
+fn test_map_from_dict_copies() {
+ let dict = Function::get_global("ffi.Dict")
+ .unwrap()
+ .call_tuple((String::from("a"), 1i64, String::from("b"), 2i64))
+ .unwrap();
+ let map = Map::<String, i64>::try_from(dict.clone()).unwrap();
+ assert_eq!(map.len(), 2);
+ assert_eq!(map.get(&String::from("b")).unwrap(), Some(2));
+ assert_eq!(dict.debug_strong_count(), Some(1));
+}
diff --git a/rust/tvm-ffi/tests/test_object_reflection.rs
b/rust/tvm-ffi/tests/test_object_reflection.rs
index a7dbf137..18990179 100644
--- a/rust/tvm-ffi/tests/test_object_reflection.rs
+++ b/rust/tvm-ffi/tests/test_object_reflection.rs
@@ -177,7 +177,10 @@ fn register_types() {
fn test_reflected_fields_are_registered_with_the_type() {
register_types();
let info = type_info::<PointObj>();
- let names: Vec<&str> = fields(info).iter().map(|f|
f.name.as_str()).collect();
+ let names: Vec<&str> = fields(info)
+ .iter()
+ .map(|f| unsafe { f.name.as_str() })
+ .collect();
assert_eq!(names, ["x", "y", "label"]);
let [x, y, label] = fields(info) else {
@@ -187,7 +190,7 @@ fn test_reflected_fields_are_registered_with_the_type() {
assert_eq!(x.offset, std::mem::offset_of!(PointObj, x) as i64);
assert_eq!(x.field_static_type_index, TypeIndex::kTVMFFIInt as i32);
assert_eq!(
- x.metadata.as_str(),
+ unsafe { x.metadata.as_str() },
r#"{"type_schema":"{\"type\":\"int\"}"}"#
);
assert_eq!(
@@ -212,10 +215,13 @@ fn test_reflected_fields_are_registered_with_the_type() {
// The subtype registers only its own fields, and inherits the kind.
let info = type_info::<Point3Obj>();
- let names: Vec<&str> = fields(info).iter().map(|f|
f.name.as_str()).collect();
+ let names: Vec<&str> = fields(info)
+ .iter()
+ .map(|f| unsafe { f.name.as_str() })
+ .collect();
assert_eq!(names, ["z", "labels", "type"]);
assert_eq!(
- fields(info)[1].metadata.as_str(),
+ unsafe { fields(info)[1].metadata.as_str() },
r#"{"type_schema":"{\"type\":\"ffi.Array\",\"args\":[{\"type\":\"ffi.String\"}]}"}"#
);
assert_eq!(
@@ -230,12 +236,12 @@ fn test_field_docs_come_from_doc_comments_or_reflect() {
let [x, y, label] = fields(type_info::<PointObj>()) else {
unreachable!()
};
- assert_eq!(x.doc.as_str(), "The x coordinate.\n\nSigned.");
- assert_eq!(y.doc.as_str(), "The y coordinate.");
- assert_eq!(label.doc.as_str(), "");
+ assert_eq!(unsafe { x.doc.as_str() }, "The x coordinate.\n\nSigned.");
+ assert_eq!(unsafe { y.doc.as_str() }, "The y coordinate.");
+ assert_eq!(unsafe { label.doc.as_str() }, "");
// A renamed field keeps its Rust type and offset.
let labels = &fields(type_info::<Point3Obj>())[1];
- assert_eq!(labels.name.as_str(), "labels");
+ assert_eq!(unsafe { labels.name.as_str() }, "labels");
assert_eq!(labels.offset, std::mem::offset_of!(Point3Obj, tags) as i64);
let p3 = point3(1, 2);
let getter = FieldGetter::new(Point3Obj::type_index(), "labels").unwrap();
@@ -247,29 +253,32 @@ fn test_field_docs_come_from_doc_comments_or_reflect() {
fn test_methods_are_registered_with_the_type() {
register_types();
let info = type_info::<PointObj>();
- let names: Vec<&str> = methods(info).iter().map(|m|
m.name.as_str()).collect();
+ let names: Vec<&str> = methods(info)
+ .iter()
+ .map(|m| unsafe { m.name.as_str() })
+ .collect();
assert_eq!(names, ["norm1", "shifted", "origin"]);
let [norm1, shifted, origin] = methods(info) else {
unreachable!()
};
assert_eq!(norm1.flags, 0);
- assert_eq!(norm1.doc.as_str(), "The L1 norm.");
- assert_eq!(shifted.doc.as_str(), "");
+ assert_eq!(unsafe { norm1.doc.as_str() }, "The L1 norm.");
+ assert_eq!(unsafe { shifted.doc.as_str() }, "");
assert_eq!(
origin.flags,
TVMFFIFieldFlagBitMask::kTVMFFIFieldFlagBitMaskIsStaticMethod as i64
);
- assert_eq!(origin.doc.as_str(), "The origin.");
+ assert_eq!(unsafe { origin.doc.as_str() }, "The origin.");
// Each method records its type schema, as C++ `ObjectDef::def` does.
let point = r#"{"type":"testing.rust.ReflectedPoint"}"#;
assert_eq!(
- shifted.metadata.as_str(),
+ unsafe { shifted.metadata.as_str() },
metadata_of(&format!(
r#"{{"type":"ffi.Function","named_args":{{"return":[{point}],"params":[{point},{{"type":"int"}}]}}}}"#
))
);
assert_eq!(
- origin.metadata.as_str(),
+ unsafe { origin.metadata.as_str() },
metadata_of(&format!(
r#"{{"type":"ffi.Function","named_args":{{"return":[{point}],"params":[]}}}}"#
))
diff --git a/rust/tvm-ffi/tests/test_rs_object.rs
b/rust/tvm-ffi/tests/test_rs_object.rs
index 848fef67..6d4ad954 100644
--- a/rust/tvm-ffi/tests/test_rs_object.rs
+++ b/rust/tvm-ffi/tests/test_rs_object.rs
@@ -273,7 +273,7 @@ fn type_info<N: ObjectCore>() -> &'static TVMFFITypeInfo {
fn field_names<N: ObjectCore>() -> Vec<&'static str> {
fields(type_info::<N>())
.iter()
- .map(|f| f.name.as_str())
+ .map(|f| unsafe { f.name.as_str() })
.collect()
}
@@ -333,12 +333,12 @@ fn test_fields_are_registered_with_their_flags() {
};
assert_eq!(x.flags, FieldFlags::NONE.bits());
assert_eq!(x.offset, std::mem::offset_of!(RsPointObj, x) as i64);
- assert_eq!(x.doc.as_str(), "The x coordinate.");
+ assert_eq!(unsafe { x.doc.as_str() }, "The x coordinate.");
assert_eq!(
label.flags,
(FieldFlags::WRITABLE | FieldFlags::SEQ_HASH_IGNORE).bits()
);
- assert_eq!(label.doc.as_str(), "A label.");
+ assert_eq!(unsafe { label.doc.as_str() }, "A label.");
let metadata = unsafe { &*type_info::<RsPointObj>().metadata };
assert_eq!(
metadata.total_size as usize,
@@ -370,8 +370,11 @@ fn test_reflection_registers_methods() {
let info = type_info::<RsPointObj>();
assert_eq!(info.num_methods, 1);
let method = unsafe { &*info.methods };
- assert_eq!(method.name.as_str(), "with_label");
- assert_eq!(method.doc.as_str(), "The point with another label.");
+ assert_eq!(unsafe { method.name.as_str() }, "with_label");
+ assert_eq!(
+ unsafe { method.doc.as_str() },
+ "The point with another label."
+ );
let with_label = tvm_ffi::reflection::get_method("testing.rust.RsPoint",
"with_label").unwrap();
let relabeled = with_label
.call_tuple((point(4, "a"), tvm_ffi::String::from("b")))
diff --git a/rust/tvm-ffi/tests/test_tensor.rs
b/rust/tvm-ffi/tests/test_tensor.rs
index 7cb6cd08..85704d5b 100644
--- a/rust/tvm-ffi/tests/test_tensor.rs
+++ b/rust/tvm-ffi/tests/test_tensor.rs
@@ -35,7 +35,7 @@ fn test_tensor_basic() {
assert_eq!(tensor.ndim(), 3);
assert_eq!(tensor.dtype().code, DLDataTypeCode::kDLFloat as u8);
assert_eq!(tensor.dtype().bits, 32_u8);
- assert_eq!(tensor.device().device_type, DLDeviceType::kDLCPU);
+ assert_eq!(tensor.device().device_type, DLDeviceType::kDLCPU as i32);
// Test strides (should be calculated correctly for row-major layout)
let strides = tensor.strides();