This is an automated email from the ASF dual-hosted git repository.
junrushao 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 488309e7 feat(core): support ObjectPtr in FFI type traits (#665)
488309e7 is described below
commit 488309e7e0dc2d8234056153a7707e9b35c35628
Author: Junru Shao <[email protected]>
AuthorDate: Tue Jul 14 15:31:12 2026 -0700
feat(core): support ObjectPtr in FFI type traits (#665)
Architecture:
- Add object-subclass and qualification detection for unqualified Object
pointees without depending on ObjectRef wrappers.
- Specialize runtime type indexing, subsumption, and TypeTraits for
ObjectPtr<TObject> so ownership crosses Any and packed-function
boundaries.
- Integrate direct pointers with reflected field lifetime,
serialization, and
structural traversal while keeping container shorthand out of this
boundary.
Public Interfaces:
- Allow ObjectPtr<TObject> in Any, typed function parameters, reflected
fields,
and containers that explicitly name ObjectPtr storage.
- Add base/derived pointer subsumption and report the pointee runtime
type key in
schemas and conversion diagnostics.
UI/UX:
- None.
Behavioral Changes:
- Copying an ObjectPtr through Any retains the object, while moving
transfers
ownership and clears the source Any value.
- Null pointers map to the ABI None value and casts enforce runtime
ancestry.
- Qualified and reference pointee types remain unsupported.
Docs:
- Document direct ObjectPtr FFI use, ownership behavior, qualification
limits,
and Optional's present-null ambiguity in the C++ guide.
Tests:
- Clean Debug tvm_ffi_tests build passed before container redirection
was added.
- All 19 ObjectPtr-matching Any, storage, reflection, serialization,
variant, and
structural visitor CTests passed.
- Clean editable build and 450 focused metadata/dataclass Python tests
passed.
- Scoped pre-commit hooks passed for all files in this boundary.
Untested Edge Cases:
- Rust bindings, Windows/MSVC, and the full cross-platform test matrix
were not
run for this intermediate boundary.
- Doxygen was not run; the original target tree is known to warn about
missing
documentation on the new ObjectPtr subsumption specialization.
- Stub regeneration still exposes pre-existing mutable-container
annotation
drift, so those unrelated generated edits are deferred.
Co-authored-by: Meghan Cowan <[email protected]>
Co-authored-by: Kathryn (Jinqi) Chen
<[email protected]>
---
docs/guides/cpp_lang_guide.md | 9 ++
include/tvm/ffi/object.h | 99 +++++++++++++++
python/tvm_ffi/testing/__init__.py | 1 +
python/tvm_ffi/testing/testing.py | 16 +++
src/ffi/extra/dataclass.cc | 7 ++
src/ffi/testing/testing.cc | 12 ++
tests/cpp/extra/test_serialization.cc | 26 ++++
tests/cpp/extra/test_structural_visit.cc | 19 ++-
tests/cpp/test_any.cc | 90 ++++++++++++-
tests/cpp/test_object.cc | 209 +++++++++++++++++++++++++++++++
tests/cpp/test_reflection.cc | 81 ++++++++++++
tests/cpp/testing_object.h | 32 +++++
tests/python/test_dataclass_py_class.py | 93 +++++++++++++-
tests/python/test_metadata.py | 12 +-
14 files changed, 702 insertions(+), 4 deletions(-)
diff --git a/docs/guides/cpp_lang_guide.md b/docs/guides/cpp_lang_guide.md
index f222e8b7..0b5733a8 100644
--- a/docs/guides/cpp_lang_guide.md
+++ b/docs/guides/cpp_lang_guide.md
@@ -125,6 +125,15 @@ void ExampleObjectPtr() {
}
```
+For an unqualified `Object` subclass `T`, `ObjectPtr<T>` can also cross the
FFI boundary
+directly. It can be stored in `Any` and typed containers such as `Array`,
`List`, `Map`,
+`Dict`, `Tuple`, `Optional`, `Variant`, and `Expected`, and can be used as a
reflected field or
+function parameter. Copying through `Any` retains the object; moving transfers
its reference. A
+null pointer is represented by the FFI `None` value. Qualified pointee types
such as
+`ObjectPtr<const T>`, `ObjectPtr<volatile T>`, and reference pointee types are
not supported.
+Because the ABI has one `None` representation, `Optional<ObjectPtr<T>>` cannot
preserve the
+difference between an absent optional and a present null pointer across an FFI
round trip.
+
We typically provide a reference class that wraps the ObjectPtr.
The `ObjectRef` base class provides the interface and reference counting
functionality for these wrapper classes.
diff --git a/include/tvm/ffi/object.h b/include/tvm/ffi/object.h
index 27bf8c38..0c952450 100644
--- a/include/tvm/ffi/object.h
+++ b/include/tvm/ffi/object.h
@@ -529,6 +529,34 @@ class ObjectPtr {
friend struct tvm::ffi::details::ObjectUnsafe;
};
+namespace details {
+
+/*!
+ * \brief Whether T is Object or a subclass of Object.
+ * \tparam T The type to inspect.
+ */
+template <typename T>
+inline constexpr bool is_object_subclass_v = std::is_base_of_v<Object, T>;
+
+/*! \brief Whether T is an Object subclass with cv- or reference
qualification. */
+template <typename T>
+inline constexpr bool is_qualified_object_v =
+ is_object_subclass_v<std::remove_cv_t<std::remove_reference_t<T>>> &&
+ !std::is_same_v<T, std::remove_cv_t<std::remove_reference_t<T>>>;
+
+} // namespace details
+
+/// \cond Doxygen_Suppress
+/*!
+ * \brief Whether target ObjectPtr storage subsumes source ObjectPtr storage.
+ * \tparam BaseObject The target ObjectPtr pointee type.
+ * \tparam DerivedObject The source ObjectPtr pointee type.
+ */
+template <typename BaseObject, typename DerivedObject>
+inline constexpr bool type_subsumes_v<ObjectPtr<BaseObject>,
ObjectPtr<DerivedObject>> =
+ std::is_base_of_v<BaseObject, DerivedObject>;
+/// \endcond
+
/*!
* \brief A custom smart pointer for Object.
* \tparam T the content data type.
@@ -1275,6 +1303,77 @@ struct TypeToRuntimeTypeIndex<T,
std::enable_if_t<std::is_base_of_v<ObjectRef, T
static int32_t v() { return T::ContainerType::RuntimeTypeIndex(); }
};
+template <typename TObject>
+struct TypeToRuntimeTypeIndex<
+ ObjectPtr<TObject>,
std::enable_if_t<details::is_object_subclass_v<TObject> &&
+ std::is_same_v<TObject,
std::remove_cv_t<TObject>>>> {
+ static int32_t v() { return TObject::RuntimeTypeIndex(); }
+};
+
+/*!
+ * \brief Type traits for an owning pointer to an unqualified Object subclass.
+ * \tparam TObject The unqualified Object subclass.
+ */
+template <typename TObject>
+struct TypeTraits<ObjectPtr<TObject>,
+ std::enable_if_t<details::is_object_subclass_v<TObject> &&
+ std::is_same_v<TObject,
std::remove_cv_t<TObject>>>>
+ : public TypeTraitsBase {
+ static constexpr int32_t field_static_type_index = TypeIndex::kTVMFFIObject;
+
+ TVM_FFI_INLINE static void CopyToAnyView(const ObjectPtr<TObject>& src,
TVMFFIAny* result) {
+ if (src == nullptr) {
+ TypeTraits<std::nullptr_t>::CopyToAnyView(nullptr, result);
+ return;
+ }
+ TVMFFIObject* obj_ptr =
details::ObjectUnsafe::TVMFFIObjectPtrFromObjectPtr(src);
+ result->type_index = obj_ptr->type_index;
+ result->zero_padding = 0;
+ TVM_FFI_CLEAR_PTR_PADDING_IN_FFI_ANY(result);
+ result->v_obj = obj_ptr;
+ }
+
+ TVM_FFI_INLINE static void MoveToAny(ObjectPtr<TObject> src, TVMFFIAny*
result) {
+ if (src == nullptr) {
+ TypeTraits<std::nullptr_t>::MoveToAny(nullptr, result);
+ return;
+ }
+ TVMFFIObject* obj_ptr =
details::ObjectUnsafe::MoveObjectPtrToTVMFFIObjectPtr(std::move(src));
+ result->type_index = obj_ptr->type_index;
+ result->zero_padding = 0;
+ TVM_FFI_CLEAR_PTR_PADDING_IN_FFI_ANY(result);
+ result->v_obj = obj_ptr;
+ }
+
+ TVM_FFI_INLINE static bool CheckAnyStrict(const TVMFFIAny* src) {
+ if (src->type_index == TypeIndex::kTVMFFINone) return true;
+ return src->type_index >= TypeIndex::kTVMFFIStaticObjectBegin &&
+ details::IsObjectInstance<TObject>(src->type_index);
+ }
+
+ TVM_FFI_INLINE static ObjectPtr<TObject> CopyFromAnyViewAfterCheck(const
TVMFFIAny* src) {
+ if (src->type_index == TypeIndex::kTVMFFINone) return nullptr;
+ return details::ObjectUnsafe::ObjectPtrFromUnowned<TObject>(src->v_obj);
+ }
+
+ TVM_FFI_INLINE static ObjectPtr<TObject> MoveFromAnyAfterCheck(TVMFFIAny*
src) {
+ if (src->type_index == TypeIndex::kTVMFFINone) return nullptr;
+ ObjectPtr<TObject> result =
details::ObjectUnsafe::ObjectPtrFromOwned<TObject>(src->v_obj);
+ TypeTraits<std::nullptr_t>::MoveToAny(nullptr, src);
+ return result;
+ }
+
+ TVM_FFI_INLINE static std::optional<ObjectPtr<TObject>>
TryCastFromAnyView(const TVMFFIAny* src) {
+ if (CheckAnyStrict(src)) return CopyFromAnyViewAfterCheck(src);
+ return std::nullopt;
+ }
+
+ TVM_FFI_INLINE static std::string TypeStr() { return TObject::_type_key; }
+ TVM_FFI_INLINE static std::string TypeSchema() {
+ return R"({"type":"Optional","args":[{"type":")" +
std::string(TObject::_type_key) + R"("}]})";
+ }
+};
+
template <typename TObjRef>
struct ObjectRefTypeTraitsBase : public TypeTraitsBase {
static constexpr int32_t field_static_type_index = TypeIndex::kTVMFFIObject;
diff --git a/python/tvm_ffi/testing/__init__.py
b/python/tvm_ffi/testing/__init__.py
index 4468123f..9ac65d73 100644
--- a/python/tvm_ffi/testing/__init__.py
+++ b/python/tvm_ffi/testing/__init__.py
@@ -28,6 +28,7 @@ from .testing import (
TestNonCopyable,
TestObjectBase,
TestObjectDerived,
+ TestObjectPtrHolder,
_SchemaAllTypes,
_TestCxxAutoInit,
_TestCxxAutoInitAllInitOff,
diff --git a/python/tvm_ffi/testing/testing.py
b/python/tvm_ffi/testing/testing.py
index 00bda30c..3c9dd624 100644
--- a/python/tvm_ffi/testing/testing.py
+++ b/python/tvm_ffi/testing/testing.py
@@ -109,6 +109,22 @@ class TestObjectDerived(TestObjectBase):
# tvm-ffi-stubgen(end)
+@c_class("testing.TestObjectPtrHolder")
+class TestObjectPtrHolder(Object):
+ """Test object with an ``ObjectPtr<TestObjectBase>`` field."""
+
+ __test__ = False
+
+ # tvm-ffi-stubgen(begin): object/testing.TestObjectPtrHolder
+ # fmt: off
+ value: TestObjectBase | None
+ if TYPE_CHECKING:
+ def __init__(self, value: TestObjectBase | None) -> None: ...
+ def __ffi_init__(self, value: TestObjectBase | None) -> None: ... #
ty: ignore[invalid-method-override]
+ # fmt: on
+ # tvm-ffi-stubgen(end)
+
+
@c_class("testing.TestNonCopyable")
class TestNonCopyable(Object):
"""Test object with deleted copy constructor."""
diff --git a/src/ffi/extra/dataclass.cc b/src/ffi/extra/dataclass.cc
index 9a193d08..ea94c475 100644
--- a/src/ffi/extra/dataclass.cc
+++ b/src/ffi/extra/dataclass.cc
@@ -1708,6 +1708,13 @@ class RecursiveComparer : public
ObjectGraphDFS<RecursiveComparer, CompareFrame,
// ---------- Python-defined type support ----------
+// Python-defined layouts use ObjectRef as the common in-memory representation
for every
+// object-valued C++ parent field, including ObjectPtr<T> fields.
+static_assert(std::is_standard_layout_v<ObjectPtr<Object>>);
+static_assert(std::is_standard_layout_v<ObjectRef>);
+static_assert(sizeof(ObjectPtr<Object>) == sizeof(ObjectRef));
+static_assert(alignof(ObjectPtr<Object>) == alignof(ObjectRef));
+
enum class PyClassFieldStorageKind {
kPOD,
kAny,
diff --git a/src/ffi/testing/testing.cc b/src/ffi/testing/testing.cc
index cce98c38..501f71a9 100644
--- a/src/ffi/testing/testing.cc
+++ b/src/ffi/testing/testing.cc
@@ -158,6 +158,16 @@ class TestObjectDerived : public TestObjectBase {
TVM_FFI_DECLARE_OBJECT_INFO_FINAL("testing.TestObjectDerived",
TestObjectDerived, TestObjectBase);
};
+class TestObjectPtrHolder : public Object {
+ public:
+ ObjectPtr<TestObjectBase> value;
+
+ explicit TestObjectPtrHolder(UnsafeInit) {}
+
+ static constexpr bool _type_mutable = true;
+ TVM_FFI_DECLARE_OBJECT_INFO("testing.TestObjectPtrHolder",
TestObjectPtrHolder, Object);
+};
+
class TestCxxClassBase : public Object {
public:
int64_t v_i64 = 0;
@@ -460,6 +470,8 @@ TVM_FFI_STATIC_INIT_BLOCK() {
.def_rw("v_map", &TestObjectDerived::v_map)
.def_rw("v_array", &TestObjectDerived::v_array);
+ refl::ObjectDef<TestObjectPtrHolder>().def_rw("value",
&TestObjectPtrHolder::value);
+
refl::ObjectDef<TestCxxClassBase>()
.def_rw("v_i64", &TestCxxClassBase::v_i64, refl::repr(false))
.def_rw("v_i32", &TestCxxClassBase::v_i32, refl::repr(false));
diff --git a/tests/cpp/extra/test_serialization.cc
b/tests/cpp/extra/test_serialization.cc
index c918a4a0..65f7d414 100644
--- a/tests/cpp/extra/test_serialization.cc
+++ b/tests/cpp/extra/test_serialization.cc
@@ -757,6 +757,32 @@ TEST(Serialization, SharedObjectReferences) {
EXPECT_EQ(result->body[0].get(), result->body[1].get());
}
+TEST(Serialization, ObjectPtrFields) {
+ ObjectPtr<TIntObj> shared = make_object<TIntObj>(42);
+ ObjectPtr<TIntObj> value = shared;
+ ObjectPtr<TNumberObj> alias = shared;
+ TObjectPtrHolder holder(std::move(value), std::move(alias));
+
+ Any deserialized = FromJSONGraph(ToJSONGraph(holder));
+ TObjectPtrHolder result = deserialized.cast<TObjectPtrHolder>();
+
+ ASSERT_NE(result->value, nullptr);
+ ASSERT_NE(result->alias, nullptr);
+ EXPECT_EQ(result->value.get(), result->alias.get());
+ ASSERT_TRUE(result->value->IsInstance<TIntObj>());
+ EXPECT_EQ(static_cast<TIntObj*>(result->value.get())->value, 42);
+}
+
+TEST(Serialization, NullObjectPtrFields) {
+ TObjectPtrHolder holder(nullptr, nullptr);
+
+ Any deserialized = FromJSONGraph(ToJSONGraph(holder));
+ TObjectPtrHolder result = deserialized.cast<TObjectPtrHolder>();
+
+ EXPECT_EQ(result->value, nullptr);
+ EXPECT_EQ(result->alias, nullptr);
+}
+
// ---------------------------------------------------------------------------
// Nested objects
// ---------------------------------------------------------------------------
diff --git a/tests/cpp/extra/test_structural_visit.cc
b/tests/cpp/extra/test_structural_visit.cc
index df8ee11c..1cc0a9df 100644
--- a/tests/cpp/extra/test_structural_visit.cc
+++ b/tests/cpp/extra/test_structural_visit.cc
@@ -332,7 +332,7 @@ TEST(StructuralVisitor, WalkVisitsPOD) {
}
TEST(StructuralVisitor, WalkVisitsObjectPtr) {
- TVar root("x");
+ ObjectPtr<TVarObj> root = make_object<TVarObj>("x");
std::vector<std::string> visited;
Optional<VisitInterrupt> result =
@@ -345,6 +345,23 @@ TEST(StructuralVisitor, WalkVisitsObjectPtr) {
ExpectTrace(visited, {"x"});
}
+TEST(StructuralVisitor, WalkTraversesObjectPtrFields) {
+ ObjectPtr<TIntObj> value = make_object<TIntObj>(42);
+ ObjectPtr<TNumberObj> alias = value;
+ TObjectPtrHolder root(value, alias);
+ size_t num_int_fields = 0;
+
+ Optional<VisitInterrupt> result =
+ StructuralWalk<WalkOrder::kPreOrder>(root, [&](const TIntObj* obj) ->
Expected<WalkResult> {
+ EXPECT_EQ(obj, value.get());
+ ++num_int_fields;
+ return WalkResult::Advance();
+ });
+
+ EXPECT_FALSE(result.has_value());
+ EXPECT_EQ(num_int_fields, 2U);
+}
+
TEST(StructuralVisitor, WalkReceivesDefRegionKind) {
TVar x("x");
TVar y("y");
diff --git a/tests/cpp/test_any.cc b/tests/cpp/test_any.cc
index 76ae1569..aeaaf506 100644
--- a/tests/cpp/test_any.cc
+++ b/tests/cpp/test_any.cc
@@ -31,6 +31,21 @@ namespace {
using namespace tvm::ffi;
using namespace tvm::ffi::testing;
+static_assert(TypeTraits<ObjectPtr<TIntObj>>::convert_enabled);
+static_assert(TypeTraits<ObjectPtr<TIntObj>>::storage_enabled);
+static_assert(!TypeTraits<ObjectPtr<const TIntObj>>::convert_enabled);
+static_assert(!TypeTraits<ObjectPtr<const TIntObj>>::storage_enabled);
+static_assert(!TypeTraits<ObjectPtr<volatile TIntObj>>::convert_enabled);
+static_assert(!TypeTraits<ObjectPtr<volatile TIntObj>>::storage_enabled);
+static_assert(!TypeTraits<ObjectPtr<const volatile TIntObj>>::convert_enabled);
+static_assert(!TypeTraits<ObjectPtr<const volatile TIntObj>>::storage_enabled);
+static_assert(TypeToFieldStaticTypeIndex<ObjectPtr<TIntObj>>::value ==
TypeIndex::kTVMFFIObject);
+static_assert(details::is_object_subclass_v<TIntObj>);
+static_assert(!details::is_object_subclass_v<void>);
+static_assert(type_subsumes_v<ObjectPtr<TNumberObj>, ObjectPtr<TIntObj>>);
+static_assert(!type_subsumes_v<ObjectPtr<TIntObj>, ObjectPtr<TNumberObj>>);
+static_assert(!type_subsumes_v<ObjectPtr<TIntObj>, ObjectPtr<TFloatObj>>);
+
TEST(Any, Int) {
AnyView view0;
EXPECT_EQ(view0.CopyToTVMFFIAny().type_index, TypeIndex::kTVMFFINone);
@@ -314,6 +329,79 @@ TEST(Any, Object) {
EXPECT_EQ(v1.use_count(), 3);
}
+TEST(Any, ObjectPtr) {
+ {
+ ObjectPtr<TIntObj> ptr = make_object<TIntObj>(11);
+ TIntObj* raw_ptr = ptr.get();
+ EXPECT_EQ(ptr.use_count(), 1);
+
+ AnyView view = ptr;
+ EXPECT_EQ(ptr.use_count(), 1);
+ EXPECT_EQ(view.type_index(), TIntObj::RuntimeTypeIndex());
+
+ ObjectPtr<TIntObj> exact_ptr = view.cast<ObjectPtr<TIntObj>>();
+ EXPECT_EQ(ptr.use_count(), 2);
+ EXPECT_EQ(exact_ptr.get(), raw_ptr);
+ exact_ptr.reset();
+ EXPECT_EQ(ptr.use_count(), 1);
+
+ ObjectPtr<TNumberObj> base_ptr = view.cast<ObjectPtr<TNumberObj>>();
+ EXPECT_EQ(ptr.use_count(), 2);
+ EXPECT_EQ(base_ptr.get(), static_cast<TNumberObj*>(raw_ptr));
+ EXPECT_EQ(static_cast<TIntObj*>(base_ptr.get())->value, 11);
+ base_ptr.reset();
+ EXPECT_EQ(ptr.use_count(), 1);
+
+ EXPECT_FALSE(view.try_cast<ObjectPtr<TFloatObj>>().has_value());
+ }
+
+ {
+ ObjectPtr<TIntObj> ptr = make_object<TIntObj>(12);
+ TIntObj* raw_ptr = ptr.get();
+ Any value = ptr;
+ EXPECT_EQ(ptr.use_count(), 2);
+ EXPECT_EQ(value.type_index(), TIntObj::RuntimeTypeIndex());
+
+ ObjectPtr<TIntObj> copied_ptr = value.cast<ObjectPtr<TIntObj>>();
+ EXPECT_EQ(copied_ptr.get(), raw_ptr);
+ EXPECT_EQ(ptr.use_count(), 3);
+ copied_ptr.reset();
+ EXPECT_EQ(ptr.use_count(), 2);
+
+ value.reset();
+ EXPECT_EQ(ptr.use_count(), 1);
+ }
+
+ {
+ ObjectPtr<TIntObj> ptr = make_object<TIntObj>(13);
+ TIntObj* raw_ptr = ptr.get();
+ Any value = std::move(ptr);
+ EXPECT_TRUE(ptr == nullptr); //
NOLINT(bugprone-use-after-move,clang-analyzer-cplusplus.Move)
+ EXPECT_EQ(raw_ptr->use_count(), 1);
+
+ ObjectPtr<TIntObj> moved_ptr = std::move(value).cast<ObjectPtr<TIntObj>>();
+ EXPECT_TRUE(value == nullptr); //
NOLINT(bugprone-use-after-move,clang-analyzer-cplusplus.Move)
+ EXPECT_EQ(moved_ptr.get(), raw_ptr);
+ EXPECT_EQ(moved_ptr.use_count(), 1);
+ }
+
+ {
+ ObjectPtr<TIntObj> ptr;
+ AnyView view = ptr;
+ EXPECT_EQ(view.type_index(), TypeIndex::kTVMFFINone);
+ EXPECT_EQ(view.cast<ObjectPtr<TIntObj>>(), nullptr);
+
+ Any value = ptr;
+ EXPECT_EQ(value.type_index(), TypeIndex::kTVMFFINone);
+ EXPECT_EQ(std::move(value).cast<ObjectPtr<TIntObj>>(), nullptr);
+ EXPECT_TRUE(value == nullptr); //
NOLINT(bugprone-use-after-move,clang-analyzer-cplusplus.Move)
+ }
+
+ EXPECT_EQ(TypeToRuntimeTypeIndex<ObjectPtr<TIntObj>>::v(),
TIntObj::RuntimeTypeIndex());
+ EXPECT_EQ(TypeTraits<ObjectPtr<TNumberObj>>::TypeSchema(),
+ R"({"type":"Optional","args":[{"type":"test.Number"}]})");
+}
+
TEST(Any, ObjectRefWithFallbackTraits) {
// Test case for TPrimExpr fallback from Any
Any any1 = TPrimExpr("float32", 3.14);
@@ -459,7 +547,7 @@ TEST(Any, ObjectMove) {
auto v0 = std::move(any1).cast<TPrimExpr>();
EXPECT_EQ(v0->value, 3.14);
EXPECT_EQ(v0.use_count(), 1);
- EXPECT_TRUE(any1 == nullptr); // NOLINT(bugprone-use-after-move)
+ EXPECT_TRUE(any1 == nullptr); //
NOLINT(bugprone-use-after-move,clang-analyzer-cplusplus.Move)
}
TEST(Any, AnyEqualHash) {
diff --git a/tests/cpp/test_object.cc b/tests/cpp/test_object.cc
index 620fb119..70e453a9 100644
--- a/tests/cpp/test_object.cc
+++ b/tests/cpp/test_object.cc
@@ -24,11 +24,13 @@
#include <tvm/ffi/container/map.h>
#include <tvm/ffi/container/tuple.h>
#include <tvm/ffi/container/variant.h>
+#include <tvm/ffi/expected.h>
#include <tvm/ffi/memory.h>
#include <tvm/ffi/object.h>
#include <tvm/ffi/optional.h>
#include <stdexcept>
+#include <type_traits>
#include "./testing_object.h"
@@ -471,4 +473,211 @@ TEST(Object, OpaqueObject) {
EXPECT_EQ(deleter_trigger_counter, 1);
}
+using NumberPtr = ObjectPtr<TNumberObj>;
+
+NumberPtr MakeInt(int64_t value) { return make_object<TIntObj>(value); }
+
+NumberPtr MakeFloat(double value) { return make_object<TFloatObj>(value); }
+
+TEST(ObjectPtrStorage, Array) {
+ NumberPtr first = MakeInt(1);
+ NumberPtr second = MakeFloat(2.0);
+ NumberPtr null;
+ Array<NumberPtr> array{first, null};
+
+ EXPECT_EQ(array.size(), 2U);
+ EXPECT_EQ(array[0], first);
+ EXPECT_EQ(array[1], nullptr);
+
+ array.Set(1, second);
+ array.push_back(null);
+ EXPECT_EQ(array[1], second);
+ EXPECT_EQ(array[2], nullptr);
+
+ size_t defined = 0;
+ for (NumberPtr item : array) {
+ defined += item != nullptr;
+ }
+ EXPECT_EQ(defined, 2U);
+
+ Array<NumberPtr> roundtrip = Any(array).cast<Array<NumberPtr>>();
+ EXPECT_TRUE(roundtrip.same_as(array));
+ EXPECT_EQ(roundtrip[0], first);
+ EXPECT_EQ(roundtrip[1], second);
+ EXPECT_EQ(roundtrip[2], nullptr);
+
+ ObjectPtr<TIntObj> derived = make_object<TIntObj>(3);
+ Array<ObjectPtr<TIntObj>> derived_array{derived};
+ Array<NumberPtr> upcast_array = derived_array;
+ EXPECT_TRUE(upcast_array.same_as(derived_array));
+ EXPECT_EQ(upcast_array[0].get(), static_cast<TNumberObj*>(derived.get()));
+}
+
+TEST(ObjectPtrStorage, List) {
+ NumberPtr first = MakeInt(1);
+ NumberPtr second = MakeFloat(2.0);
+ NumberPtr null;
+ List<NumberPtr> list{first, null};
+
+ list.Set(1, second);
+ list.insert(list.begin() + 1, null);
+ list.push_back(first);
+ EXPECT_EQ(list.size(), 4U);
+ EXPECT_EQ(list[0], first);
+ EXPECT_EQ(list[1], nullptr);
+ EXPECT_EQ(list[2], second);
+ EXPECT_EQ(list[3], first);
+
+ List<NumberPtr> roundtrip = Any(list).cast<List<NumberPtr>>();
+ EXPECT_TRUE(roundtrip.same_as(list));
+ roundtrip.Set(1, second);
+ EXPECT_EQ(list[1], second);
+}
+
+TEST(ObjectPtrStorage, Map) {
+ NumberPtr key = MakeInt(1);
+ NumberPtr value = MakeFloat(2.0);
+ NumberPtr null;
+ Map<NumberPtr, NumberPtr> map{{key, value}};
+
+ EXPECT_EQ(map.count(key), 1U);
+ EXPECT_EQ(map[key], value);
+ map.Set(key, null);
+ map.Set(null, key);
+ EXPECT_EQ(map[key], nullptr);
+ EXPECT_EQ(map[null], key);
+
+ size_t entries = 0;
+ for (const auto& [stored_key, stored_value] : map) {
+ if (stored_key == key) {
+ EXPECT_EQ(stored_value, nullptr);
+ }
+ if (stored_key == nullptr) {
+ EXPECT_EQ(stored_value, key);
+ }
+ ++entries;
+ }
+ EXPECT_EQ(entries, 2U);
+
+ Map<NumberPtr, NumberPtr> roundtrip = Any(map).cast<Map<NumberPtr,
NumberPtr>>();
+ EXPECT_TRUE(roundtrip.same_as(map));
+ EXPECT_EQ(roundtrip[null], key);
+}
+
+TEST(ObjectPtrStorage, Dict) {
+ NumberPtr key = MakeInt(1);
+ NumberPtr value = MakeFloat(2.0);
+ NumberPtr null;
+ Dict<NumberPtr, NumberPtr> dict{{key, value}, {null, key}};
+ Dict<NumberPtr, NumberPtr> alias = dict;
+
+ EXPECT_EQ(dict[key], value);
+ EXPECT_EQ(dict[null], key);
+ dict.Set(key, null);
+ EXPECT_EQ(alias[key], nullptr);
+
+ auto found = dict.Get(null);
+ ASSERT_TRUE(found.has_value());
+ EXPECT_EQ(found.value(), key); // NOLINT(bugprone-unchecked-optional-access)
+
+ Dict<NumberPtr, NumberPtr> roundtrip = Any(dict).cast<Dict<NumberPtr,
NumberPtr>>();
+ EXPECT_TRUE(roundtrip.same_as(dict));
+ roundtrip.erase(null);
+ EXPECT_EQ(dict.count(null), 0U);
+}
+
+TEST(ObjectPtrStorage, Tuple) {
+ NumberPtr first = MakeInt(1);
+ NumberPtr second = MakeFloat(2.0);
+ NumberPtr null;
+ Tuple<NumberPtr, NumberPtr> tuple(first, null);
+
+ EXPECT_EQ(tuple.get<0>(), first);
+ EXPECT_EQ(tuple.get<1>(), nullptr);
+ tuple.Set<1>(second);
+ EXPECT_EQ(tuple.get<1>(), second);
+
+ Tuple<NumberPtr, NumberPtr> roundtrip = Any(tuple).cast<Tuple<NumberPtr,
NumberPtr>>();
+ EXPECT_TRUE(roundtrip.same_as(tuple));
+ EXPECT_EQ(roundtrip.get<0>(), first);
+ EXPECT_EQ(roundtrip.get<1>(), second);
+}
+
+TEST(ObjectPtrStorage, Variant) {
+ using NumberOrInt = Variant<NumberPtr, int64_t>;
+ NumberPtr first = MakeInt(1);
+ NumberPtr null;
+
+ NumberOrInt variant = first;
+ EXPECT_EQ(variant.get<NumberPtr>(), first);
+ NumberOrInt roundtrip = Any(variant).cast<NumberOrInt>();
+ EXPECT_EQ(roundtrip.get<NumberPtr>(), first);
+
+ variant = int64_t{2};
+ EXPECT_EQ(variant.get<int64_t>(), 2);
+ variant = null;
+ EXPECT_EQ(variant.get<NumberPtr>(), nullptr);
+}
+
+TEST(ObjectPtrStorage, OptionalAndVariantComposition) {
+ using OptionalNumber = Optional<NumberPtr>;
+ using NestedOptionalNumber = Optional<OptionalNumber>;
+ using OptionalNumberOrInt = Variant<OptionalNumber, int64_t>;
+ using OptionalNumberOrIntValue = Optional<Variant<NumberPtr, int64_t>>;
+
+ NumberPtr number = MakeInt(1);
+ OptionalNumber optional_number = number;
+ Array<OptionalNumber> array{optional_number, std::nullopt};
+ EXPECT_TRUE(array[0].has_value());
+ EXPECT_EQ(array[0].value(), number);
+ EXPECT_FALSE(array[1].has_value());
+
+ OptionalNumberOrInt variant = optional_number;
+ OptionalNumber variant_value = variant.get<OptionalNumber>();
+ ASSERT_TRUE(variant_value.has_value());
+ EXPECT_EQ(variant_value.value(), number);
+
+ OptionalNumberOrIntValue optional_variant = Variant<NumberPtr,
int64_t>(number);
+ Any encoded = optional_variant;
+ OptionalNumberOrIntValue decoded = encoded.cast<OptionalNumberOrIntValue>();
+ ASSERT_TRUE(decoded.has_value());
+ EXPECT_EQ(decoded.value().get<NumberPtr>(), number);
+
+ OptionalNumber absent = std::nullopt;
+ OptionalNumber present_null = NumberPtr();
+ EXPECT_FALSE(Any(absent).cast<OptionalNumber>().has_value());
+ EXPECT_FALSE(Any(present_null).cast<OptionalNumber>().has_value());
+
+ NestedOptionalNumber nested = optional_number;
+ NestedOptionalNumber nested_roundtrip =
Any(nested).cast<NestedOptionalNumber>();
+ ASSERT_TRUE(nested_roundtrip.has_value());
+ ASSERT_TRUE(nested_roundtrip.value().has_value());
+ EXPECT_EQ(nested_roundtrip.value().value(), number);
+
+ NestedOptionalNumber present_absent = OptionalNumber(std::nullopt);
+ EXPECT_FALSE(Any(present_absent).cast<NestedOptionalNumber>().has_value());
+
+ EXPECT_EQ(TypeTraits<OptionalNumber>::TypeSchema(),
+
R"({"type":"Optional","args":[{"type":"Optional","args":[{"type":"test.Number"}]}]})");
+ EXPECT_EQ(
+ TypeTraits<OptionalNumberOrInt>::TypeSchema(),
+
R"({"type":"Variant","args":[{"type":"Optional","args":[{"type":"Optional","args":[{"type":"test.Number"}]}]},{"type":"int"}]})");
+}
+
+TEST(ObjectPtrStorage, Expected) {
+ NumberPtr number = MakeInt(1);
+ Expected<NumberPtr> success = number;
+ EXPECT_TRUE(success.is_ok());
+ EXPECT_EQ(success.value(), number);
+
+ Expected<NumberPtr> success_roundtrip =
Any(success).cast<Expected<NumberPtr>>();
+ EXPECT_TRUE(success_roundtrip.is_ok());
+ EXPECT_EQ(success_roundtrip.value(), number);
+
+ Expected<NumberPtr> failure = Error("ValueError", "expected failure", "");
+ Expected<NumberPtr> failure_roundtrip =
Any(failure).cast<Expected<NumberPtr>>();
+ EXPECT_TRUE(failure_roundtrip.is_err());
+ EXPECT_EQ(failure_roundtrip.error().kind(), "ValueError");
+}
+
} // namespace
diff --git a/tests/cpp/test_reflection.cc b/tests/cpp/test_reflection.cc
index d0930eba..711c9faf 100644
--- a/tests/cpp/test_reflection.cc
+++ b/tests/cpp/test_reflection.cc
@@ -20,6 +20,7 @@
#include <gtest/gtest.h>
#include <tvm/ffi/container/array.h>
#include <tvm/ffi/container/map.h>
+#include <tvm/ffi/extra/json.h>
#include <tvm/ffi/object.h>
#include <tvm/ffi/reflection/access_path.h>
#include <tvm/ffi/reflection/accessor.h>
@@ -27,6 +28,8 @@
#include <tvm/ffi/reflection/registry.h>
#include <tvm/ffi/string.h>
+#include <string_view>
+
#include "./testing_object.h"
/*! \brief Look up __ffi_init__ from the TypeAttrColumn (not the method
table). */
@@ -67,6 +70,7 @@ TVM_FFI_STATIC_INIT_BLOCK() {
TPrimExprObj::RegisterReflection();
TVarObj::RegisterReflection();
TPairObj::RegisterReflection();
+ TObjectPtrHolderObj::RegisterReflection();
TVarWithDepObj::RegisterReflection();
TDefHolderObj::RegisterReflection();
TFuncObj::RegisterReflection();
@@ -117,6 +121,83 @@ TEST(Reflection, FieldSetter) {
EXPECT_EQ(a.as<TFloatObj>()->value, 20.0);
}
+TEST(Reflection, ObjectPtrField) {
+ ObjectPtr<TIntObj> initial = make_object<TIntObj>(10);
+ TIntObj* initial_raw = initial.get();
+ TObjectPtrHolder holder(initial);
+ EXPECT_EQ(initial.use_count(), 2);
+
+ reflection::FieldGetter getter("test.ObjectPtrHolder", "value");
+ Any value = getter(holder);
+ EXPECT_EQ(initial.use_count(), 3);
+ ObjectPtr<TIntObj> reflected = value.cast<ObjectPtr<TIntObj>>();
+ EXPECT_EQ(initial.use_count(), 4);
+ EXPECT_EQ(reflected.get(), initial_raw);
+ reflected.reset();
+ value.reset();
+ EXPECT_EQ(initial.use_count(), 2);
+
+ reflection::FieldSetter setter("test.ObjectPtrHolder", "value");
+ ObjectPtr<TIntObj> replacement = make_object<TIntObj>(20);
+ setter(holder, replacement);
+ EXPECT_EQ(replacement.use_count(), 2);
+ EXPECT_EQ(holder->value.get(), replacement.get());
+ EXPECT_EQ(initial.use_count(), 1);
+
+ ObjectPtr<TFloatObj> incompatible = make_object<TFloatObj>(2.5);
+ EXPECT_THROW(setter(holder, incompatible), Error);
+ EXPECT_EQ(holder->value.get(), replacement.get());
+ EXPECT_EQ(incompatible.use_count(), 1);
+
+ ObjectPtr<TIntObj> null_value;
+ setter(holder, null_value);
+ EXPECT_EQ(holder->value, nullptr);
+ EXPECT_EQ(replacement.use_count(), 1);
+
+ EXPECT_THROW(setter(holder, String("not a number")), Error);
+ EXPECT_EQ(holder->value, nullptr);
+}
+
+TEST(Reflection, ObjectPtrFieldInfo) {
+ const TVMFFIFieldInfo* info =
reflection::GetFieldInfo("test.ObjectPtrHolder", "value");
+ EXPECT_EQ(info->field_static_type_index, TypeIndex::kTVMFFIObject);
+ EXPECT_EQ(info->size, sizeof(ObjectPtr<TIntObj>));
+ EXPECT_EQ(info->alignment, alignof(ObjectPtr<TIntObj>));
+ Map<String, Any> metadata =
json::Parse(String(info->metadata)).cast<Map<String, Any>>();
+ EXPECT_EQ(metadata["type_schema"].cast<String>(),
+ R"({"type":"Optional","args":[{"type":"test.Int"}]})");
+
+ const TVMFFIFieldInfo* alias_info =
reflection::GetFieldInfo("test.ObjectPtrHolder", "alias");
+ Map<String, Any> alias_metadata =
+ json::Parse(String(alias_info->metadata)).cast<Map<String, Any>>();
+ EXPECT_EQ(alias_metadata["type_schema"].cast<String>(),
+ R"({"type":"Optional","args":[{"type":"test.Number"}]})");
+}
+
+TEST(Reflection, ObjectPtrMethod) {
+ Function identity = reflection::GetMethod("test.ObjectPtrHolder",
"identity");
+ ObjectPtr<TIntObj> input = make_object<TIntObj>(21);
+ TIntObj* raw_input = input.get();
+ Any result = identity(input);
+ EXPECT_EQ(input.use_count(), 2);
+
+ ObjectPtr<TIntObj> output = std::move(result).cast<ObjectPtr<TIntObj>>();
+ EXPECT_EQ(result, nullptr); // NOLINT(bugprone-use-after-move)
+ EXPECT_EQ(output.get(), raw_input);
+ EXPECT_EQ(input.use_count(), 2);
+
+ Any null_result = identity(ObjectPtr<TIntObj>());
+ EXPECT_EQ(null_result, nullptr);
+ EXPECT_THROW(identity(make_object<TFloatObj>(2.5)), Error);
+ EXPECT_THROW(identity(String("not a number")), Error);
+
+ const TVMFFIMethodInfo* info =
reflection::GetMethodInfo("test.ObjectPtrHolder", "identity");
+ Map<String, Any> metadata =
json::Parse(String(info->metadata)).cast<Map<String, Any>>();
+ EXPECT_EQ(
+ metadata["type_schema"].cast<String>(),
+
R"({"type":"ffi.Function","args":[{"type":"Optional","args":[{"type":"test.Int"}]},{"type":"Optional","args":[{"type":"test.Int"}]}]})");
+}
+
TEST(Reflection, FieldInfo) {
const TVMFFIFieldInfo* info_int = reflection::GetFieldInfo("test.Int",
"value");
EXPECT_FALSE(info_int->flags & kTVMFFIFieldFlagBitMaskHasDefault);
diff --git a/tests/cpp/testing_object.h b/tests/cpp/testing_object.h
index 1c0d40c0..c2bc8e0a 100644
--- a/tests/cpp/testing_object.h
+++ b/tests/cpp/testing_object.h
@@ -234,6 +234,38 @@ class TPair : public ObjectRef {
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(TPair, ObjectRef, TPairObj);
};
+class TObjectPtrHolderObj : public Object {
+ public:
+ ObjectPtr<TIntObj> value;
+ ObjectPtr<TNumberObj> alias;
+
+ TObjectPtrHolderObj(ObjectPtr<TIntObj> value, ObjectPtr<TNumberObj> alias)
+ : value(std::move(value)), alias(std::move(alias)) {}
+ explicit TObjectPtrHolderObj(UnsafeInit) {}
+
+ static ObjectPtr<TIntObj> Identity(ObjectPtr<TIntObj> value) { return value;
}
+
+ static void RegisterReflection() {
+ namespace refl = tvm::ffi::reflection;
+ refl::ObjectDef<TObjectPtrHolderObj>()
+ .def_rw("value", &TObjectPtrHolderObj::value)
+ .def_rw("alias", &TObjectPtrHolderObj::alias)
+ .def_static("identity", &TObjectPtrHolderObj::Identity);
+ }
+
+ static constexpr bool _type_mutable = true;
+ TVM_FFI_DECLARE_OBJECT_INFO_FINAL("test.ObjectPtrHolder",
TObjectPtrHolderObj, Object);
+};
+
+class TObjectPtrHolder : public ObjectRef {
+ public:
+ TObjectPtrHolder(ObjectPtr<TIntObj> value, ObjectPtr<TNumberObj> alias =
nullptr) {
+ data_ = make_object<TObjectPtrHolderObj>(std::move(value),
std::move(alias));
+ }
+
+ TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(TObjectPtrHolder, ObjectRef,
TObjectPtrHolderObj);
+};
+
// FreeVar test object that has a sub-field referencing another FreeVar.
// This models the "var with nested vars" case (analogous to a relax::Var
// whose struct_info contains tir shape vars). It is used to exercise the
diff --git a/tests/python/test_dataclass_py_class.py
b/tests/python/test_dataclass_py_class.py
index 1f14779c..6b9bbdd4 100644
--- a/tests/python/test_dataclass_py_class.py
+++ b/tests/python/test_dataclass_py_class.py
@@ -46,7 +46,15 @@ from tvm_ffi.dataclasses import (
py_class,
)
from tvm_ffi.registry import _add_class_attrs
-from tvm_ffi.testing import TestObjectBase as _TestObjectBase
+from tvm_ffi.testing import (
+ TestObjectBase as _TestObjectBase,
+)
+from tvm_ffi.testing import (
+ TestObjectDerived as _TestObjectDerived,
+)
+from tvm_ffi.testing import (
+ TestObjectPtrHolder as _TestObjectPtrHolder,
+)
from tvm_ffi.testing.testing import requires_py310
# ---------------------------------------------------------------------------
@@ -3649,6 +3657,89 @@ class TestNativeParentInheritance:
assert obj_copy.v_f64 == 2.0
assert obj_copy.v_str == "x"
+ def test_object_ptr_parent_field_access_and_assignment(self) -> None:
+ use_count = tvm_ffi.get_global_func("testing.object_use_count")
+ target = _TestObjectBase()
+ replacement = _TestObjectBase(v_i64=20)
+ assert use_count(target) == 1
+ assert use_count(replacement) == 1
+
+ holder = _TestObjectPtrHolder(target)
+ assert use_count(target) == 2
+ stored = holder.value
+ assert stored is not None
+ assert stored.same_as(target)
+ assert use_count(target) == 3
+ del stored
+ gc.collect()
+ assert use_count(target) == 2
+
+ holder.value = None
+ assert holder.value is None
+ assert use_count(target) == 1
+
+ holder.value = replacement
+ assert use_count(replacement) == 2
+ holder.value = target
+ assert use_count(target) == 2
+ assert use_count(replacement) == 1
+
+ unrelated = _TestObjectPtrHolder(None)
+ with pytest.raises(TypeError):
+ holder.value = unrelated # ty: ignore[invalid-assignment]
+ assert holder.value is not None
+ assert holder.value.same_as(target)
+
+ del holder
+ gc.collect()
+ assert use_count(target) == 1
+
+ def test_object_ptr_parent_field_copy_and_destruction(self) -> None:
+ @py_class(_unique_key("ObjectPtrNativeParent"))
+ class Child(_TestObjectPtrHolder):
+ extra: int
+
+ use_count = tvm_ffi.get_global_func("testing.object_use_count")
+ target = _TestObjectDerived(
+ v_map={"answer": 42},
+ v_array=[1, "two"],
+ v_i64=7,
+ v_f64=2.0,
+ v_str="target",
+ )
+ child = Child(value=target, extra=3)
+ assert use_count(target) == 2
+
+ child_copy = copy.copy(child)
+ assert child_copy.extra == 3
+ assert use_count(target) == 3
+ copied_value = child_copy.value
+ assert copied_value is not None
+ assert copied_value.same_as(target)
+ del copied_value
+
+ del child_copy
+ gc.collect()
+ assert use_count(target) == 2
+
+ child_deepcopy = copy.deepcopy(child)
+ deepcopied_value = child_deepcopy.value
+ assert isinstance(deepcopied_value, _TestObjectDerived)
+ assert not deepcopied_value.same_as(target)
+ assert deepcopied_value.v_i64 == 7
+ assert deepcopied_value.v_f64 == 2.0
+ assert deepcopied_value.v_str == "target"
+ assert deepcopied_value.v_map["answer"] == 42
+ assert tuple(deepcopied_value.v_array) == (1, "two")
+ del deepcopied_value
+ del child_deepcopy
+ gc.collect()
+ assert use_count(target) == 2
+
+ del child
+ gc.collect()
+ assert use_count(target) == 1
+
# ###########################################################################
# 16. Deep Copy
diff --git a/tests/python/test_metadata.py b/tests/python/test_metadata.py
index 24883c04..5fe09d16 100644
--- a/tests/python/test_metadata.py
+++ b/tests/python/test_metadata.py
@@ -19,7 +19,7 @@ from typing import Any
import pytest
from tvm_ffi import get_global_func_metadata, register_global_func,
remove_global_func
from tvm_ffi.core import TypeInfo, TypeSchema, _lookup_type_attr
-from tvm_ffi.testing import _SchemaAllTypes
+from tvm_ffi.testing import TestObjectPtrHolder, _SchemaAllTypes
def _replace_container_types(ty: str) -> str:
@@ -128,6 +128,16 @@ def test_schema_field(field_name: str, expected: str) ->
None:
raise ValueError(f"Field not found: {field_name}")
+def test_schema_object_ptr_field() -> None:
+ type_info: TypeInfo = getattr(TestObjectPtrHolder, "__tvm_ffi_type_info__")
+ assert len(type_info.fields) == 1
+ assert type_info.fields[0].name == "value"
+ assert (
+
str(TypeSchema.from_json_str(type_info.fields[0].metadata["type_schema"]))
+ == "testing.TestObjectBase | None"
+ )
+
+
@pytest.mark.parametrize(
"method_name,expected",
[