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",
     [


Reply via email to