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]);

Reply via email to