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(