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 5fdba0f4 [FFI][Rust] Thread safety only where it holds; TryFrom 
between object references (#938)
5fdba0f4 is described below

commit 5fdba0f4c562465435ab4a860da602ab80f6820d
Author: Tianqi Chen <[email protected]>
AuthorDate: Thu Nov 5 22:48:19 2026 -0500

    [FFI][Rust] Thread safety only where it holds; TryFrom between object 
references (#938)
    
    Finishes the Rust API's object story for stabilization: thread safety is
    claimed only where it holds, and downcasting between references uses
    `TryFrom`.
    
    ## Thread safety
    
    Before this change, the root `ObjectRef` and any derived reference whose
    fields happened to be thread-safe were `Send + Sync` automatically, even
    when the object was shared mutable state: a `List` behind an
    `ObjectRef`, or a C++ class whose methods mutate it.
    
    - **Object handles are not `Send`/`Sync` by default.** `ObjectRef` and
    derived references are single-threaded unless their class opts in. `Any`
    already was.
    - **`ImmutableObject`:** an `unsafe` marker for object types whose
    shared instances are never mutated (after zerocopy's `Immutable` and
    pyo3's `frozen`). A handle is `Send + Sync` when its object type
    implements it and its fields are thread-safe, so an `Any` field still
    keeps it single-threaded.
    - **`_type_mutable` decides generated classes.** C++ records
    `Class::_type_mutable` as the type attribute `__ffi_type_mutable__`,
    next to `__ffi_type_final__`. The Rust stub generator marks a class
    `ImmutableObject` only when it is not mutable (handles give const
    access, and mutation is copy-on-write); mutable or unknown classes get
    nothing.
    - **tvm-ffi's immutable built-ins are `Send + Sync`:** `String`,
    `Bytes`, `BigInt`, `Shape`, `Error` (so a `Result` can cross threads),
    and `Array<T>`/`Map<K, V>` when their elements are. `List` stays
    single-threaded.
    - **`Function` stays `Send + Sync`,** which global caches need. Its Rust
    callbacks must be `Send + Sync` (`from_packed`, `from_typed`,
    `register_global_typed`, `ObjectDef::def`/`def_static`), so a closure
    capturing an `Rc` or `RefCell` is rejected. A callee's own thread
    safety, for C++ functions and objects, is that of its implementation.
    
    ## `TryFrom` between references
    
    - `unsafe_impl_object_upcast!(Child => Parent)` also generates
    `TryFrom<Parent> for Child` and `TryFrom<&Parent> for Child`, and the
    derive's upcast to `ObjectRef` gives every reference
    `TryFrom<ObjectRef>`. Containers get the same by hand. The cast is
    strict, since references never convert, and fails with a `TypeError`
    naming both types, so the common downcast is `let op: Op =
    expr.try_into()?`. The `&Parent` form checks in place and clones only on
    success. `downcast` remains for trying another type, as it hands the
    value back, and `downcast_ref` for borrowing.
    - `object::fmt_object_ref` is documented, for hand-written reference
    types implementing `Debug`.
    - `AnyCompatible` is a supertrait of `ObjectRefCore`, so generic code
    needs one bound.
---
 docs/guides/rust_lang_guide.rst               | 13 +++--
 include/tvm/ffi/reflection/accessor.h         |  8 +++
 include/tvm/ffi/reflection/registry.h         |  3 +-
 python/tvm_ffi/stub/rust_generator/codegen.py | 18 ++++++
 python/tvm_ffi/stub/utils.py                  |  3 +
 rust/tvm-ffi/src/big_int/mod.rs               |  4 ++
 rust/tvm-ffi/src/collections/array.rs         | 21 +++++++
 rust/tvm-ffi/src/collections/list.rs          | 19 +++++-
 rust/tvm-ffi/src/collections/map.rs           | 29 ++++++++-
 rust/tvm-ffi/src/collections/shape.rs         |  4 ++
 rust/tvm-ffi/src/error.rs                     | 12 +++-
 rust/tvm-ffi/src/extra/structural_mutate.rs   |  4 +-
 rust/tvm-ffi/src/extra/unchanged.rs           |  4 +-
 rust/tvm-ffi/src/function.rs                  | 29 +++++++--
 rust/tvm-ffi/src/function_internal.rs         |  6 +-
 rust/tvm-ffi/src/lib.rs                       |  4 +-
 rust/tvm-ffi/src/macros.rs                    | 29 +++++++++
 rust/tvm-ffi/src/match_any_internal.rs        |  2 +-
 rust/tvm-ffi/src/object.rs                    | 84 +++++++++++++++++++++++----
 rust/tvm-ffi/src/reflection/registry.rs       |  6 +-
 rust/tvm-ffi/src/rvalue_ref.rs                | 20 +++----
 rust/tvm-ffi/src/string.rs                    |  8 +++
 rust/tvm-ffi/src/type_traits.rs               |  4 +-
 rust/tvm-ffi/tests/test_cast.rs               | 18 ++++--
 rust/tvm-ffi/tests/test_function_metadata.rs  |  2 +-
 rust/tvm-ffi/tests/test_list.rs               |  2 +-
 rust/tvm-ffi/tests/test_structural_mutate.rs  |  7 ++-
 tests/python/test_stubgen_rust.py             | 10 ++++
 28 files changed, 313 insertions(+), 60 deletions(-)

diff --git a/docs/guides/rust_lang_guide.rst b/docs/guides/rust_lang_guide.rst
index a04e2154..7a810785 100644
--- a/docs/guides/rust_lang_guide.rst
+++ b/docs/guides/rust_lang_guide.rst
@@ -132,11 +132,14 @@ element type to ``T``. Named reference types implement 
|rs-objectrefcore|_ for s
    assert_eq!(array.downcast_ref::<ArrayObj>().unwrap().size, 3);
    assert_eq!(array.get(1).unwrap(), 2); // A method of the named type.
 
-Managed references provide ``downcast_ref::<T>()`` with the same inheritance 
and
-borrowing rules as ``Any`` and ``AnyView``, and ``downcast::<T>()``, which 
takes the
-reference and hands it back unchanged as ``Err`` when the object is not a 
``T``, as
-std's ``Box<dyn Any>::downcast`` does. 
``.map_err(Error::downcast_failed::<T>)?``
-turns that into a ``TypeError``. Upcasts are ``From``/``Into``.
+Upcasts are ``From``/``Into``. Each upcast also gives the strict downcast back 
as
+``TryFrom``, so ``let array: Array<i64> = object.try_into()?;`` checks the 
object's type
+and returns a ``TypeError`` naming both types; between references it never 
converts.
+``downcast::<T>()`` takes the reference and hands it back unchanged as 
``Err``, so another
+type can be tried, as std's ``Box<dyn Any>::downcast`` does, and 
``downcast_ref::<T>()``
+borrows it in place, with the same rules as ``Any`` and ``AnyView``. 
``try_from`` an
+``Any`` may convert by the FFI's conversion rules, such as an integer into a 
float, while
+``try_from`` a reference only checks.
 
 Loading a Module
 ~~~~~~~~~~~~~~~~
diff --git a/include/tvm/ffi/reflection/accessor.h 
b/include/tvm/ffi/reflection/accessor.h
index 5c251787..613a9bd6 100644
--- a/include/tvm/ffi/reflection/accessor.h
+++ b/include/tvm/ffi/reflection/accessor.h
@@ -437,6 +437,14 @@ inline constexpr 
reflection::AttrKey<TypedFunction<ObjectRef(const Object*)>> kS
  * never gain a subtype, so a binding generator may store it by value.
  */
 inline constexpr reflection::AttrKey<bool> kTypeFinal{"__ffi_type_final__"};
+/*!
+ * \brief Whether the class exposes mutable access (``Class::_type_mutable``).
+ *
+ * Registered as a ``bool`` by every ``ObjectDef<Class>``. A class that is not
+ * mutable is only changed through copy-on-write, so a binding generator may
+ * treat a shared instance as immutable.
+ */
+inline constexpr reflection::AttrKey<bool> 
kTypeMutable{"__ffi_type_mutable__"};
 /*!
  * \brief Custom Any-level hash for use as ``Map``/``Dict`` key.
  *
diff --git a/include/tvm/ffi/reflection/registry.h 
b/include/tvm/ffi/reflection/registry.h
index 4444b81d..d5fb6f4a 100644
--- a/include/tvm/ffi/reflection/registry.h
+++ b/include/tvm/ffi/reflection/registry.h
@@ -782,8 +782,9 @@ class ObjectDef : public ReflectionDefBase {
         }
       }
     }
-    // Step 4. Publish finality, which no other registered metadata exposes.
+    // Step 4. Publish finality and mutability, which no other registered 
metadata exposes.
     RegisterTypeAttrValue(type_index_, type_attr::kTypeFinal.name, 
Class::_type_final);
+    RegisterTypeAttrValue(type_index_, type_attr::kTypeMutable.name, 
Class::_type_mutable);
   }
 
   /*!
diff --git a/python/tvm_ffi/stub/rust_generator/codegen.py 
b/python/tvm_ffi/stub/rust_generator/codegen.py
index 655bd297..5ed67467 100644
--- a/python/tvm_ffi/stub/rust_generator/codegen.py
+++ b/python/tvm_ffi/stub/rust_generator/codegen.py
@@ -574,6 +574,21 @@ class _ObjectRenderer:
         )
         return sections
 
+    def _immutable_object_lines(self) -> list[str] | None:
+        """``ImmutableObject`` for a class that C++ declares not 
``_type_mutable``.
+
+        Such a class only hands out const access and changes through 
copy-on-write,
+        so its handles may cross threads when its fields can. A mutable class, 
or one
+        whose mutability is not registered, stays single-threaded.
+        """
+        if self.info.is_mutable is not False:
+            return None
+        return [
+            f"// SAFETY: `{self.type_key}` is not `_type_mutable`: a shared 
instance is never",
+            "// mutated, as C++ changes it only through copy-on-write.",
+            f"unsafe impl tvm_ffi::object::ImmutableObject for 
{self.obj_struct} {{}}",
+        ]
+
     def _shallow_copy_lines(self) -> list[str] | None:
         """``ShallowCopy``, which a subtype defined in Rust needs of its 
parent's object.
 
@@ -693,6 +708,9 @@ class _ObjectRenderer:
             if shallow_copy:
                 sections.append(shallow_copy)
                 sections.append(self._unsafe_init_lines())
+        thread_safe = self._immutable_object_lines()
+        if thread_safe:
+            sections.append(thread_safe)
         upcasts = self._upcast_lines()
         if upcasts:
             sections.append(upcasts)
diff --git a/python/tvm_ffi/stub/utils.py b/python/tvm_ffi/stub/utils.py
index 848a85ae..d7625666 100644
--- a/python/tvm_ffi/stub/utils.py
+++ b/python/tvm_ffi/stub/utils.py
@@ -212,6 +212,8 @@ class ObjectInfo:
     """Native ``sizeof`` in bytes, or ``None`` when the type has no metadata 
of its own."""
     is_final: bool | None = None
     """``__ffi_type_final__``: the class admits no subtype. ``None`` when 
unregistered."""
+    is_mutable: bool | None = None
+    """``__ffi_type_mutable__``: shared instances may be mutated. ``None`` 
when unregistered."""
 
     def has_overloaded_methods(self) -> bool:
         """Return whether reflection exposed multiple signatures for a 
method."""
@@ -276,4 +278,5 @@ class ObjectInfo:
             ancestors=[info.type_key for info in ancestor_infos],
             total_size=type_info.total_size if type_info._has_type_metadata 
else None,
             is_final=_lookup_type_attr(type_info.type_index, 
"__ffi_type_final__"),
+            is_mutable=_lookup_type_attr(type_info.type_index, 
"__ffi_type_mutable__"),
         )
diff --git a/rust/tvm-ffi/src/big_int/mod.rs b/rust/tvm-ffi/src/big_int/mod.rs
index dc965d95..64fb97f6 100644
--- a/rust/tvm-ffi/src/big_int/mod.rs
+++ b/rust/tvm-ffi/src/big_int/mod.rs
@@ -56,6 +56,10 @@ pub struct BigInt {
     data: TVMFFIAny,
 }
 
+// SAFETY: a `BigInt` is immutable: an inline `i64` or an immutable 
big-integer object.
+unsafe impl Send for BigInt {}
+unsafe impl Sync for BigInt {}
+
 // BigIntObj for heap-allocated integers: the word count, then the canonical
 // two's-complement words as trailing items. Mirrors C++ `detail::BigIntObj`.
 #[repr(C)]
diff --git a/rust/tvm-ffi/src/collections/array.rs 
b/rust/tvm-ffi/src/collections/array.rs
index 6155b24e..6a6f989e 100644
--- a/rust/tvm-ffi/src/collections/array.rs
+++ b/rust/tvm-ffi/src/collections/array.rs
@@ -79,6 +79,10 @@ pub struct Array<T> {
     _marker: PhantomData<T>,
 }
 
+// SAFETY: an `Array` is immutable, so sharing it is as safe as sharing its 
elements.
+unsafe impl<T: Send + Sync> Send for Array<T> {}
+unsafe impl<T: Send + Sync> Sync for Array<T> {}
+
 impl<T: ContainerElement + Clone> Debug for Array<T> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         let full_name = std::any::type_name::<T>();
@@ -107,6 +111,23 @@ impl<T: ContainerElement + Clone> From<Array<T>> for 
crate::object::ObjectRef {
     }
 }
 
+/// The strict downcast from the root [`ObjectRef`](crate::object::ObjectRef).
+impl<T: ContainerElement + Clone> TryFrom<crate::object::ObjectRef> for 
Array<T> {
+    type Error = crate::error::Error;
+    #[inline]
+    fn try_from(value: crate::object::ObjectRef) -> crate::error::Result<Self> 
{
+        crate::object::root_try_into(value)
+    }
+}
+
+impl<T: ContainerElement + Clone> TryFrom<&crate::object::ObjectRef> for 
Array<T> {
+    type Error = crate::error::Error;
+    #[inline]
+    fn try_from(value: &crate::object::ObjectRef) -> 
crate::error::Result<Self> {
+        crate::object::root_try_into_ref(value)
+    }
+}
+
 unsafe impl<T: ContainerElement + Clone> ObjectRefCore for Array<T> {
     type ContainerType = ArrayObj;
 
diff --git a/rust/tvm-ffi/src/collections/list.rs 
b/rust/tvm-ffi/src/collections/list.rs
index 00adb998..c0988c02 100644
--- a/rust/tvm-ffi/src/collections/list.rs
+++ b/rust/tvm-ffi/src/collections/list.rs
@@ -186,7 +186,24 @@ impl<T: ContainerElement> From<List<T>> for 
crate::object::ObjectRef {
     }
 }
 
-unsafe impl<T> ObjectRefCore for List<T> {
+/// The strict downcast from the root [`ObjectRef`](crate::object::ObjectRef).
+impl<T: ContainerElement> TryFrom<crate::object::ObjectRef> for List<T> {
+    type Error = crate::error::Error;
+    #[inline]
+    fn try_from(value: crate::object::ObjectRef) -> crate::error::Result<Self> 
{
+        crate::object::root_try_into(value)
+    }
+}
+
+impl<T: ContainerElement> TryFrom<&crate::object::ObjectRef> for List<T> {
+    type Error = crate::error::Error;
+    #[inline]
+    fn try_from(value: &crate::object::ObjectRef) -> 
crate::error::Result<Self> {
+        crate::object::root_try_into_ref(value)
+    }
+}
+
+unsafe impl<T: ContainerElement> ObjectRefCore for List<T> {
     type ContainerType = ListObj;
 
     fn data(this: &Self) -> &ObjectArc<ListObj> {
diff --git a/rust/tvm-ffi/src/collections/map.rs 
b/rust/tvm-ffi/src/collections/map.rs
index 54cf6a44..f354558e 100644
--- a/rust/tvm-ffi/src/collections/map.rs
+++ b/rust/tvm-ffi/src/collections/map.rs
@@ -120,6 +120,10 @@ pub struct Map<K, V> {
     _marker: PhantomData<(K, V)>,
 }
 
+// SAFETY: a `Map` is immutable, so sharing it is as safe as sharing its 
entries.
+unsafe impl<K: Send + Sync, V: Send + Sync> Send for Map<K, V> {}
+unsafe impl<K: Send + Sync, V: Send + Sync> Sync for Map<K, V> {}
+
 // Manual `Clone`: the data is the shared `ObjectArc`, so `Map<K, V>` is 
`Clone`
 // regardless of whether `K`/`V` are (they are only phantom markers).
 impl<K, V> Clone for Map<K, V> {
@@ -142,7 +146,30 @@ where
     }
 }
 
-unsafe impl<K, V> ObjectRefCore for Map<K, V> {
+/// The strict downcast from the root [`ObjectRef`](crate::object::ObjectRef).
+impl<K, V> TryFrom<crate::object::ObjectRef> for Map<K, V>
+where
+    Map<K, V>: ObjectRefCore,
+{
+    type Error = crate::error::Error;
+    #[inline]
+    fn try_from(value: crate::object::ObjectRef) -> crate::error::Result<Self> 
{
+        crate::object::root_try_into(value)
+    }
+}
+
+impl<K, V> TryFrom<&crate::object::ObjectRef> for Map<K, V>
+where
+    Map<K, V>: ObjectRefCore + crate::type_traits::DowncastRefFromObject,
+{
+    type Error = crate::error::Error;
+    #[inline]
+    fn try_from(value: &crate::object::ObjectRef) -> 
crate::error::Result<Self> {
+        crate::object::root_try_into_ref(value)
+    }
+}
+
+unsafe impl<K: ContainerElement, V: ContainerElement> ObjectRefCore for Map<K, 
V> {
     type ContainerType = MapObj;
 
     fn data(this: &Self) -> &ObjectArc<MapObj> {
diff --git a/rust/tvm-ffi/src/collections/shape.rs 
b/rust/tvm-ffi/src/collections/shape.rs
index 4a1aa44a..0600a4a4 100644
--- a/rust/tvm-ffi/src/collections/shape.rs
+++ b/rust/tvm-ffi/src/collections/shape.rs
@@ -54,6 +54,10 @@ pub struct Shape {
     data: ObjectArc<ShapeObj>,
 }
 
+// SAFETY: a `Shape` is immutable.
+unsafe impl Send for Shape {}
+unsafe impl Sync for Shape {}
+
 impl Shape {
     /// Create a new empty Shape
     pub fn new() -> Self {
diff --git a/rust/tvm-ffi/src/error.rs b/rust/tvm-ffi/src/error.rs
index 50aa98b5..be0b7675 100644
--- a/rust/tvm-ffi/src/error.rs
+++ b/rust/tvm-ffi/src/error.rs
@@ -107,6 +107,9 @@ pub struct ErrorObj {
 /// assert_eq!(error.kind(), VALUE_ERROR);
 /// assert_eq!(error.message(), "not a digit: 'x'");
 /// ```
+///
+/// An `Error` is `Send + Sync`, so a `Result` can cross threads; its attached
+/// context is treated as immutable diagnostic data.
 #[repr(C)]
 #[derive(Clone, ObjectRef)]
 #[custom_debug]
@@ -114,6 +117,11 @@ pub struct Error {
     data: ObjectArc<ErrorObj>,
 }
 
+// SAFETY: an `Error` is immutable once built; `extra_context` is diagnostic 
data.
+// Deliberately thread-safe so a `Result` can cross threads.
+unsafe impl Send for Error {}
+unsafe impl Sync for Error {}
+
 /// A `Result` whose error type defaults to [`Error`].
 pub type Result<T, E = Error> = std::result::Result<T, E>;
 
@@ -151,9 +159,7 @@ impl Error {
     /// let error = 
object.downcast::<Array<f64>>().map_err(Error::downcast_failed::<Array<f64>>);
     /// assert_eq!(error.unwrap_err().kind(), TYPE_ERROR);
     /// ```
-    pub fn downcast_failed<B: AnyCompatible>(
-        source: impl object::ObjectRefCore + AnyCompatible,
-    ) -> Self {
+    pub fn downcast_failed<B: AnyCompatible>(source: impl 
object::ObjectRefCore) -> Self {
         let mut cell = TVMFFIAny::new();
         // SAFETY: `source` stays alive while its borrowed cell is inspected.
         unsafe { AnyCompatible::copy_to_any_view(&source, &mut cell) };
diff --git a/rust/tvm-ffi/src/extra/structural_mutate.rs 
b/rust/tvm-ffi/src/extra/structural_mutate.rs
index 1d4af5b4..224dfa8c 100644
--- a/rust/tvm-ffi/src/extra/structural_mutate.rs
+++ b/rust/tvm-ffi/src/extra/structural_mutate.rs
@@ -621,7 +621,7 @@ impl<'a, T> MutationView<'a, T> {
 mod mutation_field {
     pub trait Sealed {}
     impl Sealed for crate::Any {}
-    impl<T: crate::object::ObjectRefCore + crate::type_traits::AnyCompatible> 
Sealed for T {}
+    impl<T: crate::object::ObjectRefCore> Sealed for T {}
 }
 
 /// Owned storage supported by [`MutationView::field`].
@@ -629,7 +629,7 @@ mod mutation_field {
 #[doc(hidden)]
 pub trait MutationField: crate::type_traits::ContainerElement + 
mutation_field::Sealed {}
 impl MutationField for Any {}
-impl<T: object::ObjectRefCore + crate::type_traits::AnyCompatible> 
MutationField for T {}
+impl<T: object::ObjectRefCore> MutationField for T {}
 
 impl<T: object::ObjectRefCore> std::ops::Deref for MutationView<'_, T> {
     type Target = T::ContainerType;
diff --git a/rust/tvm-ffi/src/extra/unchanged.rs 
b/rust/tvm-ffi/src/extra/unchanged.rs
index f012dae3..66678193 100644
--- a/rust/tvm-ffi/src/extra/unchanged.rs
+++ b/rust/tvm-ffi/src/extra/unchanged.rs
@@ -469,7 +469,7 @@ impl From<Unchanged> for UnchangedOr<Any> {
 }
 
 /// A managed reference replaces the original value.
-impl<T: crate::object::ObjectRefCore + AnyCompatible> From<T> for 
UnchangedOr<Any> {
+impl<T: crate::object::ObjectRefCore> From<T> for UnchangedOr<Any> {
     #[inline]
     fn from(value: T) -> Self {
         Self::changed(Any::from(value))
@@ -477,7 +477,7 @@ impl<T: crate::object::ObjectRefCore + AnyCompatible> 
From<T> for UnchangedOr<An
 }
 
 /// A typed result keeps its unchanged marker or replacement.
-impl<T: crate::object::ObjectRefCore + AnyCompatible> From<UnchangedOr<T>> for 
UnchangedOr<Any> {
+impl<T: crate::object::ObjectRefCore> From<UnchangedOr<T>> for 
UnchangedOr<Any> {
     #[inline]
     fn from(value: UnchangedOr<T>) -> Self {
         Self::from_any(Any::from(value))
diff --git a/rust/tvm-ffi/src/function.rs b/rust/tvm-ffi/src/function.rs
index 65a2e9eb..25513ab4 100644
--- a/rust/tvm-ffi/src/function.rs
+++ b/rust/tvm-ffi/src/function.rs
@@ -56,11 +56,26 @@ pub struct FunctionObj {
 /// assert_eq!(sum, 3);
 /// # Ok::<(), tvm_ffi::Error>(())
 /// ```
+///
+/// A `Function` may be called from any thread, so a Rust callback must be
+/// `Send + Sync`; a callback holding an `Rc` is rejected. The callee's own
+/// thread safety, for C++ functions and the objects they touch, is that of its
+/// implementation.
+///
+/// ```compile_fail
+/// use std::rc::Rc;
+/// let count = Rc::new(0_i64);
+/// tvm_ffi::Function::from_typed(move || -> tvm_ffi::Result<i64> { Ok(*count) 
});
+/// ```
 #[derive(Clone, ObjectRef)]
 pub struct Function {
     data: ObjectArc<FunctionObj>,
 }
 
+// SAFETY: a `Function` is immutable, and every Rust callback it wraps is 
`Send + Sync`.
+unsafe impl Send for Function {}
+unsafe impl Sync for Function {}
+
 //------------------------------------------------------------------------
 // CallbackFunctionObjImpl
 //------------------------------------------------------------------------
@@ -70,12 +85,12 @@ pub struct Function {
 /// can correctly delete the entire object including callback part
 /// then we will convert to ObjectArc<FunctionObj> to be used as function
 #[repr(C)]
-struct CallbackFunctionObjImpl<F: Fn(&[AnyView]) -> Result<Any> + 'static> {
+struct CallbackFunctionObjImpl<F: Fn(&[AnyView]) -> Result<Any> + Send + Sync 
+ 'static> {
     function: FunctionObj,
     callback: F,
 }
 
-impl<F: Fn(&[AnyView]) -> Result<Any> + 'static> CallbackFunctionObjImpl<F> {
+impl<F: Fn(&[AnyView]) -> Result<Any> + Send + Sync + 'static> 
CallbackFunctionObjImpl<F> {
     pub fn from_callback(callback: F) -> Self {
         Self {
             function: FunctionObj {
@@ -117,7 +132,9 @@ impl<F: Fn(&[AnyView]) -> Result<Any> + 'static> 
CallbackFunctionObjImpl<F> {
     }
 }
 
-unsafe impl<F: Fn(&[AnyView]) -> Result<Any> + 'static> ObjectCore for 
CallbackFunctionObjImpl<F> {
+unsafe impl<F: Fn(&[AnyView]) -> Result<Any> + Send + Sync + 'static> 
ObjectCore
+    for CallbackFunctionObjImpl<F>
+{
     const TYPE_KEY: &'static str = FunctionObj::TYPE_KEY;
     const TYPE_DEPTH: i32 = FunctionObj::TYPE_DEPTH;
     fn type_index() -> i32 {
@@ -350,7 +367,7 @@ impl Function {
     /// See also [`get_global`](Self::get_global).
     pub fn register_global_typed<F, I, O>(name: &str, func: F, doc: &str) -> 
Result<()>
     where
-        F: AsPackedCallable<I, O> + 'static,
+        F: AsPackedCallable<I, O> + Send + Sync + 'static,
     {
         let type_schema = F::type_schema();
         Self::register_global_with_info(name, &Self::from_typed(func), 
&type_schema, doc)
@@ -405,7 +422,7 @@ impl Function {
     /// See also [`Function`].
     pub fn from_packed<F>(func: F) -> Self
     where
-        F: Fn(&[AnyView]) -> Result<Any> + 'static,
+        F: Fn(&[AnyView]) -> Result<Any> + Send + Sync + 'static,
     {
         unsafe {
             let callback_arc = 
ObjectArc::new(CallbackFunctionObjImpl::from_callback(func));
@@ -441,7 +458,7 @@ impl Function {
     /// See also [`Function`].
     pub fn from_typed<F, I, O>(func: F) -> Self
     where
-        F: AsPackedCallable<I, O> + 'static,
+        F: AsPackedCallable<I, O> + Send + Sync + 'static,
     {
         let closure = move |packed_args: &[AnyView]| -> Result<Any> {
             let ret_value = func.call_packed(packed_args)?;
diff --git a/rust/tvm-ffi/src/function_internal.rs 
b/rust/tvm-ffi/src/function_internal.rs
index 4d71d9a0..ed6c8ca3 100644
--- a/rust/tvm-ffi/src/function_internal.rs
+++ b/rust/tvm-ffi/src/function_internal.rs
@@ -266,7 +266,7 @@ impl PackedArg for Any {
 
 impl<T> PackedArg for RValueRef<T>
 where
-    T: ObjectRefCore + AnyCompatible,
+    T: ObjectRefCore,
 {
     #[inline]
     fn as_packed_arg(&self) -> AnyView<'_> {
@@ -353,7 +353,7 @@ impl<T: ContainerElement + Clone> ArgIntoRef for 
&crate::Array<T> {
 
 impl<T> IntoArgHolder for RValueRef<T>
 where
-    T: ObjectRefCore + AnyCompatible,
+    T: ObjectRefCore,
 {
     type Target = Self;
     fn into_arg_holder(self) -> Self::Target {
@@ -363,7 +363,7 @@ where
 
 impl<T> ArgIntoRef for RValueRef<T>
 where
-    T: ObjectRefCore + AnyCompatible,
+    T: ObjectRefCore,
 {
     type Target = Self;
     fn to_ref(&self) -> &Self::Target {
diff --git a/rust/tvm-ffi/src/lib.rs b/rust/tvm-ffi/src/lib.rs
index 9ebfdd46..f6163a62 100644
--- a/rust/tvm-ffi/src/lib.rs
+++ b/rust/tvm-ffi/src/lib.rs
@@ -74,8 +74,8 @@ pub use crate::extra::unchanged::{Unchanged, UnchangedOr};
 pub use crate::function::Function;
 pub use crate::function_internal::AsPackedCallable;
 pub use crate::object::{
-    IntoObjectRef, Object, ObjectArc, ObjectCore, ObjectCoreWithExtraItems, 
ObjectIdentity,
-    ObjectRefCore,
+    ImmutableObject, IntoObjectRef, Object, ObjectArc, ObjectCore, 
ObjectCoreWithExtraItems,
+    ObjectIdentity, ObjectRefCore,
 };
 pub use crate::optional::{Optional, OptionalCompatible};
 pub use crate::reflection::{
diff --git a/rust/tvm-ffi/src/macros.rs b/rust/tvm-ffi/src/macros.rs
index c5e38a2b..5bd78d5b 100644
--- a/rust/tvm-ffi/src/macros.rs
+++ b/rust/tvm-ffi/src/macros.rs
@@ -66,6 +66,8 @@ macro_rules! cached_global_func {
 /// `Target`, `AsRef<Target>`, which borrows a `&Source` as `&Target`
 /// without touching the reference count, and
 /// [`IntoObjectRef<Target>`](crate::object::IntoObjectRef) for `Source` and 
`&Source`.
+/// It also implements the strict downcast back, `TryFrom<Target>` and
+/// `TryFrom<&Target>` for `Source`, which return a `TypeError` naming both 
types.
 ///
 /// # Safety
 ///
@@ -139,6 +141,33 @@ macro_rules! unsafe_impl_object_upcast {
                 }
             }
 
+            // The strict downcast back, as a `TryFrom` for `?`: a reference
+            // never converts, so this only checks the object's runtime type.
+            impl ::core::convert::TryFrom<$target> for $source {
+                type Error = $crate::error::Error;
+                #[inline]
+                fn try_from(value: $target) -> ::core::result::Result<Self, 
Self::Error> {
+                    $crate::object::ObjectRefCore::downcast::<$source>(value)
+                        
.map_err($crate::error::Error::downcast_failed::<$source>)
+                }
+            }
+
+            impl ::core::convert::TryFrom<&$target> for $source {
+                type Error = $crate::error::Error;
+                #[inline]
+                fn try_from(value: &$target) -> ::core::result::Result<Self, 
Self::Error> {
+                    match 
$crate::object::ObjectRefCore::downcast_ref::<$source>(value) {
+                        ::core::option::Option::Some(found) => {
+                            
::core::result::Result::Ok(::core::clone::Clone::clone(found))
+                        }
+                        ::core::option::Option::None => 
::core::result::Result::Err(
+                            $crate::error::Error::downcast_failed::<$source>(
+                                ::core::clone::Clone::clone(value),
+                            ),
+                        ),
+                    }
+                }
+            }
         )+
     };
 }
diff --git a/rust/tvm-ffi/src/match_any_internal.rs 
b/rust/tvm-ffi/src/match_any_internal.rs
index f08eae09..caddc78a 100644
--- a/rust/tvm-ffi/src/match_any_internal.rs
+++ b/rust/tvm-ffi/src/match_any_internal.rs
@@ -59,7 +59,7 @@ pub trait PrepareSource {
     type Source;
     fn prepare(&mut self) -> Self::Source;
 }
-impl<T: crate::ObjectRefCore + AnyCompatible> PrepareSource for SourceProbe<T> 
{
+impl<T: crate::ObjectRefCore> PrepareSource for SourceProbe<T> {
     type Source = OwnedSource;
     #[inline(always)]
     fn prepare(&mut self) -> Self::Source {
diff --git a/rust/tvm-ffi/src/object.rs b/rust/tvm-ffi/src/object.rs
index afe7c223..a07ff29e 100644
--- a/rust/tvm-ffi/src/object.rs
+++ b/rust/tvm-ffi/src/object.rs
@@ -48,8 +48,32 @@ pub struct ObjectArc<T: ObjectCore> {
     _phantom: std::marker::PhantomData<T>,
 }
 
-unsafe impl<T: Send + Sync + ObjectCore> Send for ObjectArc<T> {}
-unsafe impl<T: Send + Sync + ObjectCore> Sync for ObjectArc<T> {}
+// Not `Send`/`Sync` by default: an object's thread safety is that of its 
(often
+// C++) implementation, which the handle cannot see. An object type opts in 
with
+// `ImmutableObject`; tvm-ffi's own immutable handles opt in explicitly.
+
+/// An object type whose shared instances are never mutated, so handles to it 
may
+/// cross threads.
+///
+/// A handle `ObjectArc<T>`, and every reference built on it, is `Send + Sync`
+/// when `T: ImmutableObject` and `T`'s fields are themselves `Send + Sync`, so
+/// a field such as `Any`, which may hold a mutable object, keeps the handle
+/// single-threaded. The stub generator implements it for each class whose C++
+/// declaration is not `_type_mutable`. An erased `ObjectRef` may point at any
+/// object, so it never crosses threads.
+///
+/// # Safety
+///
+/// No code may mutate an instance while it is shared: changes go through
+/// copy-on-write, in place only on a uniquely held instance, as for C++ 
classes
+/// that are not `_type_mutable`.
+pub unsafe trait ImmutableObject: ObjectCore {}
+
+// SAFETY: `T` is never mutated while shared, its fields are thread-safe, and 
the
+// reference count is atomic.
+unsafe impl<T: ImmutableObject + Send + Sync> Send for ObjectArc<T> {}
+// SAFETY: as for `Send`.
+unsafe impl<T: ImmutableObject + Send + Sync> Sync for ObjectArc<T> {}
 
 /// The native representation of an object-system type.
 ///
@@ -148,7 +172,7 @@ pub unsafe trait ObjectCoreWithExtraItems: ObjectCore {
 /// must be able to reclaim that owned representation exactly once, and a true
 /// `check_any_strict` result must guarantee that both after-check constructors
 /// are valid for it.
-pub unsafe trait ObjectRefCore: Sized + Clone {
+pub unsafe trait ObjectRefCore: Sized + Clone + AnyCompatible {
     type ContainerType: ObjectCore;
     fn data(this: &Self) -> &ObjectArc<Self::ContainerType>;
     fn into_data(this: Self) -> ObjectArc<Self::ContainerType>;
@@ -194,23 +218,21 @@ pub unsafe trait ObjectRefCore: Sized + Clone {
     ///
     /// `B` is a subtype, or a parameterized container such as `Array<i64>`,
     /// whose elements are checked too. On a mismatch the reference is returned
-    /// unchanged, so another type can be tried;
-    /// [`Error::downcast_failed`](crate::Error::downcast_failed) turns it 
into a
-    /// `TypeError` for `?`.
+    /// unchanged, so another type can be tried. For `?`, use the `TryFrom` 
that
+    /// each upcast provides, which returns a `TypeError` instead.
     ///
     /// ```
-    /// use tvm_ffi::{object::ObjectRef, Array, Error, ObjectRefCore};
+    /// use tvm_ffi::{object::ObjectRef, Array, ObjectRefCore};
     /// let object: ObjectRef = Array::from_vec(vec![1_i64]).into();
     /// let object = object.downcast::<Array<f64>>().unwrap_err();
-    /// let array: Array<i64> = 
object.downcast().map_err(Error::downcast_failed::<Array<i64>>)?;
+    /// let array: Array<i64> = object.try_into()?;
     /// assert_eq!(array.len(), 1);
     /// # Ok::<(), tvm_ffi::Error>(())
     /// ```
     #[inline(always)]
     fn downcast<B>(self) -> std::result::Result<B, Self>
     where
-        Self: AnyCompatible,
-        B: ObjectRefCore + AnyCompatible,
+        B: ObjectRefCore,
     {
         let mut any_data = TVMFFIAny::new();
         unsafe {
@@ -405,6 +427,30 @@ pub fn is_instance_of<Target: 
ObjectCore>(object_type_index: i32) -> bool {
     }
 }
 
+/// The strict downcast from the root [`ObjectRef`] to `B`, for the containers'
+/// `TryFrom<ObjectRef>`, which are hand-written as their upcasts are.
+#[inline(always)]
+pub(crate) fn root_try_into<B>(value: ObjectRef) -> crate::error::Result<B>
+where
+    B: ObjectRefCore,
+{
+    value
+        .downcast::<B>()
+        .map_err(crate::error::Error::downcast_failed::<B>)
+}
+
+/// [`root_try_into`] from a borrowed reference, cloning only on success.
+#[inline(always)]
+pub(crate) fn root_try_into_ref<B>(value: &ObjectRef) -> 
crate::error::Result<B>
+where
+    B: ObjectRefCore + crate::type_traits::DowncastRefFromObject,
+{
+    match value.downcast_ref::<B>() {
+        Some(found) => Ok(found.clone()),
+        None => Err(crate::error::Error::downcast_failed::<B>(value.clone())),
+    }
+}
+
 /// Upcast a managed reference to the root [`ObjectRef`], keeping its 
allocation.
 #[inline(always)]
 pub(crate) fn into_root_object_ref<S: ObjectRefCore>(value: S) -> ObjectRef {
@@ -420,7 +466,23 @@ pub(crate) fn into_root_object_ref<S: 
ObjectRefCore>(value: S) -> ObjectRef {
 
 /// Format a managed reference as its runtime type key and object address, as
 /// the `Debug` implementation of `#[derive(ObjectRef)]` does.
-#[doc(hidden)]
+///
+/// Call it from `Debug` for a reference type implemented by hand, such as a
+/// typed view over another reference, so that `downcast(..).unwrap()` and
+/// `assert_eq!` can print it.
+///
+/// ```
+/// use tvm_ffi::object::{fmt_object_ref, ObjectRef};
+/// use tvm_ffi::Array;
+/// struct Shown(ObjectRef);
+/// impl std::fmt::Debug for Shown {
+///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+///         fmt_object_ref(&self.0, f)
+///     }
+/// }
+/// let shown = Shown(Array::from_vec(vec![1_i64]).into());
+/// assert!(format!("{shown:?}").starts_with("ffi.Array@"));
+/// ```
 pub fn fmt_object_ref<T: ObjectRefCore>(
     this: &T,
     f: &mut std::fmt::Formatter<'_>,
diff --git a/rust/tvm-ffi/src/reflection/registry.rs 
b/rust/tvm-ffi/src/reflection/registry.rs
index 9a6f3622..90511e64 100644
--- a/rust/tvm-ffi/src/reflection/registry.rs
+++ b/rust/tvm-ffi/src/reflection/registry.rs
@@ -309,7 +309,7 @@ impl<N: ObjectCore> ObjectDef<N> {
     /// type's registration finishes.
     pub fn def<F, I, O>(&mut self, name: &str, func: F, doc: &str) -> &mut Self
     where
-        F: AsPackedCallable<I, O> + 'static,
+        F: AsPackedCallable<I, O> + Send + Sync + 'static,
     {
         self.method(name, func, doc, false)
     }
@@ -318,14 +318,14 @@ impl<N: ObjectCore> ObjectDef<N> {
     /// `ObjectDef::def_static` does; otherwise as [`ObjectDef::def`].
     pub fn def_static<F, I, O>(&mut self, name: &str, func: F, doc: &str) -> 
&mut Self
     where
-        F: AsPackedCallable<I, O> + 'static,
+        F: AsPackedCallable<I, O> + Send + Sync + 'static,
     {
         self.method(name, func, doc, true)
     }
 
     fn method<F, I, O>(&mut self, name: &str, func: F, doc: &str, is_static: 
bool) -> &mut Self
     where
-        F: AsPackedCallable<I, O> + 'static,
+        F: AsPackedCallable<I, O> + Send + Sync + 'static,
     {
         if !self.fresh || self.error.is_some() {
             return self;
diff --git a/rust/tvm-ffi/src/rvalue_ref.rs b/rust/tvm-ffi/src/rvalue_ref.rs
index bb739271..5cd020b4 100644
--- a/rust/tvm-ffi/src/rvalue_ref.rs
+++ b/rust/tvm-ffi/src/rvalue_ref.rs
@@ -25,7 +25,7 @@ use std::marker::PhantomData;
 use tvm_ffi_sys::{TVMFFIAny, TVMFFIObject, TVMFFITypeIndex as TypeIndex};
 
 use crate::any::ArgTryFromAnyView;
-use crate::{AnyCompatible, AnyView, Error, ObjectRefCore, Result};
+use crate::{AnyView, Error, ObjectRefCore, Result};
 
 /// A move-aware object argument compatible with C++ `ffi::RValueRef<T>`.
 ///
@@ -33,7 +33,7 @@ use crate::{AnyCompatible, AnyView, Error, ObjectRefCore, 
Result};
 /// count; otherwise this wrapper retains and releases it.
 pub struct RValueRef<T>
 where
-    T: ObjectRefCore + AnyCompatible,
+    T: ObjectRefCore,
 {
     slot: UnsafeCell<*mut TVMFFIObject>,
     _marker: PhantomData<T>,
@@ -41,7 +41,7 @@ where
 
 impl<T> RValueRef<T>
 where
-    T: ObjectRefCore + AnyCompatible,
+    T: ObjectRefCore,
 {
     /// Transfer an owned object reference into an rvalue argument slot.
     pub fn new(value: T) -> Self {
@@ -96,7 +96,7 @@ where
 
 impl<T> From<T> for RValueRef<T>
 where
-    T: ObjectRefCore + AnyCompatible,
+    T: ObjectRefCore,
 {
     fn from(value: T) -> Self {
         Self::new(value)
@@ -105,7 +105,7 @@ where
 
 impl<'a, T> From<&'a RValueRef<T>> for AnyView<'a>
 where
-    T: ObjectRefCore + AnyCompatible,
+    T: ObjectRefCore,
 {
     fn from(value: &'a RValueRef<T>) -> Self {
         let mut raw = TVMFFIAny::new();
@@ -117,7 +117,7 @@ where
 
 impl<T> TryFrom<AnyView<'_>> for RValueRef<T>
 where
-    T: ObjectRefCore + AnyCompatible,
+    T: ObjectRefCore,
 {
     type Error = Error;
 
@@ -128,7 +128,7 @@ where
 
 impl<T> crate::type_traits::TypeSchema for RValueRef<T>
 where
-    T: ObjectRefCore + AnyCompatible,
+    T: ObjectRefCore,
 {
     fn type_schema() -> std::string::String {
         crate::type_traits::type_schema("ObjectRValueRef", &[T::type_schema()])
@@ -137,7 +137,7 @@ where
 
 impl<T> ArgTryFromAnyView for RValueRef<T>
 where
-    T: ObjectRefCore + AnyCompatible,
+    T: ObjectRefCore,
 {
     fn try_from_any_view(value: &AnyView<'_>, arg_index: usize) -> 
Result<Self> {
         unsafe { Self::from_view(value, Some(arg_index)) }
@@ -146,7 +146,7 @@ where
 
 impl<T> Drop for RValueRef<T>
 where
-    T: ObjectRefCore + AnyCompatible,
+    T: ObjectRefCore,
 {
     fn drop(&mut self) {
         let object = *self.slot.get_mut();
@@ -177,7 +177,7 @@ unsafe fn conversion_error<T>(
     already_moved: bool,
 ) -> Error
 where
-    T: ObjectRefCore + AnyCompatible,
+    T: ObjectRefCore,
 {
     let source = if already_moved {
         "an already-moved RValueRef".to_string()
diff --git a/rust/tvm-ffi/src/string.rs b/rust/tvm-ffi/src/string.rs
index 35b710db..ec55ea5a 100644
--- a/rust/tvm-ffi/src/string.rs
+++ b/rust/tvm-ffi/src/string.rs
@@ -45,6 +45,10 @@ pub struct Bytes {
     data: TVMFFIAny,
 }
 
+// SAFETY: `Bytes` is immutable: inline bytes or an immutable bytes object.
+unsafe impl Send for Bytes {}
+unsafe impl Sync for Bytes {}
+
 // BytesObj for heap-allocated bytes
 #[repr(C)]
 #[derive(Object)]
@@ -255,6 +259,10 @@ pub struct String {
     data: TVMFFIAny,
 }
 
+// SAFETY: a `String` is immutable: an inline small string or an immutable 
string object.
+unsafe impl Send for String {}
+unsafe impl Sync for String {}
+
 // StringObj for heap-allocated string
 #[repr(C)]
 #[derive(Object)]
diff --git a/rust/tvm-ffi/src/type_traits.rs b/rust/tvm-ffi/src/type_traits.rs
index 33448dad..6e65c2a3 100644
--- a/rust/tvm-ffi/src/type_traits.rs
+++ b/rust/tvm-ffi/src/type_traits.rs
@@ -306,7 +306,7 @@ unsafe impl RefFromAnyUnchecked for 
tvm_ffi_sys::dlpack::DLDevice {
 // An optional object reference is one nullable object pointer (`None` is null,
 // as `OptionalCompatible` relies on), which is the payload word of a `None` or
 // object cell, so `&Option<T>` borrows that word.
-unsafe impl<T: crate::object::ObjectRefCore + AnyCompatible> 
RefFromAnyUnchecked for Option<T> {
+unsafe impl<T: crate::object::ObjectRefCore> RefFromAnyUnchecked for Option<T> 
{
     #[inline(always)]
     unsafe fn ref_from_any_unchecked(cell: &TVMFFIAny) -> &Self {
         const {
@@ -322,7 +322,7 @@ unsafe impl<T: crate::object::ObjectRefCore + 
AnyCompatible> RefFromAnyUnchecked
     }
 }
 
-unsafe impl<T: crate::object::ObjectRefCore + AnyCompatible> 
DowncastRefFromAny for Option<T> {
+unsafe impl<T: crate::object::ObjectRefCore> DowncastRefFromAny for Option<T> {
     #[inline(always)]
     unsafe fn downcast_ref_from_any(cell: &TVMFFIAny) -> Option<&Self> {
         downcast_ref_from_any_in_place::<Self>(cell)
diff --git a/rust/tvm-ffi/tests/test_cast.rs b/rust/tvm-ffi/tests/test_cast.rs
index 300e2823..6e1e1386 100644
--- a/rust/tvm-ffi/tests/test_cast.rs
+++ b/rust/tvm-ffi/tests/test_cast.rs
@@ -213,10 +213,7 @@ fn test_downcast_failure() {
     let base = base.downcast::<TestDerived>().unwrap_err();
     assert_eq!(base.data.value, 1);
     assert_eq!(ObjectArc::strong_count(&base.data), 1);
-    let err = expect_err(
-        base.downcast::<TestDerived>()
-            .map_err(Error::downcast_failed::<TestDerived>),
-    );
+    let err = expect_err(TestDerived::try_from(base));
     assert!(err.message().contains("testing.TestObjectBase"));
     assert!(err.message().contains("testing.TestObjectDerived"));
     assert_eq!(delete_counter.load(Ordering::Relaxed), 1);
@@ -252,3 +249,16 @@ fn test_any_conversion_preserves_runtime_subtype() {
     drop(derived_from_any);
     assert_eq!(delete_counter.load(Ordering::Relaxed), 1);
 }
+
+// `TestBaseObj` opts in; `TestDerivedObj` does not, so only `TestBase` 
crosses threads.
+// SAFETY: test objects are never mutated after construction.
+unsafe impl tvm_ffi::ImmutableObject for TestBaseObj {}
+
+#[test]
+fn test_thread_safe_object_handles() {
+    fn thread_safe<T: Send + Sync>() {}
+    thread_safe::<TestBase>();
+    let base = new_base(3, Arc::new(AtomicU32::new(0)));
+    let value = std::thread::spawn(move || base.data.value).join().unwrap();
+    assert_eq!(value, 3);
+}
diff --git a/rust/tvm-ffi/tests/test_function_metadata.rs 
b/rust/tvm-ffi/tests/test_function_metadata.rs
index d1bcf056..a5c609fe 100644
--- a/rust/tvm-ffi/tests/test_function_metadata.rs
+++ b/rust/tvm-ffi/tests/test_function_metadata.rs
@@ -147,7 +147,7 @@ fn test_exports_without_metadata() {
 /// Registers `func` as `testing.rust.<name>` and records `name`.
 fn register_typed<F, I, O>(names: &mut Vec<&'static str>, name: &'static str, 
func: F)
 where
-    F: AsPackedCallable<I, O> + 'static,
+    F: AsPackedCallable<I, O> + Send + Sync + 'static,
 {
     Function::register_global_typed(&format!("testing.rust.{name}"), func, 
"").unwrap();
     names.push(name);
diff --git a/rust/tvm-ffi/tests/test_list.rs b/rust/tvm-ffi/tests/test_list.rs
index cb3174fe..acf3e1be 100644
--- a/rust/tvm-ffi/tests/test_list.rs
+++ b/rust/tvm-ffi/tests/test_list.rs
@@ -20,7 +20,7 @@ use tvm_ffi::*;
 
 /// Address of the element buffer and of its deleter, read through the C ABI
 /// layout of C++ `ListObj`: the object header, then its sequence cell.
-fn buffer_identity<T>(list: &List<T>) -> (usize, usize) {
+fn buffer_identity<T: tvm_ffi::type_traits::ContainerElement>(list: &List<T>) 
-> (usize, usize) {
     #[repr(C)]
     struct SeqCell {
         data: *mut core::ffi::c_void,
diff --git a/rust/tvm-ffi/tests/test_structural_mutate.rs 
b/rust/tvm-ffi/tests/test_structural_mutate.rs
index 35dcbb7a..e69053cf 100644
--- a/rust/tvm-ffi/tests/test_structural_mutate.rs
+++ b/rust/tvm-ffi/tests/test_structural_mutate.rs
@@ -249,7 +249,12 @@ where
     ObjectArc::as_ptr(<Array<T> as ObjectRefCore>::data(array))
 }
 
-fn map_pointer<K, V>(map: &Map<K, V>) -> *const 
tvm_ffi::collections::map::MapObj {
+fn map_pointer<
+    K: tvm_ffi::type_traits::ContainerElement,
+    V: tvm_ffi::type_traits::ContainerElement,
+>(
+    map: &Map<K, V>,
+) -> *const tvm_ffi::collections::map::MapObj {
     ObjectArc::as_ptr(<Map<K, V> as ObjectRefCore>::data(map))
 }
 
diff --git a/tests/python/test_stubgen_rust.py 
b/tests/python/test_stubgen_rust.py
index 2ce66af9..bbb5f043 100644
--- a/tests/python/test_stubgen_rust.py
+++ b/tests/python/test_stubgen_rust.py
@@ -76,6 +76,7 @@ def _info(
     parent: str | None = "ffi.Object",
     ancestors: list[str] | None = None,
     is_final: bool | None = None,
+    is_mutable: bool | None = None,
     total_size: int | None = None,
 ) -> ObjectInfo:
     if ancestors is None:
@@ -87,6 +88,7 @@ def _info(
         parent_type_key=parent,
         ancestors=ancestors,
         is_final=is_final,
+        is_mutable=is_mutable,
         total_size=total_size,
     )
 
@@ -443,6 +445,14 @@ def test_render_object_without_fields_has_no_impl() -> 
None:
     assert "FieldGetter" not in _uses(imports)
 
 
+def test_render_thread_safe_only_when_not_mutable() -> None:
+    """A class C++ declares not ``_type_mutable`` is ``ImmutableObject``; 
others are not."""
+    marker = "unsafe impl tvm_ffi::object::ImmutableObject for MarkerObj {}"
+    assert marker in _render(_info("demo.Marker", is_final=True, 
is_mutable=False))[0]
+    assert marker not in _render(_info("demo.Marker", is_final=True, 
is_mutable=True))[0]
+    assert marker not in _render(_info("demo.Marker", is_final=True))[0]
+
+
 def test_render_derived_object_same_module() -> None:
     """A generated parent is embedded, dereferenced to, and upcast to along 
the chain."""
     info = _info(

Reply via email to