This is an automated email from the ASF dual-hosted git repository.
tlopex 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 3523725e [FEAT][RUST] Add object binding runtime support (#716)
3523725e is described below
commit 3523725eb7b04e1c617063bd0ce77037cbf48599
Author: Shushi Hong <[email protected]>
AuthorDate: Sat Aug 29 19:45:58 2026 -0400
[FEAT][RUST] Add object binding runtime support (#716)
This PR adds the Rust runtime support needed by handwritten and
stubgen-generated object bindings.
### Object reference support
- Adds zero-copy derived-to-base conversions through
`impl_object_upcast!`
- Generates `From<&ObjectRef>` conversions for inexpensive owned-handle
cloning
- Adds `ObjectRefCore::same_as` for object identity comparison
- Adds the owning and hashable `ObjectIdentity` key
- Makes `ObjectRefCore::from_data` unsafe because typed reference views
may require additional invariants
### Reflection access
- Adds `TypeAttrColumn` and `get_type_attr` for type-attribute lookup
- Adds `FieldGetter` for cached access to reflected object fields
- Adds `Function::from_type_attr`
- Reuses the public type-attribute implementation in structural visit
and mutate
### Packed-call support
- Adds `RValueRef<T>`, compatible with C++ `ffi::RValueRef<T>`, for
move-aware object arguments
- Avoids an unnecessary reference-count increment when the callee
consumes an object argument
- Adds typed packed-call holders for `Any`, `Option<T>`, `Array<T>`,
`DLDataType`, `DLDevice`, and unit values
No C++ implementation or C ABI changes are introduced.
The complete Rust workspace test suite passes.
---
rust/tvm-ffi-macros/src/object_macros.rs | 9 +-
rust/tvm-ffi/src/collections/array.rs | 5 +-
rust/tvm-ffi/src/collections/map.rs | 2 +-
rust/tvm-ffi/src/extra/structural_mutate.rs | 11 +-
rust/tvm-ffi/src/extra/structural_visit.rs | 47 ++----
rust/tvm-ffi/src/function.rs | 17 ++
rust/tvm-ffi/src/function_internal.rs | 127 ++++++++++++++-
rust/tvm-ffi/src/lib.rs | 8 +-
rust/tvm-ffi/src/macros.rs | 45 ++++++
rust/tvm-ffi/src/object.rs | 72 ++++++++-
rust/tvm-ffi/src/reflection.rs | 234 ++++++++++++++++++++++++++++
rust/tvm-ffi/src/rvalue_ref.rs | 183 ++++++++++++++++++++++
rust/tvm-ffi/tests/test_cast.rs | 21 +++
rust/tvm-ffi/tests/test_function.rs | 50 ++++++
rust/tvm-ffi/tests/test_object.rs | 25 +++
rust/tvm-ffi/tests/test_structural_visit.rs | 27 +++-
16 files changed, 825 insertions(+), 58 deletions(-)
diff --git a/rust/tvm-ffi-macros/src/object_macros.rs
b/rust/tvm-ffi-macros/src/object_macros.rs
index a285740e..68acb3df 100644
--- a/rust/tvm-ffi-macros/src/object_macros.rs
+++ b/rust/tvm-ffi-macros/src/object_macros.rs
@@ -161,11 +161,18 @@ pub fn derive_object_ref(input: proc_macro::TokenStream)
-> TokenStream {
this.data
}
#[inline]
- fn from_data(data: ObjectArc<Self::ContainerType>) -> Self {
+ unsafe fn from_data(data: ObjectArc<Self::ContainerType>) -> Self {
Self { data}
}
}
+ impl ::std::convert::From<&#struct_name> for #struct_name {
+ #[inline]
+ fn from(value: &#struct_name) -> Self {
+ value.clone()
+ }
+ }
+
// implement AnyCompatible for #struct_name
unsafe impl #tvm_ffi_crate::type_traits::AnyCompatible for
#struct_name {
const MATCH_ANY_EXACT: bool = {
diff --git a/rust/tvm-ffi/src/collections/array.rs
b/rust/tvm-ffi/src/collections/array.rs
index 614125ff..86f5d28f 100644
--- a/rust/tvm-ffi/src/collections/array.rs
+++ b/rust/tvm-ffi/src/collections/array.rs
@@ -81,7 +81,7 @@ unsafe impl<T: ContainerElement + Clone> ObjectRefCore for
Array<T> {
this.data
}
- fn from_data(data: ObjectArc<Self::ContainerType>) -> Self {
+ unsafe fn from_data(data: ObjectArc<Self::ContainerType>) -> Self {
Self {
data,
_marker: PhantomData,
@@ -122,7 +122,8 @@ impl<T: ContainerElement + Clone> Array<T> {
core::ptr::write(base_ptr.add(i), raw);
}
}
- Self::from_data(arc)
+ // SAFETY: `arc` was allocated and initialized above as an `Array<T>`.
+ unsafe { Self::from_data(arc) }
}
pub fn len(&self) -> usize {
diff --git a/rust/tvm-ffi/src/collections/map.rs
b/rust/tvm-ffi/src/collections/map.rs
index 64154bb1..50ae01eb 100644
--- a/rust/tvm-ffi/src/collections/map.rs
+++ b/rust/tvm-ffi/src/collections/map.rs
@@ -126,7 +126,7 @@ unsafe impl<K, V> ObjectRefCore for Map<K, V> {
this.data
}
- fn from_data(data: ObjectArc<MapObj>) -> Self {
+ unsafe fn from_data(data: ObjectArc<MapObj>) -> Self {
Self {
data,
_marker: PhantomData,
diff --git a/rust/tvm-ffi/src/extra/structural_mutate.rs
b/rust/tvm-ffi/src/extra/structural_mutate.rs
index 7d5b607a..5fc7da44 100644
--- a/rust/tvm-ffi/src/extra/structural_mutate.rs
+++ b/rust/tvm-ffi/src/extra/structural_mutate.rs
@@ -37,6 +37,7 @@ use crate::any::{Any, AnyView};
use crate::error::{Error, Result, RUNTIME_ERROR, TYPE_ERROR};
use crate::function::Function;
use crate::object::{self, Object, ObjectArc, ObjectCore};
+use crate::reflection::TypeAttrColumn;
use crate::tvm_ffi_sys::TVMFFIFieldFlagBitMask::{
kTVMFFIFieldFlagBitMaskSEqHashIgnore,
kTVMFFIFieldFlagBitSetterIsFunctionObj,
};
@@ -53,7 +54,7 @@ use super::structural_common::{
};
use super::structural_visit::{
field_def_region, for_each_field_info, free_var_child_region,
type_attr_column, type_key_of,
- DefRegionKind, TypeAttrColumn, WalkOrder,
+ DefRegionKind, WalkOrder,
};
const STRUCTURAL_MUTATE_ATTR: &str = "__s_mutate__";
@@ -1952,8 +1953,8 @@ fn call_registered_structural_mutate(
) -> Result<Option<Any>> {
let use_inplace = permit == Permit::MaybeInPlace && object_is_unique(raw);
if use_inplace {
- if let Some(attr) =
- structural_maybe_inplace_mutate_column().and_then(|column|
column.get(raw.type_index))
+ if let Some(attr) = structural_maybe_inplace_mutate_column()
+ .and_then(|column| column.get_raw(raw.type_index))
{
if attr.type_index == TVMFFITypeIndex::kTVMFFIOpaquePtr as i32
|| attr.type_index == TVMFFITypeIndex::kTVMFFIFunction as i32
@@ -1963,7 +1964,7 @@ fn call_registered_structural_mutate(
}
}
- let Some(attr) = structural_mutate_column().and_then(|column|
column.get(raw.type_index))
+ let Some(attr) = structural_mutate_column().and_then(|column|
column.get_raw(raw.type_index))
else {
return Ok(None);
};
@@ -2162,7 +2163,7 @@ where
}
fn shallow_copy(raw: TVMFFIAny) -> Result<Any> {
- let Some(attr) = shallow_copy_column().and_then(|column|
column.get(raw.type_index)) else {
+ let Some(attr) = shallow_copy_column().and_then(|column|
column.get_raw(raw.type_index)) else {
return Err(Error::new(
TYPE_ERROR,
&format!(
diff --git a/rust/tvm-ffi/src/extra/structural_visit.rs
b/rust/tvm-ffi/src/extra/structural_visit.rs
index 5fb5b7fa..511a5a93 100644
--- a/rust/tvm-ffi/src/extra/structural_visit.rs
+++ b/rust/tvm-ffi/src/extra/structural_visit.rs
@@ -55,14 +55,14 @@ use crate::any::{Any, AnyView};
use crate::error::{Error, Result, RUNTIME_ERROR, TYPE_ERROR};
use crate::function::Function;
use crate::object::{Object, ObjectArc, ObjectCore};
+use crate::reflection::TypeAttrColumn;
use crate::tvm_ffi_sys::TVMFFIFieldFlagBitMask::{
kTVMFFIFieldFlagBitMaskSEqHashDefNonRecursive,
kTVMFFIFieldFlagBitMaskSEqHashDefRecursive,
kTVMFFIFieldFlagBitMaskSEqHashIgnore,
};
use crate::tvm_ffi_sys::{
- TVMFFIAny, TVMFFIByteArray, TVMFFIDefRegionKind, TVMFFIFieldInfo,
TVMFFIGetTypeAttrColumn,
- TVMFFIGetTypeInfo, TVMFFIObject, TVMFFISEqHashKind, TVMFFITypeAttrColumn,
TVMFFITypeIndex,
- TVMFFITypeKeyToIndex,
+ TVMFFIAny, TVMFFIByteArray, TVMFFIDefRegionKind, TVMFFIFieldInfo,
TVMFFIGetTypeInfo,
+ TVMFFIObject, TVMFFISEqHashKind, TVMFFITypeAttrColumn, TVMFFITypeIndex,
TVMFFITypeKeyToIndex,
};
use super::structural_common::{impl_callback_chain_tuple_arities,
with_structural_error_context};
@@ -1174,7 +1174,9 @@ fn visit_children_raw<C: ChildVisit>(
driver_context: *mut c_void,
def_region_kind: DefRegionKind,
) -> NativeResult {
- if let Some(attr) = structural_visit_column().and_then(|column|
column.get(value.type_index)) {
+ if let Some(attr) =
+ structural_visit_column().and_then(|column|
column.get_raw(value.type_index))
+ {
if attr.type_index != TVMFFITypeIndex::kTVMFFINone as i32 {
let active = active_structural_visitor()?;
return with_current_visitor_context(active, driver_context, || {
@@ -1907,37 +1909,8 @@ fn runtime_error(message: &str) -> Error {
Error::new(RUNTIME_ERROR, message, "")
}
-#[derive(Clone, Copy)]
-pub(crate) struct TypeAttrColumn(NonNull<TVMFFITypeAttrColumn>);
-
-impl TypeAttrColumn {
- pub(crate) unsafe fn from_non_null(pointer: NonNull<TVMFFITypeAttrColumn>)
-> Self {
- Self(pointer)
- }
-
- pub(crate) fn as_ptr(self) -> *mut TVMFFITypeAttrColumn {
- self.0.as_ptr()
- }
-
- /// Copy one borrowed cell; ownership remains with the registry.
- pub(crate) fn get(self, type_index: i32) -> Option<TVMFFIAny> {
- unsafe {
- let column = self.0.as_ref();
- let index = type_index - column.begin_index;
- if index < 0 || index >= column.size || column.data.is_null() {
- None
- } else {
- Some(*column.data.offset(index as isize))
- }
- }
- }
-}
-
pub(crate) fn type_attr_column(attr_name: &str) -> Option<TypeAttrColumn> {
- unsafe {
- let attr_name = TVMFFIByteArray::from_str(attr_name);
-
NonNull::new(TVMFFIGetTypeAttrColumn(&attr_name).cast_mut()).map(TypeAttrColumn)
- }
+ TypeAttrColumn::new(attr_name)
}
/// Cached `__s_visit__` column pointer (0 = not seen yet). A registry column
@@ -1951,7 +1924,7 @@ fn structural_visit_column() -> Option<TypeAttrColumn> {
let cached = STRUCTURAL_VISIT_COLUMN.load(Ordering::Relaxed);
if cached != 0 {
let pointer = cached as *mut TVMFFITypeAttrColumn;
- return Some(TypeAttrColumn(unsafe { NonNull::new_unchecked(pointer)
}));
+ return Some(unsafe {
TypeAttrColumn::from_non_null(NonNull::new_unchecked(pointer)) });
}
initialize_structural_visit_column()
}
@@ -1959,7 +1932,7 @@ fn structural_visit_column() -> Option<TypeAttrColumn> {
#[inline]
fn has_registered_visit_hook(type_index: i32) -> bool {
structural_visit_column()
- .and_then(|column| column.get(type_index))
+ .and_then(|column| column.get_raw(type_index))
.is_some_and(|attr| attr.type_index != TVMFFITypeIndex::kTVMFFINone as
i32)
}
@@ -1967,7 +1940,7 @@ fn has_registered_visit_hook(type_index: i32) -> bool {
#[inline(never)]
fn initialize_structural_visit_column() -> Option<TypeAttrColumn> {
let column = type_attr_column(STRUCTURAL_VISIT_ATTR)?;
- STRUCTURAL_VISIT_COLUMN.store(column.0.as_ptr() as usize,
Ordering::Relaxed);
+ STRUCTURAL_VISIT_COLUMN.store(column.as_ptr() as usize, Ordering::Relaxed);
Some(column)
}
diff --git a/rust/tvm-ffi/src/function.rs b/rust/tvm-ffi/src/function.rs
index 2d37a2bd..d87e72a9 100644
--- a/rust/tvm-ffi/src/function.rs
+++ b/rust/tvm-ffi/src/function.rs
@@ -255,6 +255,23 @@ impl Function {
}
}
+ /// Look up a function-valued attribute for a concrete runtime type.
+ ///
+ /// Type attributes are not inherited from base types.
+ pub fn from_type_attr(type_index: i32, attr_name: &str) ->
Result<Function> {
+ let value = crate::reflection::get_type_attr(type_index,
attr_name).ok_or_else(|| {
+ crate::error::Error::new(
+ crate::error::TYPE_ERROR,
+ &format!(
+ "Cannot find type attribute `{}` for type_index={}",
+ attr_name, type_index
+ ),
+ "",
+ )
+ })?;
+ Function::try_from(value)
+ }
+
/// Look up a reflected method of a type by type key and method name
///
/// Same as [`Function::from_type_method`], but resolves `type_key` to a
diff --git a/rust/tvm-ffi/src/function_internal.rs
b/rust/tvm-ffi/src/function_internal.rs
index 43aab97c..fae1b46c 100644
--- a/rust/tvm-ffi/src/function_internal.rs
+++ b/rust/tvm-ffi/src/function_internal.rs
@@ -18,6 +18,8 @@
*/
use crate::any::{Any, AnyView, ArgTryFromAnyView};
use crate::error::Result;
+use crate::object::ObjectRefCore;
+use crate::rvalue_ref::RValueRef;
use crate::string::{Bytes, String};
use crate::type_traits::{AnyCompatible, ContainerElement};
@@ -88,7 +90,25 @@ pub trait IntoArgHolder {
}
crate::impl_into_arg_holder_default!(
- bool, i8, i16, i32, i64, isize, u8, u16, u32, u64, usize, f32, f64,
String, Bytes
+ (),
+ bool,
+ i8,
+ i16,
+ i32,
+ i64,
+ isize,
+ u8,
+ u16,
+ u32,
+ u64,
+ usize,
+ f32,
+ f64,
+ String,
+ Bytes,
+ Any,
+ crate::DLDataType,
+ crate::DLDevice
);
// string will be converted to String for argument passing
@@ -148,12 +168,87 @@ pub trait ArgIntoRef {
fn to_ref(&self) -> &Self::Target;
}
+/// Convert a canonical argument holder into its packed ABI view.
+#[doc(hidden)]
+pub trait PackedArg {
+ fn as_packed_arg(&self) -> AnyView<'_>;
+}
+
+impl<T: AnyCompatible> PackedArg for T {
+ #[inline]
+ fn as_packed_arg(&self) -> AnyView<'_> {
+ AnyView::from(self)
+ }
+}
+
+impl PackedArg for Any {
+ #[inline]
+ fn as_packed_arg(&self) -> AnyView<'_> {
+ AnyView::from(self)
+ }
+}
+
+impl<T> PackedArg for RValueRef<T>
+where
+ T: ObjectRefCore + AnyCompatible,
+{
+ #[inline]
+ fn as_packed_arg(&self) -> AnyView<'_> {
+ AnyView::from(self)
+ }
+}
+
crate::impl_arg_into_ref!(
- bool, i8, i16, i32, i64, isize, u8, u16, u32, u64, usize, f32, f64,
String, Bytes
+ (),
+ bool,
+ i8,
+ i16,
+ i32,
+ i64,
+ isize,
+ u8,
+ u16,
+ u32,
+ u64,
+ usize,
+ f32,
+ f64,
+ String,
+ Bytes,
+ Any,
+ crate::DLDataType,
+ crate::DLDevice
);
-// Parametric containers pass by value/reference like the scalars above, but
-// their type parameters keep them out of the `impl_*!` macros.
+// Generic holders require explicit implementations rather than scalar macro
entries.
+impl<T: AnyCompatible> IntoArgHolder for Option<T> {
+ type Target = Self;
+ fn into_arg_holder(self) -> Self::Target {
+ self
+ }
+}
+
+impl<'a, T: AnyCompatible> IntoArgHolder for &'a Option<T> {
+ type Target = &'a Option<T>;
+ fn into_arg_holder(self) -> Self::Target {
+ self
+ }
+}
+
+impl<T: AnyCompatible> ArgIntoRef for Option<T> {
+ type Target = Self;
+ fn to_ref(&self) -> &Self::Target {
+ self
+ }
+}
+
+impl<T: AnyCompatible> ArgIntoRef for &Option<T> {
+ type Target = Option<T>;
+ fn to_ref(&self) -> &Self::Target {
+ self
+ }
+}
+
impl<T: ContainerElement + Clone> IntoArgHolder for crate::Array<T> {
type Target = crate::Array<T>;
fn into_arg_holder(self) -> Self::Target {
@@ -179,6 +274,26 @@ impl<T: ContainerElement + Clone> ArgIntoRef for
&crate::Array<T> {
}
}
+impl<T> IntoArgHolder for RValueRef<T>
+where
+ T: ObjectRefCore + AnyCompatible,
+{
+ type Target = Self;
+ fn into_arg_holder(self) -> Self::Target {
+ self
+ }
+}
+
+impl<T> ArgIntoRef for RValueRef<T>
+where
+ T: ObjectRefCore + AnyCompatible,
+{
+ type Target = Self;
+ fn to_ref(&self) -> &Self::Target {
+ self
+ }
+}
+
impl<K: ContainerElement, V: ContainerElement> IntoArgHolder for crate::Map<K,
V> {
type Target = crate::Map<K, V>;
fn into_arg_holder(self) -> Self::Target {
@@ -220,14 +335,14 @@ macro_rules! impl_tuple_as_packed_args {
where
$(
$T: ArgIntoRef,
- $T::Target: AnyCompatible,
+ $T::Target: PackedArg,
)*
{
const LEN: usize = $len;
fn fill_any_view<'a>(&'a self, _any_view: &mut [AnyView<'a>]) {
$(
- _any_view[$idx] = AnyView::from(self.$idx.to_ref());
+ _any_view[$idx] = self.$idx.to_ref().as_packed_arg();
)*
}
}
diff --git a/rust/tvm-ffi/src/lib.rs b/rust/tvm-ffi/src/lib.rs
index 688d34bf..8be52bbe 100644
--- a/rust/tvm-ffi/src/lib.rs
+++ b/rust/tvm-ffi/src/lib.rs
@@ -30,6 +30,8 @@ pub mod macros;
pub mod match_any_internal;
pub mod object;
pub mod optional;
+pub mod reflection;
+pub mod rvalue_ref;
pub mod string;
pub mod type_traits;
pub use tvm_ffi_sys;
@@ -58,8 +60,12 @@ pub use crate::extra::structural_visit::{
};
pub use crate::function::Function;
pub use crate::object::ObjectRefCast;
-pub use crate::object::{Object, ObjectArc, ObjectCore,
ObjectCoreWithExtraItems, ObjectRefCore};
+pub use crate::object::{
+ Object, ObjectArc, ObjectCore, ObjectCoreWithExtraItems, ObjectIdentity,
ObjectRefCore,
+};
pub use crate::optional::{Optional, OptionalCompatible};
+pub use crate::reflection::{get_type_attr, FieldGetter, TypeAttrColumn};
+pub use crate::rvalue_ref::RValueRef;
pub use crate::string::{Bytes, String};
pub use crate::type_traits::AnyCompatible;
pub use tvm_ffi_macros::{dispatch, match_any};
diff --git a/rust/tvm-ffi/src/macros.rs b/rust/tvm-ffi/src/macros.rs
index b9665623..27cf5250 100644
--- a/rust/tvm-ffi/src/macros.rs
+++ b/rust/tvm-ffi/src/macros.rs
@@ -59,6 +59,51 @@ macro_rules! cached_global_func {
}};
}
+/// Implement zero-copy conversions from a derived object reference to one of
+/// its base object-reference types.
+///
+/// # Safety
+///
+/// Generated bindings must ensure that `target` is a registered base of
+/// `source` and that every `source` satisfies the target reference invariants.
+/// An incorrect declaration makes the generated safe conversion unsound.
+#[macro_export]
+macro_rules! impl_object_upcast {
+ ($($source:ty => $target:ty),+ $(,)?) => {
+ $(
+ impl ::std::convert::From<$source> for $target {
+ #[inline]
+ fn from(value: $source) -> Self {
+ let data = <$source as
$crate::object::ObjectRefCore>::into_data(value);
+ // SAFETY: The macro declaration promises that `target` is
+ // a registered base of `source`. Both references retain
+ // the same allocation and only change its static view.
+ let data = unsafe {
+ $crate::object::ObjectArc::from_raw(
+ $crate::object::ObjectArc::into_raw(data).cast::
+ <<$target as
$crate::object::ObjectRefCore>::ContainerType>(),
+ )
+ };
+ // SAFETY: The macro declaration promises both the
+ // container inheritance relation and every additional
+ // invariant imposed by the target reference view.
+ unsafe {
+ <$target as
$crate::object::ObjectRefCore>::from_data(data)
+ }
+ }
+ }
+
+ impl ::std::convert::From<&$source> for $target {
+ #[inline]
+ fn from(value: &$source) -> Self {
+ value.clone().into()
+ }
+ }
+
+ )+
+ };
+}
+
/// Check the return code of the safe call
///
/// # Arguments
diff --git a/rust/tvm-ffi/src/object.rs b/rust/tvm-ffi/src/object.rs
index 7adc439b..ddfbcf55 100644
--- a/rust/tvm-ffi/src/object.rs
+++ b/rust/tvm-ffi/src/object.rs
@@ -125,7 +125,77 @@ pub unsafe trait ObjectRefCore: Sized + Clone {
type ContainerType: ObjectCore;
fn data(this: &Self) -> &ObjectArc<Self::ContainerType>;
fn into_data(this: Self) -> ObjectArc<Self::ContainerType>;
- fn from_data(data: ObjectArc<Self::ContainerType>) -> Self;
+
+ /// Construct a reference view from an owning container handle.
+ ///
+ /// # Safety
+ ///
+ /// In addition to containing a valid `ContainerType` allocation, `data`
+ /// must satisfy every semantic invariant imposed by `Self`. This matters
+ /// for zero-state views that share a container type but accept only a
+ /// subset of its values, such as a typed expression view.
+ unsafe fn from_data(data: ObjectArc<Self::ContainerType>) -> Self;
+
+ /// Return whether two object references point to the same allocation.
+ #[inline]
+ fn same_as<Other: ObjectRefCore>(&self, other: &Other) -> bool {
+ unsafe {
+ ObjectArc::as_raw(Self::data(self)).cast::<()>()
+ == ObjectArc::as_raw(Other::data(other)).cast::<()>()
+ }
+ }
+}
+
+/// An owning, hashable identity key for an FFI object.
+///
+/// The retained strong reference prevents the allocation address from being
+/// reused while the key is alive. This makes it suitable for identity-based
+/// maps without exposing raw object pointers to downstream code.
+#[derive(Clone)]
+pub struct ObjectIdentity {
+ data: ObjectArc<Object>,
+}
+
+impl ObjectIdentity {
+ /// Retain the allocation referenced by `value` as an identity key.
+ pub fn of<T: ObjectRefCore>(value: &T) -> Self {
+ unsafe {
+ let ptr = ObjectArc::as_raw(T::data(value)) as *mut TVMFFIObject;
+ unsafe_::inc_ref(ptr);
+ Self {
+ data: ObjectArc::from_raw(ptr.cast::<Object>()),
+ }
+ }
+ }
+
+ #[inline]
+ fn as_ptr(&self) -> *const TVMFFIObject {
+ unsafe { ObjectArc::as_raw(&self.data).cast::<TVMFFIObject>() }
+ }
+}
+
+impl PartialEq for ObjectIdentity {
+ #[inline]
+ fn eq(&self, other: &Self) -> bool {
+ self.as_ptr() == other.as_ptr()
+ }
+}
+
+impl Eq for ObjectIdentity {}
+
+impl std::hash::Hash for ObjectIdentity {
+ #[inline]
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ self.as_ptr().hash(state);
+ }
+}
+
+impl std::fmt::Debug for ObjectIdentity {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_tuple("ObjectIdentity")
+ .field(&self.as_ptr())
+ .finish()
+ }
}
/// Check whether a runtime type index refers to `Target` or one of its
diff --git a/rust/tvm-ffi/src/reflection.rs b/rust/tvm-ffi/src/reflection.rs
new file mode 100644
index 00000000..d62425ac
--- /dev/null
+++ b/rust/tvm-ffi/src/reflection.rs
@@ -0,0 +1,234 @@
+/*
+ * 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.
+ */
+
+//! Safe access to object reflection metadata.
+
+use std::ffi::c_void;
+use std::ptr::NonNull;
+
+use crate::tvm_ffi_sys::{
+ TVMFFIAny, TVMFFIByteArray, TVMFFIFieldGetter, TVMFFIFieldInfo,
TVMFFIGetTypeAttrColumn,
+ TVMFFIGetTypeInfo, TVMFFIObject, TVMFFITypeAttrColumn, TVMFFITypeIndex,
+};
+use crate::{Any, AnyView, Error, ObjectCore, Result, TYPE_ERROR};
+
+/// A registry-owned type-attribute column indexed by runtime type.
+///
+/// [`TypeAttrColumn::get`] returns owning copies. Registration must not race
+/// with reads.
+#[derive(Clone, Copy)]
+pub struct TypeAttrColumn(NonNull<TVMFFITypeAttrColumn>);
+
+// Type-attribute columns and their cells are registry-owned process-lifetime
+// data. Once registration is complete, reading a cell does not mutate the
+// registry and is safe from any thread.
+unsafe impl Send for TypeAttrColumn {}
+unsafe impl Sync for TypeAttrColumn {}
+
+impl TypeAttrColumn {
+ /// Look up a registered type-attribute column by name.
+ pub fn new(name: &str) -> Option<Self> {
+ unsafe {
+ let name = TVMFFIByteArray::from_str(name);
+ NonNull::new(TVMFFIGetTypeAttrColumn(&name).cast_mut()).map(Self)
+ }
+ }
+
+ /// Return an owning copy of this attribute for `type_index`.
+ pub fn get(self, type_index: i32) -> Option<Any> {
+ let raw = self.get_raw(type_index)?;
+ if raw.type_index == TVMFFITypeIndex::kTVMFFINone as i32 {
+ return None;
+ }
+ Some(Any::from(unsafe { AnyView::from_raw_ffi_any(raw) }))
+ }
+
+ pub(crate) unsafe fn from_non_null(pointer: NonNull<TVMFFITypeAttrColumn>)
-> Self {
+ Self(pointer)
+ }
+
+ pub(crate) fn as_ptr(self) -> *mut TVMFFITypeAttrColumn {
+ self.0.as_ptr()
+ }
+
+ /// Copy one borrowed cell without taking ownership.
+ pub(crate) fn get_raw(self, type_index: i32) -> Option<TVMFFIAny> {
+ unsafe {
+ let column = self.0.as_ref();
+ let index = type_index - column.begin_index;
+ if index < 0 || index >= column.size || column.data.is_null() {
+ None
+ } else {
+ Some(*column.data.offset(index as isize))
+ }
+ }
+ }
+}
+
+/// Look up one type attribute and copy it into an owning value.
+pub fn get_type_attr(type_index: i32, attr_name: &str) -> Option<Any> {
+ TypeAttrColumn::new(attr_name)?.get(type_index)
+}
+
+/// Resolves a reflected field once, then uses its registered C ABI getter.
+#[derive(Clone, Copy)]
+pub struct FieldGetter {
+ owner_type_index: i32,
+ owner_type_depth: i32,
+ field_offset: i64,
+ getter: TVMFFIFieldGetter,
+}
+
+impl FieldGetter {
+ /// Resolve a reflected field declared by `type_index` or one of its bases.
+ pub fn new(type_index: i32, field_name: &str) -> Result<Self> {
+ let type_info = unsafe { TVMFFIGetTypeInfo(type_index) };
+ if type_info.is_null() {
+ return Err(Error::new(
+ TYPE_ERROR,
+ &format!("Cannot find type info for type_index={type_index}"),
+ "",
+ ));
+ }
+
+ let field = unsafe { find_field(type_info, field_name) }.ok_or_else(||
{
+ let type_key = unsafe { (*type_info).type_key.as_str() };
+ Error::new(
+ TYPE_ERROR,
+ &format!("Cannot find reflected field `{field_name}` in type
`{type_key}`"),
+ "",
+ )
+ })?;
+ let field = unsafe { field.as_ref() };
+ let getter = field.getter.ok_or_else(|| {
+ Error::new(
+ TYPE_ERROR,
+ &format!("Reflected field `{}` has no getter",
field.name.as_str()),
+ "",
+ )
+ })?;
+ Ok(Self {
+ owner_type_index: type_index,
+ owner_type_depth: unsafe { (*type_info).type_depth },
+ field_offset: field.offset,
+ getter,
+ })
+ }
+
+ /// Read the field as an owning [`Any`].
+ ///
+ /// `object` may have the declared owner type or any registered subtype.
+ pub fn get_any<N: ObjectCore>(&self, object: &N) -> Result<Any> {
+ let object_pointer = std::ptr::from_ref(object);
+ let header = object_pointer.cast::<TVMFFIObject>();
+ let dynamic_type_index = unsafe { (*header).type_index };
+ if !unsafe {
+ is_type_or_subtype(
+ dynamic_type_index,
+ self.owner_type_index,
+ self.owner_type_depth,
+ )
+ } {
+ return Err(Error::new(
+ TYPE_ERROR,
+ &format!(
+ "Cannot read a field of type_index={} from object
type_index={dynamic_type_index}",
+ self.owner_type_index
+ ),
+ "",
+ ));
+ }
+
+ let field_address = unsafe {
+ object_pointer
+ .cast::<u8>()
+ .offset(self.field_offset as isize)
+ .cast_mut()
+ .cast::<c_void>()
+ };
+ let mut result = Any::new();
+ if unsafe { (self.getter)(field_address, Any::as_data_ptr(&mut
result)) } != 0 {
+ return Err(Error::from_raised());
+ }
+ Ok(result)
+ }
+
+ /// Read and convert the field to `T`.
+ pub fn get<N, T>(&self, object: &N) -> Result<T>
+ where
+ N: ObjectCore,
+ T: TryFrom<Any, Error = Error>,
+ {
+ T::try_from(self.get_any(object)?)
+ }
+}
+
+unsafe fn find_field(
+ type_info: *const crate::tvm_ffi_sys::TVMFFITypeInfo,
+ field_name: &str,
+) -> Option<NonNull<TVMFFIFieldInfo>> {
+ // Prefer the most-derived declaration, then search nearest ancestors.
+ if let Some(field) = find_field_at_level(type_info, field_name) {
+ return Some(field);
+ }
+ for depth in (0..(*type_info).type_depth).rev() {
+ let ancestor = *(*type_info).type_acenstors.add(depth as usize);
+ if let Some(field) = find_field_at_level(ancestor, field_name) {
+ return Some(field);
+ }
+ }
+ None
+}
+
+unsafe fn find_field_at_level(
+ type_info: *const crate::tvm_ffi_sys::TVMFFITypeInfo,
+ field_name: &str,
+) -> Option<NonNull<TVMFFIFieldInfo>> {
+ if type_info.is_null() || (*type_info).fields.is_null() {
+ return None;
+ }
+ for index in 0..(*type_info).num_fields as usize {
+ let field = (*type_info).fields.add(index);
+ if (*field).name.as_str() == field_name {
+ return NonNull::new(field.cast_mut());
+ }
+ }
+ None
+}
+
+unsafe fn is_type_or_subtype(
+ dynamic_type_index: i32,
+ target_type_index: i32,
+ target_type_depth: i32,
+) -> bool {
+ if dynamic_type_index == target_type_index {
+ return true;
+ }
+ let dynamic_info = TVMFFIGetTypeInfo(dynamic_type_index);
+ if dynamic_info.is_null()
+ || (*dynamic_info).type_depth <= target_type_depth
+ || (*dynamic_info).type_acenstors.is_null()
+ {
+ return false;
+ }
+ let ancestor = *(*dynamic_info)
+ .type_acenstors
+ .add(target_type_depth as usize);
+ !ancestor.is_null() && (*ancestor).type_index == target_type_index
+}
diff --git a/rust/tvm-ffi/src/rvalue_ref.rs b/rust/tvm-ffi/src/rvalue_ref.rs
new file mode 100644
index 00000000..f9d691b2
--- /dev/null
+++ b/rust/tvm-ffi/src/rvalue_ref.rs
@@ -0,0 +1,183 @@
+/*
+ * 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.
+ */
+
+//! Move-aware object arguments compatible with C++ `ffi::RValueRef<T>`.
+
+use std::cell::UnsafeCell;
+use std::marker::PhantomData;
+
+use tvm_ffi_sys::{TVMFFIAny, TVMFFIObject, TVMFFITypeIndex as TypeIndex};
+
+use crate::any::ArgTryFromAnyView;
+use crate::{AnyCompatible, AnyView, Error, ObjectRefCore, Result};
+
+/// A move-aware object argument compatible with C++ `ffi::RValueRef<T>`.
+///
+/// The callee may take the stored strong reference without incrementing its
+/// count; otherwise this wrapper retains and releases it.
+pub struct RValueRef<T>
+where
+ T: ObjectRefCore + AnyCompatible,
+{
+ slot: UnsafeCell<*mut TVMFFIObject>,
+ _marker: PhantomData<T>,
+}
+
+impl<T> RValueRef<T>
+where
+ T: ObjectRefCore + AnyCompatible,
+{
+ /// Transfer an owned object reference into an rvalue argument slot.
+ pub fn new(value: T) -> Self {
+ let mut raw = TVMFFIAny::new();
+ unsafe { T::move_to_any(value, &mut raw) };
+ debug_assert!(raw.type_index >= TypeIndex::kTVMFFIStaticObjectBegin as
i32);
+ Self {
+ slot: UnsafeCell::new(unsafe { raw.data_union.v_obj }),
+ _marker: PhantomData,
+ }
+ }
+
+ /// Take the owned object without copying or incrementing its reference
count.
+ pub fn into_inner(mut self) -> T {
+ let object = *self.slot.get_mut();
+ assert!(!object.is_null(), "RValueRef has already been moved");
+ *self.slot.get_mut() = std::ptr::null_mut();
+ unsafe {
+ let mut raw = object_any(object);
+ T::move_from_any_after_check(&mut raw)
+ }
+ }
+
+ unsafe fn from_view(value: &AnyView<'_>, arg_index: Option<usize>) ->
Result<Self> {
+ let raw = value.as_raw_ffi_any();
+ let converted = if raw.type_index == TypeIndex::kTVMFFIObjectRValueRef
as i32 {
+ let slot = raw.data_union.v_ptr.cast::<*mut TVMFFIObject>();
+ if slot.is_null() || (*slot).is_null() {
+ return Err(conversion_error::<T>(raw, arg_index, true));
+ }
+ let object = *slot;
+ let object_view = object_any(object);
+ if T::check_any_strict(&object_view) {
+ *slot = std::ptr::null_mut();
+ return Ok(Self {
+ slot: UnsafeCell::new(object),
+ _marker: PhantomData,
+ });
+ }
+ T::try_cast_from_any_view(&object_view)
+ } else if T::check_any_strict(raw) {
+ Ok(T::copy_from_any_view_after_check(raw))
+ } else {
+ T::try_cast_from_any_view(raw)
+ };
+
+ converted
+ .map(Self::new)
+ .map_err(|()| conversion_error::<T>(raw, arg_index, false))
+ }
+}
+
+impl<T> From<T> for RValueRef<T>
+where
+ T: ObjectRefCore + AnyCompatible,
+{
+ fn from(value: T) -> Self {
+ Self::new(value)
+ }
+}
+
+impl<'a, T> From<&'a RValueRef<T>> for AnyView<'a>
+where
+ T: ObjectRefCore + AnyCompatible,
+{
+ fn from(value: &'a RValueRef<T>) -> Self {
+ let mut raw = TVMFFIAny::new();
+ raw.type_index = TypeIndex::kTVMFFIObjectRValueRef as i32;
+ raw.data_union.v_ptr = value.slot.get().cast();
+ unsafe { AnyView::from_raw_ffi_any(raw) }
+ }
+}
+
+impl<T> TryFrom<AnyView<'_>> for RValueRef<T>
+where
+ T: ObjectRefCore + AnyCompatible,
+{
+ type Error = Error;
+
+ fn try_from(value: AnyView<'_>) -> Result<Self> {
+ unsafe { Self::from_view(&value, None) }
+ }
+}
+
+impl<T> ArgTryFromAnyView for RValueRef<T>
+where
+ T: ObjectRefCore + AnyCompatible,
+{
+ fn try_from_any_view(value: &AnyView<'_>, arg_index: usize) ->
Result<Self> {
+ unsafe { Self::from_view(value, Some(arg_index)) }
+ }
+}
+
+impl<T> Drop for RValueRef<T>
+where
+ T: ObjectRefCore + AnyCompatible,
+{
+ fn drop(&mut self) {
+ let object = *self.slot.get_mut();
+ if !object.is_null() {
+ unsafe { crate::object::unsafe_::dec_ref(object) };
+ }
+ }
+}
+
+unsafe fn object_any(object: *mut TVMFFIObject) -> TVMFFIAny {
+ let mut raw = TVMFFIAny::new();
+ raw.type_index = (*object).type_index;
+ raw.data_union.v_obj = object;
+ raw
+}
+
+unsafe fn conversion_error<T>(
+ raw: &TVMFFIAny,
+ arg_index: Option<usize>,
+ already_moved: bool,
+) -> Error
+where
+ T: ObjectRefCore + AnyCompatible,
+{
+ let source = if already_moved {
+ "an already-moved RValueRef".to_string()
+ } else if raw.type_index == TypeIndex::kTVMFFIObjectRValueRef as i32 {
+ "RValueRef with an incompatible object type".to_string()
+ } else {
+ T::get_mismatch_type_info(raw)
+ };
+ let prefix = arg_index
+ .map(|index| format!("Argument #{index}: "))
+ .unwrap_or_default();
+ Error::new(
+ crate::error::TYPE_ERROR,
+ &format!(
+ "{prefix}Cannot convert from `{source}` to `RValueRef<{}>`",
+ T::type_str()
+ ),
+ "",
+ )
+}
diff --git a/rust/tvm-ffi/tests/test_cast.rs b/rust/tvm-ffi/tests/test_cast.rs
index fb4d8c94..29689808 100644
--- a/rust/tvm-ffi/tests/test_cast.rs
+++ b/rust/tvm-ffi/tests/test_cast.rs
@@ -65,6 +65,8 @@ struct TestDerived {
data: ObjectArc<TestDerivedObj>,
}
+tvm_ffi::impl_object_upcast!(TestDerived => TestBase);
+
// unwrap_err() requires the Ok type to implement Debug, which ObjectRef types
do not
fn expect_err<T>(res: Result<T>) -> Error {
match res {
@@ -140,6 +142,25 @@ fn test_upcast_downcast_roundtrip() {
assert_eq!(delete_counter.load(Ordering::Relaxed), 1);
}
+#[test]
+fn test_generated_borrow_and_upcast_conversions() {
+ let delete_counter = Arc::new(AtomicU32::new(0));
+ let derived = new_derived(7, 8, delete_counter.clone());
+
+ let borrowed_clone = TestDerived::from(&derived);
+ assert!(borrowed_clone.same_as(&derived));
+
+ let base = TestBase::from(&derived);
+ assert!(base.same_as(&derived));
+ assert_eq!(base.data.value, 7);
+ assert_eq!(ObjectArc::strong_count(&derived.data), 3);
+
+ drop(base);
+ drop(borrowed_clone);
+ drop(derived);
+ assert_eq!(delete_counter.load(Ordering::Relaxed), 1);
+}
+
#[test]
fn test_cast_checks_parameterized_container_type() {
assert!(Array::new(vec![1_i64, 2_i64])
diff --git a/rust/tvm-ffi/tests/test_function.rs
b/rust/tvm-ffi/tests/test_function.rs
index 9ed53172..07ed6779 100644
--- a/rust/tvm-ffi/tests/test_function.rs
+++ b/rust/tvm-ffi/tests/test_function.rs
@@ -91,6 +91,56 @@ fn test_function_call_tuple() {
assert_eq!(result.unwrap(), 1 + offset);
}
+#[test]
+fn test_function_call_tuple_supports_all_value_holders() {
+ let echo = Function::get_global("testing.echo").unwrap();
+
+ let any = Any::from(3i64);
+ assert_eq!(i64::try_from(echo.call_tuple((&any,)).unwrap()).unwrap(), 3);
+
+ assert_eq!(
+ echo.call_tuple(((),)).unwrap().type_index(),
+ TypeIndex::kTVMFFINone as i32
+ );
+
+ let optional = Some(4i64);
+ assert_eq!(
+
Option::<i64>::try_from(echo.call_tuple((&optional,)).unwrap()).unwrap(),
+ optional
+ );
+
+ let dtype = DLDataType::try_from_str("int32").unwrap();
+ assert_eq!(
+ DLDataType::try_from(echo.call_tuple((dtype,)).unwrap()).unwrap(),
+ dtype
+ );
+
+ let device = DLDevice::new(DLDeviceType::kDLCPU, 1);
+ assert_eq!(
+ DLDevice::try_from(echo.call_tuple((device,)).unwrap()).unwrap(),
+ device
+ );
+}
+
+#[test]
+fn test_function_rvalue_ref_arguments() {
+ let strong_count = Function::from_typed(|value: RValueRef<Array<i64>>| ->
Result<i64> {
+ let value = value.into_inner();
+ Ok(AnyView::from(&value).debug_strong_count().unwrap() as i64)
+ });
+
+ let moved = Array::new(vec![1i64, 2]);
+ assert_eq!(AnyView::from(&moved).debug_strong_count(), Some(1));
+ let count =
i64::try_from(strong_count.call_tuple((RValueRef::new(moved),)).unwrap()).unwrap();
+ assert_eq!(count, 1);
+
+ let borrowed = Array::new(vec![3i64, 4]);
+ assert_eq!(AnyView::from(&borrowed).debug_strong_count(), Some(1));
+ let count =
i64::try_from(strong_count.call_tuple((&borrowed,)).unwrap()).unwrap();
+ assert_eq!(count, 2);
+ assert_eq!(AnyView::from(&borrowed).debug_strong_count(), Some(1));
+}
+
#[test]
fn test_function_into_typed_fn() {
let offset = 2;
diff --git a/rust/tvm-ffi/tests/test_object.rs
b/rust/tvm-ffi/tests/test_object.rs
index e4accba2..ebab18e0 100644
--- a/rust/tvm-ffi/tests/test_object.rs
+++ b/rust/tvm-ffi/tests/test_object.rs
@@ -18,6 +18,7 @@
*/
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
+use std::{collections::HashMap, hash::Hash};
use tvm_ffi::*;
// must have repr(C) for the object header stays in the same position
@@ -146,3 +147,27 @@ fn test_object_arc_option_size() {
std::mem::size_of::<ObjectArc<TestIntObj>>()
);
}
+
+#[test]
+fn test_object_reference_identity() {
+ fn assert_hash<T: Hash>(_value: &T) {}
+
+ let first = Array::new(vec![1i64]);
+ let alias = first.clone();
+ let second = Array::new(vec![1i64]);
+
+ assert!(first.same_as(&alias));
+ assert!(!first.same_as(&second));
+
+ let first_id = ObjectIdentity::of(&first);
+ let alias_id = ObjectIdentity::of(&alias);
+ let second_id = ObjectIdentity::of(&second);
+ assert_hash(&first_id);
+ assert_eq!(first_id, alias_id);
+ assert_ne!(first_id, second_id);
+
+ let mut identities = HashMap::new();
+ identities.insert(first_id, "first");
+ assert_eq!(identities.get(&alias_id), Some(&"first"));
+ assert_eq!(identities.get(&second_id), None);
+}
diff --git a/rust/tvm-ffi/tests/test_structural_visit.rs
b/rust/tvm-ffi/tests/test_structural_visit.rs
index 780cf32e..9a4ada40 100644
--- a/rust/tvm-ffi/tests/test_structural_visit.rs
+++ b/rust/tvm-ffi/tests/test_structural_visit.rs
@@ -27,10 +27,10 @@ use tvm_ffi::tvm_ffi_sys::{
TVMFFISEqHashKind, TVMFFITypeMetadata, TVMFFITypeRegisterAttr,
};
use tvm_ffi::{
- dispatch, structural_visit, structural_walk, Any, AnyView, Array,
DLDataType, DLDataTypeCode,
- DefRegionKind, Error, Function, Map, Object, ObjectArc, ObjectCore,
ObjectRefCast, Result,
- String as FfiString, StructuralVisitor, TypeIndex, VisitCallbacks,
VisitContext,
- VisitInterrupt, VisitValue, WalkOrder, WalkResult, RUNTIME_ERROR,
+ dispatch, get_type_attr, structural_visit, structural_walk, Any, AnyView,
Array, DLDataType,
+ DLDataTypeCode, DefRegionKind, Error, FieldGetter, Function, Map, Object,
ObjectArc,
+ ObjectCore, ObjectRefCast, Result, String as FfiString, StructuralVisitor,
TypeIndex,
+ VisitCallbacks, VisitContext, VisitInterrupt, VisitValue, WalkOrder,
WalkResult, RUNTIME_ERROR,
};
unsafe extern "C" {
@@ -341,6 +341,25 @@ fn runtime_error(message: &str) -> Error {
Error::new(RUNTIME_ERROR, message, "")
}
+#[test]
+fn public_reflection_access_uses_registered_field_and_type_attr() {
+ let root = rust_visit_hook(FfiString::from("owned field"), 99i64);
+ let type_index = RustVisitHookObj::type_index();
+
+ let getter = FieldGetter::new(type_index, "selected").unwrap();
+ let selected = getter.get::<_, FfiString>(&*root.data).unwrap();
+ drop(root);
+ assert_eq!(selected.as_str(), "owned field");
+
+ let wrong_type = rust_visit_failing_getter(0i64);
+ assert!(getter.get_any(&*wrong_type.data).is_err());
+ assert!(FieldGetter::new(type_index, "missing").is_err());
+
+ assert!(Function::try_from(get_type_attr(type_index,
"__s_visit__").unwrap()).is_ok());
+ assert!(Function::from_type_attr(type_index, "__s_visit__").is_ok());
+ assert!(get_type_attr(type_index, "missing").is_none());
+}
+
#[test]
fn plain_walk_uses_registered_array_hook() {
let root = Array::new(vec![1i64, 2, 3]);