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 503b098f feat(abi): introduce Arc for required object values (#676)
503b098f is described below

commit 503b098fcc66632600112b7f3d36abac4a48f5aa
Author: Junru Shao <[email protected]>
AuthorDate: Tue Jul 21 10:48:32 2026 +0800

    feat(abi): introduce Arc for required object values (#676)
    
    Add `Arc<T>` and `make_arc<T>` as one-pointer, reference-counted
    carriers whose safe API does not admit null. Give Arc bare object
    schemas while representing nullable `ObjectPtr<T>` as `Optional[T]`, and
    reject empty or moved-from Arc values at FFI boundaries.
    
    Teach reflected fields, typed containers, and C++ ABI generation to
    preserve object nullability. Required objects and object-only unions use
    Arc, nullable pointer fields use ObjectPtr, and 16-byte optionals use
    `Optional<Arc<T>>`.
    
    Document the ownership and unsafe-initialization rules, preserve
    container exception safety when conversion fails, and extend C++ and
    Python coverage for casts, reflection, structural operations,
    serialization, and generated layouts.
---
 docs/guides/cpp_lang_guide.md              |  49 ++++++----
 include/tvm/ffi/container/array.h          |   6 +-
 include/tvm/ffi/container/list.h           |   6 +-
 include/tvm/ffi/memory.h                   |  11 +++
 include/tvm/ffi/object.h                   | 141 +++++++++++++++++++++++++++++
 python/tvm_ffi/dataclasses/gen_abi_cpp.py  |  79 ++++++++++------
 python/tvm_ffi/testing/testing.py          |   9 +-
 src/ffi/testing/testing.cc                 |   9 +-
 tests/cpp/extra/test_serialization.cc      |  19 ++--
 tests/cpp/extra/test_structural_visit.cc   |   4 +-
 tests/cpp/test_abi_object.cc               |  16 +++-
 tests/cpp/test_object.cc                   |  46 ++++++++++
 tests/cpp/test_object_ptr.cc               | 128 ++++++++++++++++++++++++++
 tests/cpp/test_reflection.cc               |  82 +++++++++++------
 tests/cpp/testing_object.h                 |  23 +++--
 tests/python/test_dataclass_gen_abi_cpp.py | 138 +++++++++++++++++++++++-----
 tests/python/test_dataclass_py_class.py    |  25 ++++-
 tests/python/test_metadata.py              |   7 +-
 18 files changed, 660 insertions(+), 138 deletions(-)

diff --git a/docs/guides/cpp_lang_guide.md b/docs/guides/cpp_lang_guide.md
index 0b5733a8..d964421c 100644
--- a/docs/guides/cpp_lang_guide.md
+++ b/docs/guides/cpp_lang_guide.md
@@ -88,7 +88,7 @@ counting correctly when the stored value is an on-heap object.
 The tvm-ffi object system provides the foundation for all managed, 
reference-counted objects
 in the system. It enables type safety, cross-language compatibility, and 
efficient memory management.
 
-The object system is built around three key classes: Object, ObjectPtr, and 
ObjectRef.
+The object system is built around four key classes: `Object`, `ObjectPtr`, 
`Arc`, and `ObjectRef`.
 The `Object` class is the base class of all heap-allocated objects. It 
contains a common header
 that includes the `type_index`, reference counter and deleter for the object.
 Users do not need to explicitly manage these fields as part of the C++ API. 
Instead,
@@ -115,24 +115,40 @@ class MyIntPairObj : public tvm::ffi::Object {
 
 void ExampleObjectPtr() {
   namespace ffi = tvm::ffi;
-  // make_object automatically sets up the deleter correctly
-  // This function creates a new ObjectPtr with proper memory management
-  // It handles allocation, initialization, and sets up the reference counting 
system
-  ffi::ObjectPtr<MyIntPairObj> obj = ffi::make_object<MyIntPairObj>(100, 200);
+  // make_arc allocates, initializes, and sets up reference counting and the 
deleter.
+  ffi::Arc<MyIntPairObj> required = ffi::make_arc<MyIntPairObj>(100, 200);
+  // ObjectPtr is the nullable one-pointer carrier.
+  ffi::ObjectPtr<MyIntPairObj> optional = required;
   // EXPECT_EQ is used here for demonstration purposes (testing framework)
-  EXPECT_EQ(obj->a, 100);
-  EXPECT_EQ(obj->b, 200);
+  EXPECT_EQ(required->a, 100);
+  EXPECT_EQ(required->b, 200);
+  optional = nullptr;
 }
 ```
 
-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.
+For an unqualified `Object` subclass `T`, use `Arc<T>` when the object is 
required and
+`ObjectPtr<T>` when it may be absent. Both are owning pointers with the same 
one-pointer layout and
+reference-counting behavior. Copying through `Any` retains the object, while 
moving transfers its
+reference. They can be used in typed containers such as `Array`, `List`, 
`Map`, `Dict`, `Tuple`,
+`Optional`, `Variant`, and `Expected`, as well as reflected fields and 
function signatures.
+
+`Arc<T>` has no default or `nullptr` constructor, and `make_arc<T>(args...)` 
is its normal creation
+API. Its type schema is the bare object schema `{"type":"T"}`, so ABI `None` 
is rejected. By
+contrast, `ObjectPtr<T>` remains mechanically nullable, maps a null pointer to 
ABI `None`, and has
+schema `Optional[T]`. `Optional<Arc<T>>` is also supported and produces 
exactly `Optional[T]`; use
+it when the native field needs the 16-byte `Optional` carrier rather than the 
8-byte
+`ObjectPtr<T>` carrier.
+
+Normal C++ move semantics leave a moved-from `Arc` empty. `Arc(UnsafeInit{})` 
also creates an empty
+value for reflection's controlled construct-then-populate path. These states 
are exceptions to the
+safe API's non-null guarantee and must be populated before use; Arc operations 
rely on the invariant
+without runtime validation. A reflected class with an `Arc` field should 
initialize that field
+explicitly in its unsafe constructor, for example
+`HolderObj(UnsafeInit) : required(UnsafeInit{}) {}`. Because `Arc` publicly 
inherits `ObjectPtr`,
+deliberately mutating it through the base class can likewise bypass the 
guarantee and is unsafe.
+
+Qualified pointee types such as `Arc<const T>`, `ObjectPtr<const T>`, volatile 
pointees, and
+reference pointee types are not supported.
 
 We typically provide a reference class that wraps the ObjectPtr.
 The `ObjectRef` base class provides the interface and reference counting
@@ -169,7 +185,8 @@ The ObjectRef acts as a smart pointer wrapper that 
automatically manages the Obj
 The overall implementation pattern is as follows:
 
 - **Object Class**: Inherits from `ffi::Object`, stores data and implements 
the core functionality.
-- **ObjectPtr**: Smart pointer that manages the Object lifecycle and reference 
counting.
+- **Arc**: Non-null owning pointer used for required object values.
+- **ObjectPtr**: Nullable owning pointer used when the object may be absent.
 - **Ref Class**: Inherits from `ffi::ObjectRef`, provides a user-friendly 
interface and automatic memory management.
 
 This design ensures efficient memory management while providing a clean API 
for users. Once we define an ObjectRef class,
diff --git a/include/tvm/ffi/container/array.h 
b/include/tvm/ffi/container/array.h
index fd2c8233..c2da576c 100644
--- a/include/tvm/ffi/container/array.h
+++ b/include/tvm/ffi/container/array.h
@@ -425,7 +425,8 @@ class Array : public ObjectRef {
    */
   void push_back(const T& item) {
     ArrayObj* p = CopyOnWrite(1);
-    p->EmplaceInit(p->TVMFFISeqCell::size++, item);
+    p->EmplaceInit(p->TVMFFISeqCell::size, item);
+    ++p->TVMFFISeqCell::size;
   }
 
   /*!
@@ -435,7 +436,8 @@ class Array : public ObjectRef {
   template <typename... Args>
   void emplace_back(Args&&... args) {
     ArrayObj* p = CopyOnWrite(1);
-    p->EmplaceInit(p->TVMFFISeqCell::size++, std::forward<Args>(args)...);
+    p->EmplaceInit(p->TVMFFISeqCell::size, std::forward<Args>(args)...);
+    ++p->TVMFFISeqCell::size;
   }
 
   /*!
diff --git a/include/tvm/ffi/container/list.h b/include/tvm/ffi/container/list.h
index be2bda81..bf865664 100644
--- a/include/tvm/ffi/container/list.h
+++ b/include/tvm/ffi/container/list.h
@@ -323,7 +323,8 @@ class List : public ObjectRef {
    */
   void push_back(const T& item) {
     ListObj* p = EnsureCapacity(1);
-    p->EmplaceInit(p->TVMFFISeqCell::size++, item);
+    p->EmplaceInit(p->TVMFFISeqCell::size, item);
+    ++p->TVMFFISeqCell::size;
   }
 
   /*!
@@ -333,7 +334,8 @@ class List : public ObjectRef {
   template <typename... Args>
   void emplace_back(Args&&... args) {
     ListObj* p = EnsureCapacity(1);
-    p->EmplaceInit(p->TVMFFISeqCell::size++, std::forward<Args>(args)...);
+    p->EmplaceInit(p->TVMFFISeqCell::size, std::forward<Args>(args)...);
+    ++p->TVMFFISeqCell::size;
   }
 
   /*!
diff --git a/include/tvm/ffi/memory.h b/include/tvm/ffi/memory.h
index 3b02c5b7..c4e07841 100644
--- a/include/tvm/ffi/memory.h
+++ b/include/tvm/ffi/memory.h
@@ -296,6 +296,17 @@ inline ObjectPtr<T> make_object(Args&&... args) {
   return 
details::SimpleObjAllocator().make_object<T>(std::forward<Args>(args)...);
 }
 
+/*!
+ * \brief Allocate an object and return a non-null owning pointer.
+ * \param args Arguments to the constructor.
+ * \tparam T The node type.
+ * \return An Arc owning the allocated object.
+ */
+template <typename T, typename... Args>
+inline Arc<T> make_arc(Args&&... args) {
+  return 
details::ObjectUnsafe::ArcFromObjectPtr(make_object<T>(std::forward<Args>(args)...));
+}
+
 /*!
  * \brief Allocate an Object with additional ElemType[num_elems] that are 
stored right after.
  * \param num_elems The number of elements in the array.
diff --git a/include/tvm/ffi/object.h b/include/tvm/ffi/object.h
index c945b5b1..25ee7699 100644
--- a/include/tvm/ffi/object.h
+++ b/include/tvm/ffi/object.h
@@ -529,6 +529,70 @@ class ObjectPtr {
   friend struct tvm::ffi::details::ObjectUnsafe;
 };
 
+/*!
+ * \brief A non-null owning pointer for Object.
+ * \tparam T The Object subclass being owned.
+ * \sa make_arc
+ *
+ * Arc has the same layout as ObjectPtr and supports the same copy, move, and
+ * derived-to-base conversions. Unlike ObjectPtr, its safe public API cannot
+ * create a null value. A moved-from Arc and an Arc explicitly constructed with
+ * UnsafeInit may be null; such values must be populated before use. Operations
+ * on Arc rely on the non-null invariant and do not validate it at runtime.
+ */
+template <typename T>
+class Arc : public ObjectPtr<T> {
+ public:
+  Arc() = delete;
+  Arc(std::nullptr_t) = delete;
+
+  /*! \brief Copy constructor. */
+  Arc(const Arc&) = default;
+
+  /*! \brief Move constructor. */
+  Arc(Arc&&) = default;
+
+  /*! \brief Copy assignment operator. */
+  Arc& operator=(const Arc&) = default;
+
+  /*! \brief Move assignment operator. */
+  Arc& operator=(Arc&&) = default;
+
+  /*!
+   * \brief Copy-upcast an Arc of a derived Object type.
+   * \param other The Arc to copy.
+   */
+  template <typename U, std::enable_if_t<std::is_base_of_v<T, U>, int> = 0>
+  Arc(const Arc<U>& other)  // NOLINT(*)
+      : ObjectPtr<T>(static_cast<const ObjectPtr<U>&>(other)) {}
+
+  /*!
+   * \brief Move-upcast an Arc of a derived Object type.
+   * \param other The Arc to move.
+   */
+  template <typename U, std::enable_if_t<std::is_base_of_v<T, U>, int> = 0>
+  Arc(Arc<U>&& other)  // NOLINT(*)
+      : ObjectPtr<T>(std::move(static_cast<ObjectPtr<U>&>(other))) {}
+
+  /*!
+   * \brief Construct an empty Arc for controlled construct-then-populate 
flows.
+   * \param tag The unsafe initialization tag.
+   */
+  explicit Arc(UnsafeInit tag) : ObjectPtr<T>(nullptr) { 
static_cast<void>(tag); }
+
+ private:
+  explicit Arc(ObjectPtr<T> ptr) : ObjectPtr<T>(std::move(ptr)) {}
+
+  using ObjectPtr<T>::reset;
+  using ObjectPtr<T>::swap;
+
+  template <typename U, typename... Args>
+  friend Arc<U> make_arc(Args&&... args);
+  template <typename>
+  friend class Arc;
+  friend struct tvm::ffi::details::ObjectUnsafe;
+};
+
 /*!
  * \brief Whether T is Object or a subclass of Object.
  * \tparam T The type to inspect.
@@ -555,6 +619,24 @@ inline constexpr bool is_qualified_object_v =
 template <typename BaseObject, typename DerivedObject>
 inline constexpr bool type_subsumes_v<ObjectPtr<BaseObject>, 
ObjectPtr<DerivedObject>> =
     std::is_base_of_v<BaseObject, DerivedObject>;
+
+/*!
+ * \brief Whether target Arc storage subsumes source Arc storage.
+ * \tparam BaseObject The target Arc pointee type.
+ * \tparam DerivedObject The source Arc pointee type.
+ */
+template <typename BaseObject, typename DerivedObject>
+inline constexpr bool type_subsumes_v<Arc<BaseObject>, Arc<DerivedObject>> =
+    std::is_base_of_v<BaseObject, DerivedObject>;
+
+/*!
+ * \brief Whether nullable ObjectPtr storage subsumes non-null Arc storage.
+ * \tparam BaseObject The target ObjectPtr pointee type.
+ * \tparam DerivedObject The source Arc pointee type.
+ */
+template <typename BaseObject, typename DerivedObject>
+inline constexpr bool type_subsumes_v<ObjectPtr<BaseObject>, 
Arc<DerivedObject>> =
+    std::is_base_of_v<BaseObject, DerivedObject>;
 /// \endcond
 
 /*!
@@ -1256,6 +1338,11 @@ struct ObjectUnsafe {
     return ObjectPtrFromOwned<T>(reinterpret_cast<Object*>(obj_ptr));
   }
 
+  template <typename T>
+  TVM_FFI_INLINE static Arc<T> ArcFromObjectPtr(ObjectPtr<T>&& ptr) {
+    return Arc<T>(std::move(ptr));
+  }
+
   template <typename T>
   TVM_FFI_INLINE static T* RawObjectPtrFromUnowned(TVMFFIObject* obj_ptr) {
     // NOTE: this is important to first cast to Object*
@@ -1324,6 +1411,13 @@ struct TypeToRuntimeTypeIndex<
   static int32_t v() { return TObject::RuntimeTypeIndex(); }
 };
 
+template <typename TObject>
+struct TypeToRuntimeTypeIndex<
+    Arc<TObject>, std::enable_if_t<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.
@@ -1388,6 +1482,53 @@ struct TypeTraits<ObjectPtr<TObject>,
   }
 };
 
+/*!
+ * \brief Type traits for a non-null owning pointer to an unqualified Object 
subclass.
+ * \tparam TObject The unqualified Object subclass.
+ */
+template <typename TObject>
+struct TypeTraits<Arc<TObject>,
+                  std::enable_if_t<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 Arc<TObject>& src, TVMFFIAny* 
result) {
+    TypeTraits<ObjectPtr<TObject>>::CopyToAnyView(src, result);
+  }
+
+  TVM_FFI_INLINE static void MoveToAny(Arc<TObject> src, TVMFFIAny* result) {
+    TypeTraits<ObjectPtr<TObject>>::MoveToAny(std::move(src), result);
+  }
+
+  TVM_FFI_INLINE static bool CheckAnyStrict(const TVMFFIAny* src) {
+    return src->type_index >= TypeIndex::kTVMFFIStaticObjectBegin &&
+           details::IsObjectInstance<TObject>(src->type_index);
+  }
+
+  TVM_FFI_INLINE static Arc<TObject> CopyFromAnyViewAfterCheck(const 
TVMFFIAny* src) {
+    return details::ObjectUnsafe::ArcFromObjectPtr(
+        details::ObjectUnsafe::ObjectPtrFromUnowned<TObject>(src->v_obj));
+  }
+
+  TVM_FFI_INLINE static Arc<TObject> MoveFromAnyAfterCheck(TVMFFIAny* src) {
+    Arc<TObject> result = details::ObjectUnsafe::ArcFromObjectPtr(
+        details::ObjectUnsafe::ObjectPtrFromOwned<TObject>(src->v_obj));
+    TypeTraits<std::nullptr_t>::MoveToAny(nullptr, src);
+    return result;
+  }
+
+  TVM_FFI_INLINE static std::optional<Arc<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":")" + 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/dataclasses/gen_abi_cpp.py 
b/python/tvm_ffi/dataclasses/gen_abi_cpp.py
index b7db91b2..6ec58cff 100644
--- a/python/tvm_ffi/dataclasses/gen_abi_cpp.py
+++ b/python/tvm_ffi/dataclasses/gen_abi_cpp.py
@@ -316,22 +316,37 @@ class _Generator:
         self.builtins = _builtin_table()
         self.dependencies: dict[int, TypeInfo] = {info.type_index: info for 
info in emitted_infos}
 
+    def _get_dynamic_object_cpp_type(self, schema: TypeSchema) -> str:
+        if schema.origin_type_index < _DYNAMIC_OBJECT_TYPE_INDEX_BEGIN:
+            raise ValueError(f"Schema {schema!r} is not a registered object 
type")
+        info = _lookup_or_register_type_info_from_type_key(schema.origin)
+        self.dependencies[info.type_index] = info
+        return _cpp_name(info.type_key).qualified
+
     def _lower_object(self, schema: TypeSchema) -> _Carrier:
         type_index = schema.origin_type_index
         if type_index == _OBJECT_TYPE_INDEX or schema.origin == "Object":
-            return _Carrier("::tvm::ffi::ObjectPtr<::tvm::ffi::Object>", 8, 8)
+            return _Carrier("::tvm::ffi::Arc<::tvm::ffi::Object>", 8, 8)
         builtin = self.builtins.get(schema.origin_type_index)
         if builtin is not None:
             if builtin.value_type is None:
                 raise ValueError(
                     f"Static TVM-FFI type {schema.origin!r} has no supported 
C++ value wrapper"
                 )
+            if builtin.value_type.startswith("::tvm::ffi::ObjectPtr<"):
+                return _Carrier(f"::tvm::ffi::Arc<{builtin.object_type}>", 8, 
8)
             return _Carrier(builtin.value_type, builtin.size, 
builtin.alignment)
-        if schema.origin_type_index < _DYNAMIC_OBJECT_TYPE_INDEX_BEGIN:
-            raise ValueError(f"Schema {schema!r} is not a registered object 
type")
-        info = _lookup_or_register_type_info_from_type_key(schema.origin)
-        self.dependencies[info.type_index] = info
-        object_type = _cpp_name(info.type_key).qualified
+        object_type = self._get_dynamic_object_cpp_type(schema)
+        return _Carrier(f"::tvm::ffi::Arc<{object_type}>", 8, 8)
+
+    def _lower_nullable_object(self, schema: TypeSchema) -> _Carrier:
+        type_index = schema.origin_type_index
+        if type_index == _OBJECT_TYPE_INDEX or schema.origin == "Object":
+            object_type = "::tvm::ffi::Object"
+        elif (builtin := self.builtins.get(type_index)) is not None:
+            object_type = builtin.object_type
+        else:
+            object_type = self._get_dynamic_object_cpp_type(schema)
         return _Carrier(f"::tvm::ffi::ObjectPtr<{object_type}>", 8, 8)
 
     def _lower_value(
@@ -359,18 +374,22 @@ class _Generator:
         if origin in scalar:
             return scalar[origin]
         args = schema.args
-        if (
-            origin == "Optional"
-            and is_native_field
-            and container_argument
-            and args[0].origin_type_index >= _OBJECT_TYPE_INDEX
-        ):
-            return self._lower_object(args[0])
         if origin in ("Optional", "Union"):
-            # Container elements are already stored as Any cells.  Keeping the
-            # view erased avoids depending on the unstable native wrapper
-            # representation while preserving the outer container layout.
-            return _Carrier("::tvm::ffi::Any", 16, 8)
+            carrier = _Carrier("::tvm::ffi::Any", 16, 8)
+            if (
+                origin == "Optional"
+                and container_argument
+                and args[0].origin_type_index >= _OBJECT_TYPE_INDEX
+            ):
+                carrier = self._lower_nullable_object(args[0])
+            elif (
+                origin == "Union"
+                and container_argument
+                and all(arg.origin_type_index >= _OBJECT_TYPE_INDEX for arg in 
args)
+            ):
+                carrier = _Carrier("::tvm::ffi::Arc<::tvm::ffi::Object>", 8, 8)
+            # Other structural elements keep their discriminated Any storage.
+            return carrier
 
         container_origins = {
             "Array": "::tvm::ffi::Array",
@@ -441,18 +460,24 @@ class _Generator:
                 elif value_schema.origin_type_index >= _OBJECT_TYPE_INDEX and (
                     field.size,
                     field.alignment,
+                ) == (
+                    16,
+                    8,
+                ):
+                    value_carrier = self._lower_object(value_schema)
+                    carrier = _Carrier(
+                        f"::tvm::ffi::Optional<{value_carrier.cpp_type}>",
+                        16,
+                        8,
+                    )
+                elif value_schema.origin_type_index >= _OBJECT_TYPE_INDEX and (
+                    field.size,
+                    field.alignment,
                 ) == (
                     8,
                     8,
                 ):
-                    carrier = self._lower_object(value_schema)
-                    if carrier.size != 8:
-                        builtin = 
self.builtins.get(value_schema.origin_type_index)
-                        if builtin is None:
-                            raise ValueError(
-                                f"Cannot prove pointer carrier for 
{owner.type_key}.{field.name}"
-                            )
-                        carrier = 
_Carrier(f"::tvm::ffi::ObjectPtr<{builtin.object_type}>", 8, 8)
+                    carrier = self._lower_nullable_object(value_schema)
                 else:
                     raise ValueError(
                         f"Ambiguous native Optional carrier for 
{owner.type_key}.{field.name} "
@@ -463,7 +488,7 @@ class _Generator:
                 and (field.size, field.alignment) == (8, 8)
                 and all(arg.origin_type_index >= _OBJECT_TYPE_INDEX for arg in 
schema.args)
             ):
-                carrier = 
_Carrier("::tvm::ffi::ObjectPtr<::tvm::ffi::Object>", 8, 8)
+                carrier = _Carrier("::tvm::ffi::Arc<::tvm::ffi::Object>", 8, 8)
             else:
                 raise ValueError(
                     f"Ambiguous native {schema.origin} carrier for 
{owner.type_key}.{field.name} "
@@ -493,7 +518,7 @@ class _Generator:
             # derives from std::exception) contain more than one pointer.
             # A reflected one-pointer field uses the canonical object class
             # instead of pretending that the larger wrapper is 
layout-compatible.
-            carrier = 
_Carrier(f"::tvm::ffi::ObjectPtr<{builtin.object_type}>", 8, 8)
+            carrier = _Carrier(f"::tvm::ffi::Arc<{builtin.object_type}>", 8, 8)
             expected = (8, 8)
         if actual != expected:
             raise ValueError(
diff --git a/python/tvm_ffi/testing/testing.py 
b/python/tvm_ffi/testing/testing.py
index e28af64c..b4797d67 100644
--- a/python/tvm_ffi/testing/testing.py
+++ b/python/tvm_ffi/testing/testing.py
@@ -128,16 +128,17 @@ class TestObjectDerived(TestObjectBase):
 
 @c_class("testing.TestObjectPtrHolder")
 class TestObjectPtrHolder(Object):
-    """Test object with an ``ObjectPtr<TestObjectBase>`` field."""
+    """Test object with ``Arc`` and nullable ``ObjectPtr`` fields."""
 
     __test__ = False
 
     # tvm-ffi-stubgen(begin): object/testing.TestObjectPtrHolder
     # fmt: off
-    value: TestObjectBase | None
+    value: TestObjectBase
+    optional_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]
+        def __init__(self, value: TestObjectBase, optional_value: 
TestObjectBase | None = ...) -> None: ...
+        def __ffi_init__(self, value: TestObjectBase, optional_value: 
TestObjectBase | None = ...) -> None: ...  # ty: ignore[invalid-method-override]
     # fmt: on
     # tvm-ffi-stubgen(end)
 
diff --git a/src/ffi/testing/testing.cc b/src/ffi/testing/testing.cc
index 7c9e47a4..21b87b46 100644
--- a/src/ffi/testing/testing.cc
+++ b/src/ffi/testing/testing.cc
@@ -171,9 +171,10 @@ class TestObjectDerived : public TestObjectBase {
 
 class TestObjectPtrHolder : public Object {
  public:
-  ObjectPtr<TestObjectBase> value;
+  Arc<TestObjectBase> value;
+  ObjectPtr<TestObjectBase> optional_value;
 
-  explicit TestObjectPtrHolder(UnsafeInit) {}
+  explicit TestObjectPtrHolder(UnsafeInit) : value(UnsafeInit{}) {}
 
   static constexpr bool _type_mutable = true;
   TVM_FFI_DECLARE_OBJECT_INFO("testing.TestObjectPtrHolder", 
TestObjectPtrHolder, Object);
@@ -481,7 +482,9 @@ 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<TestObjectPtrHolder>()
+      .def_rw("value", &TestObjectPtrHolder::value)
+      .def_rw("optional_value", &TestObjectPtrHolder::optional_value, 
refl::default_value(nullptr));
 
   refl::ObjectDef<TestCxxClassBase>()
       .def_rw("v_i64", &TestCxxClassBase::v_i64, refl::repr(false))
diff --git a/tests/cpp/extra/test_serialization.cc 
b/tests/cpp/extra/test_serialization.cc
index 65f7d414..7c30c7f3 100644
--- a/tests/cpp/extra/test_serialization.cc
+++ b/tests/cpp/extra/test_serialization.cc
@@ -757,9 +757,9 @@ 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;
+TEST(Serialization, ArcAndObjectPtrFields) {
+  tvm::ffi::Arc<TIntObj> shared = make_arc<TIntObj>(42);
+  tvm::ffi::Arc<TIntObj> value = shared;
   ObjectPtr<TNumberObj> alias = shared;
   TObjectPtrHolder holder(std::move(value), std::move(alias));
 
@@ -767,20 +767,21 @@ TEST(Serialization, ObjectPtrFields) {
   TObjectPtrHolder result = deserialized.cast<TObjectPtrHolder>();
 
   ASSERT_NE(result->value, nullptr);
-  ASSERT_NE(result->alias, nullptr);
-  EXPECT_EQ(result->value.get(), result->alias.get());
+  ASSERT_NE(result->optional_alias, nullptr);
+  EXPECT_EQ(result->value.get(), result->optional_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);
+TEST(Serialization, NullableObjectPtrFieldNone) {
+  TObjectPtrHolder holder(make_arc<TIntObj>(42), nullptr);
 
   Any deserialized = FromJSONGraph(ToJSONGraph(holder));
   TObjectPtrHolder result = deserialized.cast<TObjectPtrHolder>();
 
-  EXPECT_EQ(result->value, nullptr);
-  EXPECT_EQ(result->alias, nullptr);
+  ASSERT_NE(result->value, nullptr);
+  EXPECT_EQ(result->value->value, 42);
+  EXPECT_EQ(result->optional_alias, nullptr);
 }
 
 // ---------------------------------------------------------------------------
diff --git a/tests/cpp/extra/test_structural_visit.cc 
b/tests/cpp/extra/test_structural_visit.cc
index 1cc0a9df..0c65a298 100644
--- a/tests/cpp/extra/test_structural_visit.cc
+++ b/tests/cpp/extra/test_structural_visit.cc
@@ -345,8 +345,8 @@ TEST(StructuralVisitor, WalkVisitsObjectPtr) {
   ExpectTrace(visited, {"x"});
 }
 
-TEST(StructuralVisitor, WalkTraversesObjectPtrFields) {
-  ObjectPtr<TIntObj> value = make_object<TIntObj>(42);
+TEST(StructuralVisitor, WalkTraversesArcAndObjectPtrFields) {
+  tvm::ffi::Arc<TIntObj> value = make_arc<TIntObj>(42);
   ObjectPtr<TNumberObj> alias = value;
   TObjectPtrHolder root(value, alias);
   size_t num_int_fields = 0;
diff --git a/tests/cpp/test_abi_object.cc b/tests/cpp/test_abi_object.cc
index 78badca8..23eef686 100644
--- a/tests/cpp/test_abi_object.cc
+++ b/tests/cpp/test_abi_object.cc
@@ -31,14 +31,14 @@ struct UnrelatedObj;
 
 }  // namespace abi_object_test
 
-// RecursiveObj is incomplete here.  Specializing the trait enables its 
ObjectPtr storage traits
-// before List<ObjectPtr<RecursiveObj>> is instantiated inside the class 
definition below.
+// RecursiveObj is incomplete here. Specializing the trait enables its Arc 
storage traits before
+// List<Arc<RecursiveObj>> is instantiated inside the class definition below.
 template <>
 inline constexpr bool 
tvm::ffi::is_object_subclass_v<::abi_object_test::RecursiveObj> = true;
 
 
static_assert(::tvm::ffi::is_object_subclass_v<::abi_object_test::RecursiveObj>);
 static_assert(
-    
::tvm::ffi::details::storage_enabled_v<::tvm::ffi::ObjectPtr<::abi_object_test::RecursiveObj>>);
+    
::tvm::ffi::details::storage_enabled_v<::tvm::ffi::Arc<::abi_object_test::RecursiveObj>>);
 
 namespace abi_object_test {
 
@@ -55,7 +55,7 @@ struct GrandchildObj : public DerivedObj {
 };
 
 struct RecursiveObj : public ::tvm::ffi::Object {
-  ::tvm::ffi::List<::tvm::ffi::ObjectPtr<RecursiveObj>> children;
+  ::tvm::ffi::List<::tvm::ffi::Arc<RecursiveObj>> children;
 
   TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.abi_object.Recursive", 1);
 };
@@ -79,8 +79,16 @@ static_assert(
     std::is_constructible_v<::tvm::ffi::ObjectPtr<BaseObj>, 
::tvm::ffi::ObjectPtr<GrandchildObj>>);
 static_assert(
     std::is_assignable_v<::tvm::ffi::ObjectPtr<BaseObj>&, 
::tvm::ffi::ObjectPtr<DerivedObj>>);
+static_assert(std::is_constructible_v<::tvm::ffi::Arc<BaseObj>, 
::tvm::ffi::Arc<GrandchildObj>>);
+static_assert(std::is_assignable_v<::tvm::ffi::Arc<BaseObj>&, 
::tvm::ffi::Arc<DerivedObj>>);
 static_assert(::tvm::ffi::type_subsumes_v<::tvm::ffi::ObjectPtr<BaseObj>,
                                           
::tvm::ffi::ObjectPtr<GrandchildObj>>);
+static_assert(
+    ::tvm::ffi::type_subsumes_v<::tvm::ffi::Arc<BaseObj>, 
::tvm::ffi::Arc<GrandchildObj>>);
+static_assert(
+    ::tvm::ffi::type_subsumes_v<::tvm::ffi::ObjectPtr<BaseObj>, 
::tvm::ffi::Arc<GrandchildObj>>);
+static_assert(
+    !::tvm::ffi::type_subsumes_v<::tvm::ffi::Arc<BaseObj>, 
::tvm::ffi::ObjectPtr<GrandchildObj>>);
 static_assert(!::tvm::ffi::type_subsumes_v<::tvm::ffi::ObjectPtr<UnrelatedObj>,
                                            ::tvm::ffi::ObjectPtr<DerivedObj>>);
 
diff --git a/tests/cpp/test_object.cc b/tests/cpp/test_object.cc
index 70e453a9..b4e70359 100644
--- a/tests/cpp/test_object.cc
+++ b/tests/cpp/test_object.cc
@@ -680,4 +680,50 @@ TEST(ObjectPtrStorage, Expected) {
   EXPECT_EQ(failure_roundtrip.error().kind(), "ValueError");
 }
 
+using NumberArc = tvm::ffi::Arc<TNumberObj>;
+
+NumberArc MakeArcInt(int64_t value) { return make_arc<TIntObj>(value); }
+
+NumberArc MakeArcFloat(double value) { return make_arc<TFloatObj>(value); }
+
+TEST(ArcStorage, Containers) {
+  NumberArc first = MakeArcInt(1);
+  NumberArc second = MakeArcFloat(2.0);
+
+  Array<NumberArc> array{first};
+  array.push_back(second);
+  EXPECT_EQ(array[1], second);
+
+  List<NumberArc> list{first};
+  list.push_back(second);
+  EXPECT_EQ(list[1], second);
+
+  Map<String, NumberArc> map{{"value", first}};
+  map.Set("value", second);
+  EXPECT_EQ(map["value"], second);
+
+  Dict<String, NumberArc> dict{{"value", first}};
+  dict.Set("value", second);
+  EXPECT_EQ(dict["value"], second);
+
+  Tuple<NumberArc, NumberArc> tuple(first, first);
+  tuple.Set<1>(second);
+  EXPECT_EQ(tuple.get<1>(), second);
+
+  Variant<NumberArc, int64_t> variant = int64_t{3};
+  variant = first;
+  EXPECT_EQ(variant.get<NumberArc>(), first);
+
+  Expected<NumberArc> expected{first};
+  ASSERT_TRUE(expected.is_ok());
+  EXPECT_EQ(expected.value(), first);
+
+  EXPECT_EQ(TypeTraits<List<NumberArc>>::TypeSchema(),
+            R"({"type":"ffi.List","args":[{"type":"test.Number"}]})");
+  EXPECT_EQ((TypeTraits<Map<String, NumberArc>>::TypeSchema()),
+            
R"({"type":"ffi.Map","args":[{"type":"ffi.String"},{"type":"test.Number"}]})");
+  EXPECT_EQ((TypeTraits<Tuple<NumberArc, int64_t>>::TypeSchema()),
+            
R"({"type":"Tuple","args":[{"type":"test.Number"},{"type":"int"}]})");
+}
+
 }  // namespace
diff --git a/tests/cpp/test_object_ptr.cc b/tests/cpp/test_object_ptr.cc
index c8811bd1..6e87ccc0 100644
--- a/tests/cpp/test_object_ptr.cc
+++ b/tests/cpp/test_object_ptr.cc
@@ -114,14 +114,18 @@ class PointerAdjustedObj : public PointerAdjustmentPad, 
public CxxBaseObj {
 namespace {
 
 using tvm::ffi::Any;
+using tvm::ffi::AnyView;
+using tvm::ffi::Arc;
 using tvm::ffi::Array;
 using tvm::ffi::Dict;
 using tvm::ffi::List;
+using tvm::ffi::make_arc;
 using tvm::ffi::make_object;
 using tvm::ffi::Map;
 using tvm::ffi::Object;
 using tvm::ffi::ObjectPtr;
 using tvm::ffi::ObjectRef;
+using tvm::ffi::Optional;
 using tvm::ffi::String;
 using tvm::ffi::TypeTraits;
 using tvm::ffi::UnsafeInit;
@@ -134,6 +138,19 @@ using tvm::ffi::testing::MutualLeftObj;
 using tvm::ffi::testing::MutualRightObj;
 using tvm::ffi::testing::PointerAdjustedObj;
 
+template <typename T, typename = void>
+struct HasPublicReset : std::false_type {};
+
+template <typename T>
+struct HasPublicReset<T, std::void_t<decltype(std::declval<T&>().reset())>> : 
std::true_type {};
+
+template <typename T, typename = void>
+struct HasPublicSwap : std::false_type {};
+
+template <typename T>
+struct HasPublicSwap<T, 
std::void_t<decltype(std::declval<T&>().swap(std::declval<T&>()))>>
+    : std::true_type {};
+
 static_assert(tvm::ffi::is_object_subclass_v<GeneratedBaseObj>);
 static_assert(tvm::ffi::is_object_subclass_v<GeneratedDerivedObj>);
 static_assert(std::is_convertible_v<GeneratedDerivedObj*, GeneratedBaseObj*>);
@@ -150,6 +167,25 @@ static_assert(
     tvm::ffi::type_subsumes_v<ObjectPtr<GeneratedBaseObj>, 
ObjectPtr<GeneratedDerivedObj>>);
 static_assert(
     !tvm::ffi::type_subsumes_v<ObjectPtr<GeneratedDerivedObj>, 
ObjectPtr<GeneratedBaseObj>>);
+static_assert(sizeof(Arc<GeneratedDerivedObj>) == 
sizeof(ObjectPtr<GeneratedDerivedObj>));
+static_assert(alignof(Arc<GeneratedDerivedObj>) == 
alignof(ObjectPtr<GeneratedDerivedObj>));
+static_assert(std::is_standard_layout_v<Arc<GeneratedDerivedObj>>);
+static_assert(std::is_base_of_v<ObjectPtr<GeneratedDerivedObj>, 
Arc<GeneratedDerivedObj>>);
+static_assert(!std::is_default_constructible_v<Arc<GeneratedDerivedObj>>);
+static_assert(!std::is_constructible_v<Arc<GeneratedDerivedObj>, 
std::nullptr_t>);
+static_assert(std::is_constructible_v<Arc<GeneratedDerivedObj>, UnsafeInit>);
+static_assert(!std::is_constructible_v<Arc<GeneratedDerivedObj>, 
ObjectPtr<GeneratedDerivedObj>>);
+static_assert(!HasPublicReset<Arc<GeneratedDerivedObj>>::value);
+static_assert(!HasPublicSwap<Arc<GeneratedDerivedObj>>::value);
+static_assert(std::is_constructible_v<Arc<GeneratedBaseObj>, 
Arc<GeneratedDerivedObj>>);
+static_assert(std::is_assignable_v<Arc<GeneratedBaseObj>&, 
Arc<GeneratedDerivedObj>>);
+static_assert(std::is_constructible_v<ObjectPtr<GeneratedBaseObj>, 
Arc<GeneratedDerivedObj>>);
+static_assert(!TypeTraits<Arc<int>>::storage_enabled);
+static_assert(tvm::ffi::type_subsumes_v<Arc<GeneratedBaseObj>, 
Arc<GeneratedDerivedObj>>);
+static_assert(tvm::ffi::type_subsumes_v<ObjectPtr<GeneratedBaseObj>, 
Arc<GeneratedDerivedObj>>);
+static_assert(!tvm::ffi::type_subsumes_v<Arc<GeneratedBaseObj>, 
ObjectPtr<GeneratedDerivedObj>>);
+static_assert(
+    !std::is_constructible_v<Array<Arc<GeneratedBaseObj>>, 
Array<ObjectPtr<GeneratedDerivedObj>>>);
 
 static_assert(std::is_same_v<Array<ObjectPtr<GeneratedDerivedObj>>::value_type,
                              ObjectPtr<GeneratedDerivedObj>>);
@@ -247,6 +283,98 @@ TEST(ObjectPtr, AnyRoundTripUsesRuntimeAncestry) {
   EXPECT_EQ(null_value.cast<ObjectPtr<GeneratedBaseObj>>(), nullptr);
 }
 
+TEST(Arc, ConstructionOwnershipAndUpcast) {
+  Arc<GeneratedDerivedObj> derived = make_arc<GeneratedDerivedObj>(42);
+  EXPECT_EQ(derived->value, 42);
+  EXPECT_EQ(derived.use_count(), 1);
+
+  Arc<GeneratedDerivedObj> copied = derived;
+  EXPECT_EQ(derived.use_count(), 2);
+  EXPECT_EQ(copied.get(), derived.get());
+
+  Arc<GeneratedBaseObj> upcast = derived;
+  EXPECT_EQ(derived.use_count(), 3);
+  EXPECT_EQ(upcast.get(), static_cast<GeneratedBaseObj*>(derived.get()));
+
+  copied = make_arc<GeneratedDerivedObj>(45);
+  EXPECT_EQ(derived.use_count(), 2);
+  EXPECT_EQ(copied->value, 45);
+
+  Arc<GeneratedDerivedObj> move_source = make_arc<GeneratedDerivedObj>(43);
+  const void* move_source_address = move_source.get();
+  Arc<GeneratedBaseObj> moved = std::move(move_source);
+  EXPECT_EQ(move_source,  // 
NOLINT(bugprone-use-after-move,clang-analyzer-cplusplus.Move)
+            nullptr);
+  EXPECT_EQ(moved.use_count(), 1);
+  EXPECT_EQ(static_cast<const void*>(moved.get()), move_source_address);
+
+  Arc<GeneratedBaseObj> assigned = make_arc<GeneratedDerivedObj>(44);
+  assigned = derived;
+  EXPECT_EQ(assigned.get(), static_cast<GeneratedBaseObj*>(derived.get()));
+  EXPECT_EQ(derived.use_count(), 3);
+}
+
+TEST(Arc, AnyRoundTripAndSchemas) {
+  Arc<GeneratedDerivedObj> derived = make_arc<GeneratedDerivedObj>(7);
+  Any value = derived;
+  EXPECT_EQ(derived.use_count(), 2);
+
+  Arc<GeneratedBaseObj> base = value.cast<Arc<GeneratedBaseObj>>();
+  EXPECT_EQ(base.get(), static_cast<GeneratedBaseObj*>(derived.get()));
+  EXPECT_EQ(derived.use_count(), 3);
+  EXPECT_FALSE(value.try_cast<Arc<GeneratedUnrelatedObj>>().has_value());
+
+  Any none;
+  EXPECT_FALSE(none.as<Arc<GeneratedBaseObj>>().has_value());
+  EXPECT_FALSE(none.try_cast<Arc<GeneratedBaseObj>>().has_value());
+  EXPECT_THROW(none.cast<Arc<GeneratedBaseObj>>(), tvm::ffi::Error);
+
+  EXPECT_EQ(tvm::ffi::TypeToRuntimeTypeIndex<Arc<GeneratedDerivedObj>>::v(),
+            GeneratedDerivedObj::RuntimeTypeIndex());
+  EXPECT_EQ(TypeTraits<Arc<GeneratedBaseObj>>::TypeSchema(), 
R"({"type":"testing.GeneratedBase"})");
+  EXPECT_EQ(TypeTraits<ObjectPtr<GeneratedBaseObj>>::TypeSchema(),
+            
R"({"type":"Optional","args":[{"type":"testing.GeneratedBase"}]})");
+  EXPECT_EQ(TypeTraits<Optional<Arc<GeneratedBaseObj>>>::TypeSchema(),
+            
R"({"type":"Optional","args":[{"type":"testing.GeneratedBase"}]})");
+
+  Optional<Arc<GeneratedBaseObj>> present = Arc<GeneratedBaseObj>(derived);
+  ASSERT_TRUE(present.has_value());
+  EXPECT_EQ(present.value().get(), 
static_cast<GeneratedBaseObj*>(derived.get()));
+  Optional<Arc<GeneratedBaseObj>> absent = std::nullopt;
+  
EXPECT_FALSE(Any(absent).cast<Optional<Arc<GeneratedBaseObj>>>().has_value());
+}
+
+TEST(Arc, ContainerStorageAndValidation) {
+  Arc<GeneratedDerivedObj> first = make_arc<GeneratedDerivedObj>(1);
+  Arc<GeneratedDerivedObj> second = make_arc<GeneratedDerivedObj>(2);
+
+  Array<Arc<GeneratedDerivedObj>> derived_array{first};
+  Array<Arc<GeneratedBaseObj>> base_array = derived_array;
+  EXPECT_TRUE(base_array.same_as(derived_array));
+  Array<ObjectPtr<GeneratedBaseObj>> nullable_base_array = derived_array;
+  EXPECT_TRUE(nullable_base_array.same_as(derived_array));
+
+  derived_array.push_back(second);
+  EXPECT_EQ(derived_array.size(), 2U);
+
+  Array<ObjectPtr<GeneratedDerivedObj>> checked_source{first};
+  Array<Arc<GeneratedBaseObj>> checked = 
Any(checked_source).cast<Array<Arc<GeneratedBaseObj>>>();
+  // Runtime casting validates every nullable source element before reusing 
the storage.
+  EXPECT_TRUE(checked.same_as(checked_source));
+  EXPECT_EQ(checked[0].get(), static_cast<GeneratedBaseObj*>(first.get()));
+
+  Array<ObjectPtr<GeneratedDerivedObj>> nullable_source{nullptr, first};
+  Any nullable_value = nullable_source;
+  
EXPECT_FALSE(nullable_value.try_cast<Array<Arc<GeneratedBaseObj>>>().has_value());
+  EXPECT_THROW(nullable_value.cast<Array<Arc<GeneratedBaseObj>>>(), 
tvm::ffi::Error);
+
+  EXPECT_EQ(TypeTraits<Array<Arc<GeneratedDerivedObj>>>::TypeSchema(),
+            
R"({"type":"ffi.Array","args":[{"type":"testing.GeneratedDerived"}]})");
+  EXPECT_EQ(
+      TypeTraits<Array<ObjectPtr<GeneratedDerivedObj>>>::TypeSchema(),
+      
R"({"type":"ffi.Array","args":[{"type":"Optional","args":[{"type":"testing.GeneratedDerived"}]}]})");
+}
+
 TEST(ObjectPtr, ContainerCovariance) {
   ObjectPtr<GeneratedDerivedObj> first = make_object<GeneratedDerivedObj>(1);
   ObjectPtr<GeneratedDerivedObj> second = make_object<GeneratedDerivedObj>(2);
diff --git a/tests/cpp/test_reflection.cc b/tests/cpp/test_reflection.cc
index 89746fc6..361ef9d5 100644
--- a/tests/cpp/test_reflection.cc
+++ b/tests/cpp/test_reflection.cc
@@ -138,79 +138,103 @@ TEST(Reflection, FieldSetter) {
 }
 
 TEST(Reflection, ObjectPtrField) {
-  ObjectPtr<TIntObj> initial = make_object<TIntObj>(10);
+  tvm::ffi::Arc<TIntObj> initial = make_arc<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();
+  {
+    Any value = getter(holder);
+    EXPECT_EQ(initial.use_count(), 3);
+    tvm::ffi::Arc<TIntObj> reflected = value.cast<tvm::ffi::Arc<TIntObj>>();
+    EXPECT_EQ(initial.use_count(), 4);
+    EXPECT_EQ(reflected.get(), initial_raw);
+  }
   EXPECT_EQ(initial.use_count(), 2);
 
   reflection::FieldSetter setter("test.ObjectPtrHolder", "value");
-  ObjectPtr<TIntObj> replacement = make_object<TIntObj>(20);
+  tvm::ffi::Arc<TIntObj> replacement = make_arc<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);
+  tvm::ffi::Arc<TFloatObj> incompatible = make_arc<TFloatObj>(2.5);
   EXPECT_THROW(setter(holder, incompatible), Error);
   EXPECT_EQ(holder->value.get(), replacement.get());
   EXPECT_EQ(incompatible.use_count(), 1);
 
+  EXPECT_THROW(setter(holder, nullptr), Error);
+  EXPECT_EQ(holder->value.get(), replacement.get());
+
   ObjectPtr<TIntObj> null_value;
-  setter(holder, null_value);
-  EXPECT_EQ(holder->value, nullptr);
-  EXPECT_EQ(replacement.use_count(), 1);
+  EXPECT_THROW(setter(holder, null_value), Error);
+  EXPECT_EQ(holder->value.get(), replacement.get());
 
   EXPECT_THROW(setter(holder, String("not a number")), Error);
-  EXPECT_EQ(holder->value, nullptr);
+  EXPECT_EQ(holder->value.get(), replacement.get());
+
+  reflection::FieldGetter optional_getter("test.ObjectPtrHolder", 
"optional_alias");
+  reflection::FieldSetter optional_setter("test.ObjectPtrHolder", 
"optional_alias");
+  EXPECT_EQ(optional_getter(holder), nullptr);
+  optional_setter(holder, initial);
+  EXPECT_EQ(optional_getter(holder).cast<ObjectPtr<TNumberObj>>().get(), 
initial.get());
+  optional_setter(holder, nullptr);
+  EXPECT_EQ(optional_getter(holder), 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>));
+  EXPECT_EQ(info->size, sizeof(tvm::ffi::Arc<TIntObj>));
+  EXPECT_EQ(info->alignment, alignof(tvm::ffi::Arc<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>(),
+  EXPECT_EQ(metadata["type_schema"].cast<String>(), R"({"type":"test.Int"})");
+
+  const TVMFFIFieldInfo* optional_info =
+      reflection::GetFieldInfo("test.ObjectPtrHolder", "optional_alias");
+  EXPECT_EQ(optional_info->size, sizeof(ObjectPtr<TNumberObj>));
+  EXPECT_EQ(optional_info->alignment, alignof(ObjectPtr<TNumberObj>));
+  Map<String, Any> optional_metadata =
+      json::Parse(String(optional_info->metadata)).cast<Map<String, Any>>();
+  EXPECT_EQ(optional_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);
+  tvm::ffi::Arc<TIntObj> input = make_arc<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>>();
+  tvm::ffi::Arc<TIntObj> output = 
std::move(result).cast<tvm::ffi::Arc<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(nullptr), Error);
+  EXPECT_THROW(identity(ObjectPtr<TIntObj>()), Error);
+  EXPECT_THROW(identity(make_arc<TFloatObj>(2.5)), Error);
   EXPECT_THROW(identity(String("not a number")), Error);
 
+  Function optional_identity = reflection::GetMethod("test.ObjectPtrHolder", 
"optional_identity");
+  EXPECT_EQ(optional_identity(nullptr), nullptr);
+  Any optional_result = optional_identity(input);
+  EXPECT_EQ(optional_result.cast<ObjectPtr<TIntObj>>().get(), raw_input);
+
   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":"test.Int"},{"type":"test.Int"}]})");
+
+  const TVMFFIMethodInfo* optional_info =
+      reflection::GetMethodInfo("test.ObjectPtrHolder", "optional_identity");
+  Map<String, Any> optional_metadata =
+      json::Parse(String(optional_info->metadata)).cast<Map<String, Any>>();
   EXPECT_EQ(
-      metadata["type_schema"].cast<String>(),
+      optional_metadata["type_schema"].cast<String>(),
       
R"({"type":"ffi.Function","args":[{"type":"Optional","args":[{"type":"test.Int"}]},{"type":"Optional","args":[{"type":"test.Int"}]}]})");
 }
 
diff --git a/tests/cpp/testing_object.h b/tests/cpp/testing_object.h
index c2bc8e0a..b5a6a394 100644
--- a/tests/cpp/testing_object.h
+++ b/tests/cpp/testing_object.h
@@ -236,21 +236,24 @@ class TPair : public ObjectRef {
 
 class TObjectPtrHolderObj : public Object {
  public:
-  ObjectPtr<TIntObj> value;
-  ObjectPtr<TNumberObj> alias;
+  Arc<TIntObj> value;
+  ObjectPtr<TNumberObj> optional_alias;
 
-  TObjectPtrHolderObj(ObjectPtr<TIntObj> value, ObjectPtr<TNumberObj> alias)
-      : value(std::move(value)), alias(std::move(alias)) {}
-  explicit TObjectPtrHolderObj(UnsafeInit) {}
+  TObjectPtrHolderObj(Arc<TIntObj> value, ObjectPtr<TNumberObj> optional_alias)
+      : value(std::move(value)), optional_alias(std::move(optional_alias)) {}
+  explicit TObjectPtrHolderObj(UnsafeInit) : value(UnsafeInit{}) {}
 
-  static ObjectPtr<TIntObj> Identity(ObjectPtr<TIntObj> value) { return value; 
}
+  static Arc<TIntObj> Identity(Arc<TIntObj> value) { return value; }
+  static ObjectPtr<TIntObj> OptionalIdentity(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);
+        .def_rw("optional_alias", &TObjectPtrHolderObj::optional_alias,
+                refl::default_value(nullptr))
+        .def_static("identity", &TObjectPtrHolderObj::Identity)
+        .def_static("optional_identity", 
&TObjectPtrHolderObj::OptionalIdentity);
   }
 
   static constexpr bool _type_mutable = true;
@@ -259,8 +262,8 @@ class TObjectPtrHolderObj : public Object {
 
 class TObjectPtrHolder : public ObjectRef {
  public:
-  TObjectPtrHolder(ObjectPtr<TIntObj> value, ObjectPtr<TNumberObj> alias = 
nullptr) {
-    data_ = make_object<TObjectPtrHolderObj>(std::move(value), 
std::move(alias));
+  TObjectPtrHolder(Arc<TIntObj> value, ObjectPtr<TNumberObj> optional_alias = 
nullptr) {
+    data_ = make_object<TObjectPtrHolderObj>(std::move(value), 
std::move(optional_alias));
   }
 
   TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(TObjectPtrHolder, ObjectRef, 
TObjectPtrHolderObj);
diff --git a/tests/python/test_dataclass_gen_abi_cpp.py 
b/tests/python/test_dataclass_gen_abi_cpp.py
index edf8c70a..98345c51 100644
--- a/tests/python/test_dataclass_gen_abi_cpp.py
+++ b/tests/python/test_dataclass_gen_abi_cpp.py
@@ -27,7 +27,13 @@ from typing import Any, Callable, Optional, Union, cast
 import pytest
 import tvm_ffi
 from tvm_ffi import Array, Device, Map, Object
-from tvm_ffi.core import DataType, TypeField, TypeInfo, 
_lookup_or_register_type_info_from_type_key
+from tvm_ffi.core import (
+    DataType,
+    TypeField,
+    TypeInfo,
+    TypeSchema,
+    _lookup_or_register_type_info_from_type_key,
+)
 from tvm_ffi.dataclasses import gen_abi_cpp, py_class
 from tvm_ffi.dataclasses.gen_abi_cpp import _Generator
 
@@ -157,6 +163,7 @@ def native_layout_probes(tmp_path_factory: 
pytest.TempPathFactory) -> tvm_ffi.Mo
     source = r"""
         #include <tvm/ffi/container/dict.h>
         #include <tvm/ffi/container/list.h>
+        #include <tvm/ffi/optional.h>
         #include <tvm/ffi/reflection/registry.h>
 
         namespace tvm {
@@ -175,6 +182,12 @@ def native_layout_probes(tmp_path_factory: 
pytest.TempPathFactory) -> tvm_ffi.Mo
           List<ABIRawTensorObj*> list_items;
           Map<String, ABIRawTensorObj*> mapping;
           Dict<String, ABIRawTensorObj*> dictionary;
+          Arc<ABIRawTensorObj> item;
+          ObjectPtr<ABIRawTensorObj> nullable_item;
+          Optional<Arc<ABIRawTensorObj>> optional_item;
+
+          explicit ABIObjectContainersObj(UnsafeInit) : item(UnsafeInit{}) {}
+
           TVM_FFI_DECLARE_OBJECT_INFO_FINAL(
               "testing.gen_abi_cpp.native.ObjectContainers",
               ABIObjectContainersObj,
@@ -194,7 +207,10 @@ def native_layout_probes(tmp_path_factory: 
pytest.TempPathFactory) -> tvm_ffi.Mo
               .def_ro("array_items", &ABIObjectContainersObj::array_items)
               .def_ro("list_items", &ABIObjectContainersObj::list_items)
               .def_ro("mapping", &ABIObjectContainersObj::mapping)
-              .def_ro("dictionary", &ABIObjectContainersObj::dictionary);
+              .def_ro("dictionary", &ABIObjectContainersObj::dictionary)
+              .def_ro("item", &ABIObjectContainersObj::item)
+              .def_ro("nullable_item", &ABIObjectContainersObj::nullable_item)
+              .def_ro("optional_item", &ABIObjectContainersObj::optional_item);
         }
 
         }  // namespace testing
@@ -370,11 +386,11 @@ struct alignas(8) MixedObj : public ::tvm::ffi::Object {
   ::tvm::ffi::String title;  // offset=88, size=16, align=8
   ::tvm::ffi::Bytes payload;  // offset=104, size=16, align=8
   ::tvm::ffi::Function callback;  // offset=120, size=8, align=8
-  ::tvm::ffi::ObjectPtr<::testing::gen_abi_cpp::DependencyObj> dependency;  // 
offset=128, size=8, align=8
-  
::tvm::ffi::Array<::tvm::ffi::ObjectPtr<::testing::gen_abi_cpp::DependencyObj>> 
array_items;  // offset=136, size=8, align=8
-  
::tvm::ffi::List<::tvm::ffi::ObjectPtr<::testing::gen_abi_cpp::DependencyObj>> 
list_items;  // offset=144, size=8, align=8
-  ::tvm::ffi::Map<::tvm::ffi::String, 
::tvm::ffi::ObjectPtr<::testing::gen_abi_cpp::DependencyObj>> mapping;  // 
offset=152, size=8, align=8
-  ::tvm::ffi::Dict<::tvm::ffi::String, 
::tvm::ffi::ObjectPtr<::testing::gen_abi_cpp::DependencyObj>> dictionary;  // 
offset=160, size=8, align=8
+  ::tvm::ffi::Arc<::testing::gen_abi_cpp::DependencyObj> dependency;  // 
offset=128, size=8, align=8
+  ::tvm::ffi::Array<::tvm::ffi::Arc<::testing::gen_abi_cpp::DependencyObj>> 
array_items;  // offset=136, size=8, align=8
+  ::tvm::ffi::List<::tvm::ffi::Arc<::testing::gen_abi_cpp::DependencyObj>> 
list_items;  // offset=144, size=8, align=8
+  ::tvm::ffi::Map<::tvm::ffi::String, 
::tvm::ffi::Arc<::testing::gen_abi_cpp::DependencyObj>> mapping;  // 
offset=152, size=8, align=8
+  ::tvm::ffi::Dict<::tvm::ffi::String, 
::tvm::ffi::Arc<::testing::gen_abi_cpp::DependencyObj>> dictionary;  // 
offset=160, size=8, align=8
   ::tvm::ffi::Any optional;  // offset=168, size=16, align=8
   ::tvm::ffi::Any choice;  // offset=184, size=16, align=8
 };
@@ -436,7 +452,7 @@ static_assert(offsetof(MixedObj, choice) == 184);
 struct alignas(8) MutualLeftObj : public ::tvm::ffi::Object {
   TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.gen_abi_cpp.MutualLeft", 1);
 
-  
::tvm::ffi::List<::tvm::ffi::ObjectPtr<::testing::gen_abi_cpp::MutualRightObj>> 
rights;  // offset=24, size=8, align=8
+  ::tvm::ffi::List<::tvm::ffi::Arc<::testing::gen_abi_cpp::MutualRightObj>> 
rights;  // offset=24, size=8, align=8
 };
 
 static_assert(sizeof(MutualLeftObj) == 32);
@@ -448,7 +464,7 @@ static_assert(offsetof(MutualLeftObj, rights) == 24);
 struct alignas(8) MutualRightObj : public ::tvm::ffi::Object {
   TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.gen_abi_cpp.MutualRight", 1);
 
-  
::tvm::ffi::List<::tvm::ffi::ObjectPtr<::testing::gen_abi_cpp::MutualLeftObj>> 
lefts;  // offset=24, size=8, align=8
+  ::tvm::ffi::List<::tvm::ffi::Arc<::testing::gen_abi_cpp::MutualLeftObj>> 
lefts;  // offset=24, size=8, align=8
 };
 
 static_assert(sizeof(MutualRightObj) == 32);
@@ -462,8 +478,8 @@ struct alignas(8) NestedStructuralObj : public 
::tvm::ffi::Object {
 
   ::tvm::ffi::List<::tvm::ffi::Any> optionals;  // offset=24, size=8, align=8
   ::tvm::ffi::Dict<::tvm::ffi::String, ::tvm::ffi::Any> unions;  // offset=32, 
size=8, align=8
-  ::tvm::ffi::List<::tvm::ffi::Any> optional_objects;  // offset=40, size=8, 
align=8
-  ::tvm::ffi::Dict<::tvm::ffi::String, ::tvm::ffi::Any> union_objects;  // 
offset=48, size=8, align=8
+  
::tvm::ffi::List<::tvm::ffi::ObjectPtr<::testing::gen_abi_cpp::DependencyObj>> 
optional_objects;  // offset=40, size=8, align=8
+  ::tvm::ffi::Dict<::tvm::ffi::String, ::tvm::ffi::Arc<::tvm::ffi::Object>> 
union_objects;  // offset=48, size=8, align=8
 };
 
 static_assert(sizeof(NestedStructuralObj) == 56);
@@ -484,7 +500,7 @@ static_assert(offsetof(NestedStructuralObj, union_objects) 
== 48);
 struct alignas(8) RecursiveObj : public ::tvm::ffi::Object {
   TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.gen_abi_cpp.Recursive", 1);
 
-  
::tvm::ffi::List<::tvm::ffi::ObjectPtr<::testing::gen_abi_cpp::RecursiveObj>> 
children;  // offset=24, size=8, align=8
+  ::tvm::ffi::List<::tvm::ffi::Arc<::testing::gen_abi_cpp::RecursiveObj>> 
children;  // offset=24, size=8, align=8
 };
 
 static_assert(sizeof(RecursiveObj) == 32);
@@ -572,7 +588,7 @@ struct alignas(8) ChildObj : public 
::testing::gen_abi_cpp::empty::EmptyObj {
   TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.gen_abi_cpp.other.Child", 3);
 
   bool child_flag;  // offset=40, size=1, align=1
-  ::tvm::ffi::ObjectPtr<::testing::gen_abi_cpp::DependencyObj> dependency;  // 
offset=48, size=8, align=8
+  ::tvm::ffi::Arc<::testing::gen_abi_cpp::DependencyObj> dependency;  // 
offset=48, size=8, align=8
 };
 
 static_assert(sizeof(ChildObj) == 56);
@@ -706,13 +722,16 @@ namespace native {
 struct alignas(8) ObjectContainersObj : public ::tvm::ffi::Object {
   
TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.gen_abi_cpp.native.ObjectContainers",
 1);
 
-  
::tvm::ffi::Array<::tvm::ffi::ObjectPtr<::testing::gen_abi_cpp::native::RawTensorObj>>
 array_items;  // offset=24, size=8, align=8
-  
::tvm::ffi::List<::tvm::ffi::ObjectPtr<::testing::gen_abi_cpp::native::RawTensorObj>>
 list_items;  // offset=32, size=8, align=8
-  ::tvm::ffi::Map<::tvm::ffi::String, 
::tvm::ffi::ObjectPtr<::testing::gen_abi_cpp::native::RawTensorObj>> mapping;  
// offset=40, size=8, align=8
-  ::tvm::ffi::Dict<::tvm::ffi::String, 
::tvm::ffi::ObjectPtr<::testing::gen_abi_cpp::native::RawTensorObj>> 
dictionary;  // offset=48, size=8, align=8
+  
::tvm::ffi::Array<::tvm::ffi::Arc<::testing::gen_abi_cpp::native::RawTensorObj>>
 array_items;  // offset=24, size=8, align=8
+  
::tvm::ffi::List<::tvm::ffi::Arc<::testing::gen_abi_cpp::native::RawTensorObj>> 
list_items;  // offset=32, size=8, align=8
+  ::tvm::ffi::Map<::tvm::ffi::String, 
::tvm::ffi::Arc<::testing::gen_abi_cpp::native::RawTensorObj>> mapping;  // 
offset=40, size=8, align=8
+  ::tvm::ffi::Dict<::tvm::ffi::String, 
::tvm::ffi::Arc<::testing::gen_abi_cpp::native::RawTensorObj>> dictionary;  // 
offset=48, size=8, align=8
+  ::tvm::ffi::Arc<::testing::gen_abi_cpp::native::RawTensorObj> item;  // 
offset=56, size=8, align=8
+  ::tvm::ffi::ObjectPtr<::testing::gen_abi_cpp::native::RawTensorObj> 
nullable_item;  // offset=64, size=8, align=8
+  
::tvm::ffi::Optional<::tvm::ffi::Arc<::testing::gen_abi_cpp::native::RawTensorObj>>
 optional_item;  // offset=72, size=16, align=8
 };
 
-static_assert(sizeof(ObjectContainersObj) == 56);
+static_assert(sizeof(ObjectContainersObj) == 88);
 static_assert(alignof(ObjectContainersObj) == 8);
 static_assert(sizeof(decltype(ObjectContainersObj::array_items)) == 8);
 static_assert(alignof(decltype(ObjectContainersObj::array_items)) == 8);
@@ -726,6 +745,15 @@ static_assert(offsetof(ObjectContainersObj, mapping) == 
40);
 static_assert(sizeof(decltype(ObjectContainersObj::dictionary)) == 8);
 static_assert(alignof(decltype(ObjectContainersObj::dictionary)) == 8);
 static_assert(offsetof(ObjectContainersObj, dictionary) == 48);
+static_assert(sizeof(decltype(ObjectContainersObj::item)) == 8);
+static_assert(alignof(decltype(ObjectContainersObj::item)) == 8);
+static_assert(offsetof(ObjectContainersObj, item) == 56);
+static_assert(sizeof(decltype(ObjectContainersObj::nullable_item)) == 8);
+static_assert(alignof(decltype(ObjectContainersObj::nullable_item)) == 8);
+static_assert(offsetof(ObjectContainersObj, nullable_item) == 64);
+static_assert(sizeof(decltype(ObjectContainersObj::optional_item)) == 16);
+static_assert(alignof(decltype(ObjectContainersObj::optional_item)) == 8);
+static_assert(offsetof(ObjectContainersObj, optional_item) == 72);
 
 }  // namespace native
 }  // namespace gen_abi_cpp
@@ -770,8 +798,8 @@ namespace gen_abi_cpp {
 struct alignas(8) ExtraObjectsObj : public ::tvm::ffi::Object {
   TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.gen_abi_cpp.ExtraObjects", 1);
 
-  ::tvm::ffi::ObjectPtr<::tvm::ffi::ModuleObj> module;  // offset=24, size=8, 
align=8
-  ::tvm::ffi::ObjectPtr<::tvm::ffi::VisitInterruptObj> interrupt;  // 
offset=32, size=8, align=8
+  ::tvm::ffi::Arc<::tvm::ffi::ModuleObj> module;  // offset=24, size=8, align=8
+  ::tvm::ffi::Arc<::tvm::ffi::VisitInterruptObj> interrupt;  // offset=32, 
size=8, align=8
 };
 
 static_assert(sizeof(ExtraObjectsObj) == 40);
@@ -871,6 +899,68 @@ def 
test_schema_less_custom_object_uses_object_ptr_carrier() -> None:
     assert (carrier.size, carrier.alignment) == (8, 8)
 
 
+def test_native_object_carriers_preserve_schema_nullability() -> None:
+    object_schema = {"type": "testing.gen_abi_cpp.Dependency"}
+    optional_schema = {"type": "Optional", "args": [object_schema]}
+    owner = cast(
+        TypeInfo,
+        SimpleNamespace(type_key="testing.gen_abi_cpp.NativeObjectCarriers"),
+    )
+
+    def lower(name: str, raw_schema: dict[str, object], size: int) -> str:
+        field = cast(
+            TypeField,
+            SimpleNamespace(
+                name=name,
+                size=size,
+                alignment=8,
+                offset=24,
+                field_static_type_index=128,
+                ty=TypeSchema.from_json_obj(raw_schema),
+                metadata={"type_schema": raw_schema},
+            ),
+        )
+        return _Generator([])._lower_field(field, owner).cpp_type
+
+    object_type = "::testing::gen_abi_cpp::DependencyObj"
+    assert lower("required", object_schema, 8) == 
f"::tvm::ffi::Arc<{object_type}>"
+    assert lower("nullable", optional_schema, 8) == 
f"::tvm::ffi::ObjectPtr<{object_type}>"
+    assert (
+        lower("optional", optional_schema, 16)
+        == f"::tvm::ffi::Optional<::tvm::ffi::Arc<{object_type}>>"
+    )
+
+
+def test_non_null_object_union_uses_arc_carrier() -> None:
+    raw_schema = {
+        "type": "Variant",
+        "args": [
+            {"type": "testing.gen_abi_cpp.Dependency"},
+            {"type": "testing.gen_abi_cpp.other.Sibling"},
+        ],
+    }
+    field = cast(
+        TypeField,
+        SimpleNamespace(
+            name="choice",
+            size=8,
+            alignment=8,
+            offset=24,
+            field_static_type_index=64,
+            ty=TypeSchema.from_json_obj(raw_schema),
+            metadata={"type_schema": raw_schema},
+        ),
+    )
+    owner = cast(
+        TypeInfo,
+        SimpleNamespace(type_key="testing.gen_abi_cpp.NativeObjectUnion"),
+    )
+
+    carrier = _Generator([])._lower_field(field, owner)
+
+    assert carrier.cpp_type == "::tvm::ffi::Arc<::tvm::ffi::Object>"
+
+
 def test_extra_object_carriers_remain_typed(tmp_path: Path) -> None:
     header = gen_abi_cpp("testing.gen_abi_cpp.ExtraObjects")
     assert header == _EXPECTED_EXTRA_PROGRAM
@@ -953,8 +1043,8 @@ def 
test_generated_header_compiles_and_reads_live_objects(tmp_path: Path) -> Non
                       ::testing::gen_abi_cpp::empty::EmptyObj,
                       ::testing::gen_abi_cpp::other::ChildObj>);
         static_assert(std::is_constructible_v<
-                      ::tvm::ffi::ObjectPtr<::testing::gen_abi_cpp::BaseObj>,
-                      ::tvm::ffi::ObjectPtr<
+                      ::tvm::ffi::Arc<::testing::gen_abi_cpp::BaseObj>,
+                      ::tvm::ffi::Arc<
                           ::testing::gen_abi_cpp::other::ChildObj>>);
 
         int64_t gen_abi_cpp_read_sequence(
@@ -969,9 +1059,9 @@ def 
test_generated_header_compiles_and_reads_live_objects(tmp_path: Path) -> Non
         }
 
         bool gen_abi_cpp_upcast_identity(
-            ::tvm::ffi::ObjectPtr<
+            ::tvm::ffi::Arc<
                 ::testing::gen_abi_cpp::other::ChildObj> child) {
-          ::tvm::ffi::ObjectPtr<::testing::gen_abi_cpp::BaseObj> base = child;
+          ::tvm::ffi::Arc<::testing::gen_abi_cpp::BaseObj> base = child;
           return reinterpret_cast<const void*>(child.get()) ==
                  reinterpret_cast<const void*>(base.get());
         }
diff --git a/tests/python/test_dataclass_py_class.py 
b/tests/python/test_dataclass_py_class.py
index 3af110ca..c001bbcb 100644
--- a/tests/python/test_dataclass_py_class.py
+++ b/tests/python/test_dataclass_py_class.py
@@ -3698,9 +3698,21 @@ class TestNativeParentInheritance:
         gc.collect()
         assert use_count(target) == 2
 
-        holder.value = None
-        assert holder.value is None
-        assert use_count(target) == 1
+        with pytest.raises(TypeError, match=r"testing\.TestObjectBase"):
+            holder.value = None  # ty: ignore[invalid-assignment]
+        assert holder.value is target
+        assert use_count(target) == 2
+
+        assert holder.optional_value is None
+        holder.optional_value = target
+        assert holder.optional_value is target
+        assert use_count(target) == 3
+        holder.optional_value = None
+        assert holder.optional_value is None
+        assert use_count(target) == 2
+
+        with pytest.raises(TypeError, match=r"testing\.TestObjectBase"):
+            _TestObjectPtrHolder(None)  # ty: ignore[invalid-argument-type]
 
         holder.value = replacement
         assert use_count(replacement) == 2
@@ -3708,11 +3720,14 @@ class TestNativeParentInheritance:
         assert use_count(target) == 2
         assert use_count(replacement) == 1
 
-        unrelated = _TestObjectPtrHolder(None)
+        unrelated = _TestObjectPtrHolder(replacement)
         with pytest.raises(TypeError):
             holder.value = unrelated  # ty: ignore[invalid-assignment]
         assert holder.value is not None
         assert holder.value.same_as(target)
+        del unrelated
+        gc.collect()
+        assert use_count(replacement) == 1
 
         del holder
         gc.collect()
@@ -3731,7 +3746,7 @@ class TestNativeParentInheritance:
             v_f64=2.0,
             v_str="target",
         )
-        child = Child(value=target, extra=3)
+        child = Child(value=target, optional_value=None, extra=3)
         assert use_count(target) == 2
 
         child_copy = copy.copy(child)
diff --git a/tests/python/test_metadata.py b/tests/python/test_metadata.py
index 5fe09d16..ad113ea4 100644
--- a/tests/python/test_metadata.py
+++ b/tests/python/test_metadata.py
@@ -130,10 +130,15 @@ def test_schema_field(field_name: str, expected: str) -> 
None:
 
 def test_schema_object_ptr_field() -> None:
     type_info: TypeInfo = getattr(TestObjectPtrHolder, "__tvm_ffi_type_info__")
-    assert len(type_info.fields) == 1
+    assert len(type_info.fields) == 2
     assert type_info.fields[0].name == "value"
     assert (
         
str(TypeSchema.from_json_str(type_info.fields[0].metadata["type_schema"]))
+        == "testing.TestObjectBase"
+    )
+    assert type_info.fields[1].name == "optional_value"
+    assert (
+        
str(TypeSchema.from_json_str(type_info.fields[1].metadata["type_schema"]))
         == "testing.TestObjectBase | None"
     )
 

Reply via email to