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 b2c7712d feat(abi): generate layout-compatible C++ object declarations 
(#670)
b2c7712d is described below

commit b2c7712d1fca9ac591dcaa93af6a2884bbaf6ff9
Author: Junru Shao <[email protected]>
AuthorDate: Wed Jul 15 21:48:45 2026 -0700

    feat(abi): generate layout-compatible C++ object declarations (#670)
    
    ## Summary
    
    - Add `tvm_ffi.dataclasses.gen_abi_cpp` to generate self-contained,
    lookup-only C++17 object declarations from registered reflection
    metadata.
    - Preserve reflected inheritance and layout across native,
    Python-defined, recursive, and mutually recursive object types,
    including `ObjectPtr<TObject>` container fields.
    - Expose `tvm::ffi::is_object_subclass_v` as the shared complete-type
    query and explicit specialization point for generated incomplete object
    declarations.
    - Extend reflection metadata and C++ lookup helpers enough to validate
    carriers and emit compile-time size, alignment, and field-offset
    assertions.
    - Add frozen presentation support for C++-reflected dataclass fields and
    keep generator regression fixtures collectable on Python 3.9.
    
    ## Architecture
    
    - Expand exact or globbed type-key selectors through their dynamic
    object ancestry, lower reflected fields to ABI-compatible C++ carriers,
    and emit deterministically grouped namespaces and declarations.
    - Generate lookup-only views that read existing registered objects
    without registering types, allocating objects, or changing the C ABI.
    - Add `TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP` for cached runtime type-index
    lookup.
    - Move `is_object_subclass_v` to public `tvm::ffi`, retain it as the
    gate for `TypeTraits<ObjectPtr<T>>`, and have generated headers emit
    direct inline explicit specializations after forward declarations.
    - Specialize only genuinely incomplete recursive types in handwritten
    compile tests; complete object types continue through the primary
    `std::is_base_of_v` definition.
    - Expose field alignment, static type indices, total size,
    native-metadata availability, and schema subtype relationships through
    Python reflection so generation can prove layout and inheritance
    compatibility.
    - Match derived-field placement to Microsoft and Itanium-family
    tail-padding rules while preserving upstream `ObjectPtr`,
    `WeakObjectPtr`, container, hashing, and subsumption behavior.
    
    ## Public Interfaces
    
    - Export `tvm_ffi.dataclasses.gen_abi_cpp`, accepting an exact type key,
    shell-style pattern, or sequence of selectors and returning C++17 header
    source.
    - Add `TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP` to the public C++ headers.
    - Expose `tvm::ffi::is_object_subclass_v` as the public object-subclass
    query and customization point for intentionally declared incomplete
    `Object` types.
    - Expose `TypeSchema.is_subtype_of`, `TypeField.alignment`,
    `TypeField.field_static_type_index`, and `TypeInfo.total_size` in Python
    reflection.
    - Add the optional `frozen` parameter to `c_class` for read-only
    presentation of C++-reflected fields.
    - Preserve established `ObjectPtr<TBase>` construction and assignment
    from `ObjectPtr<TSub>`.
    
    ## UI/UX
    
    - None.
    
    ## Behavioral Changes
    
    - Generate inheritance-preserving declarations with escaped non-C++
    identifiers, opaque dependencies, deterministic namespace ordering, and
    typed `ObjectPtr` carriers where reflection proves the object type.
    - Emit direct inline `is_object_subclass_v` specializations for
    generated forward declarations instead of a dedicated macro.
    - Use erased `ObjectPtr<Object>` carriers for schema-less custom object
    fields.
    - Reject unsafe native aliases, ambiguous `Optional` or `Union`
    carriers, unsupported static objects, unmatched selectors, and native
    types without fixed size metadata.
    - Place Python-defined subclass fields at ABI-correct tail-padding
    offsets on Windows and Itanium-family platforms.
    - Support reflection-based subtype checks and frozen reflected C++
    fields with an explicit descriptor-level mutation escape hatch.
    - Preserve backing-object identity across tested object-pointer
    container covariance and erased-value round trips.
    - Spell runtime-resolved generator test unions with `typing.Optional`
    and `typing.Union` so Python 3.9 can collect the complete generator test
    file.
    
    ## Breaking Changes
    
    - None. The feature is additive and does not alter the C ABI, registered
    object layout, or existing object-pointer conversion behavior.
    
    ## Docs
    
    - Document `c_class(..., frozen=True)`, read-only reflected fields,
    inheritance behavior, and the controlled descriptor `set` escape hatch
    in the dataclass reflection guide.
    - Document `gen_abi_cpp` selector, output, and non-mutating semantics in
    its exported function docstring; no separate generator walkthrough is
    included.
    - Document the public `is_object_subclass_v` query and lookup-only
    metadata macro inline in the C++ headers.
    
    ## Tests
    
    - Compiler probe without incomplete-type specialization — failed on
    `std::is_base_of` as expected.
    - Compiler probe with explicit `is_object_subclass_v` specialization —
    passed.
    - C++ build and full CTest suite — build passed; all 451 enabled tests
    passed and 2 remained disabled.
    - Full generator test file under the normal development Python — 14
    passed.
    - Full generator test file under isolated Python 3.9 — 14 passed.
    - Full Python unit suite before the review-driven specialization update
    — 2,393 passed, 18 skipped, 3 xfailed.
    - Pre-commit on all files — passed all hooks.
    - Pinned clang-tidy 21.1.1 on `tests/cpp/test_abi_object.cc`,
    `tests/cpp/test_object_ptr.cc`, and `tests/cpp/test_any.cc` — passed.
    - `git diff --check` — passed.
    - Rust tests were not run because Cargo and Rustup are unavailable
    locally.
    
    ## Untested Edge Cases
    
    - The complete Python suite was not rerun after the review-driven C++
    specialization/output update; both development and Python 3.9 generator
    suites cover the changed generated text and compilation path.
    - Rust workspace tests remain unexecuted locally; no Rust source or
    binding is modified.
    - Linux and Windows compiler matrices remain CI-only. Local
    generated-header compilation and platform-conditional layout tests cover
    the paths available on macOS; CI supplies cross-platform ABI
    confirmation.
---
 docs/guides/dataclass_reflection.rst       |   18 +
 include/tvm/ffi/object.h                   |   10 +-
 include/tvm/ffi/type_traits.h              |   32 +-
 python/tvm_ffi/core.pyi                    |    5 +
 python/tvm_ffi/cython/base.pxi             |    1 +
 python/tvm_ffi/cython/object.pxi           |    2 +
 python/tvm_ffi/cython/type_info.pxi        |   62 +-
 python/tvm_ffi/dataclasses/__init__.py     |    2 +
 python/tvm_ffi/dataclasses/c_class.py      |   26 +-
 python/tvm_ffi/dataclasses/gen_abi_cpp.py  |  682 +++++++++++++++++++
 python/tvm_ffi/testing/__init__.py         |    1 +
 python/tvm_ffi/testing/testing.py          |   17 +
 src/ffi/extra/structural_visit.cc          |    5 +-
 src/ffi/testing/testing.cc                 |   11 +
 tests/cpp/test_abi_object.cc               |   87 +++
 tests/cpp/test_any.cc                      |    4 +-
 tests/cpp/test_dict.cc                     |    1 +
 tests/cpp/test_list.cc                     |    7 +
 tests/cpp/test_object_ptr.cc               |  442 ++++++++++++
 tests/python/test_dataclass_gen_abi_cpp.py | 1020 ++++++++++++++++++++++++++++
 tests/python/test_dataclass_py_class.py    |   47 ++
 tests/python/test_type_converter.py        |   14 +
 22 files changed, 2481 insertions(+), 15 deletions(-)

diff --git a/docs/guides/dataclass_reflection.rst 
b/docs/guides/dataclass_reflection.rst
index 873eb6a1..803ada55 100644
--- a/docs/guides/dataclass_reflection.rst
+++ b/docs/guides/dataclass_reflection.rst
@@ -401,6 +401,24 @@ The decorator:
 4. Installs ``__copy__``, ``__deepcopy__``, ``__eq__``, ``__hash__``,
    ``__repr__``, and comparison operators.
 
+Use ``frozen=True`` when the Python class should expose reflected fields as
+read-only after construction:
+
+.. code-block:: python
+
+   @c_class("my_ext.Point", frozen=True)
+   class Point(tvm_ffi.Object):
+       x: int
+       y: int
+       label: str
+
+This marks the reflected field metadata as frozen and installs read-only
+descriptors on the Python class, even for fields that are registered as
+``def_rw`` on the C++ side. It is intended for immutable value and IR node
+families. Controlled internal updates can still use the field descriptor's
+``set`` method when a framework component needs to rebuild or annotate an
+object.
+
 .. note::
 
    ``@tvm_ffi.register_object`` can also be used, which delegates to
diff --git a/include/tvm/ffi/object.h b/include/tvm/ffi/object.h
index 0c952450..c431c551 100644
--- a/include/tvm/ffi/object.h
+++ b/include/tvm/ffi/object.h
@@ -529,8 +529,6 @@ 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.
@@ -538,10 +536,12 @@ namespace details {
 template <typename T>
 inline constexpr bool is_object_subclass_v = std::is_base_of_v<Object, T>;
 
+namespace details {
+
 /*! \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>>> &&
+    
::tvm::ffi::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
@@ -1305,7 +1305,7 @@ struct TypeToRuntimeTypeIndex<T, 
std::enable_if_t<std::is_base_of_v<ObjectRef, T
 
 template <typename TObject>
 struct TypeToRuntimeTypeIndex<
-    ObjectPtr<TObject>, 
std::enable_if_t<details::is_object_subclass_v<TObject> &&
+    ObjectPtr<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(); }
 };
@@ -1316,7 +1316,7 @@ struct TypeToRuntimeTypeIndex<
  */
 template <typename TObject>
 struct TypeTraits<ObjectPtr<TObject>,
-                  std::enable_if_t<details::is_object_subclass_v<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;
diff --git a/include/tvm/ffi/type_traits.h b/include/tvm/ffi/type_traits.h
index 1a316428..45e8749d 100644
--- a/include/tvm/ffi/type_traits.h
+++ b/include/tvm/ffi/type_traits.h
@@ -18,7 +18,7 @@
  */
 /*!
  * \file tvm/ffi/type_traits.h
- * \brief Type trait helpers for FFI values.
+ * \brief Type trait helpers for FFI values and lookup-only ABI objects.
  */
 #ifndef TVM_FFI_TYPE_TRAITS_H_
 #define TVM_FFI_TYPE_TRAITS_H_
@@ -574,4 +574,34 @@ struct FallbackOnlyTraitsBase : public TypeTraitsBase {
 }  // namespace ffi
 }  // namespace tvm
 
+/*!
+ * \brief Declare lookup-only metadata for an ABI object.
+ *
+ * Unlike TVM_FFI_DECLARE_OBJECT_INFO, this macro only looks up an existing
+ * registered type key and never allocates or registers a type index. Invoke it
+ * in the public section of an object class with a string literal key.
+ *
+ * \note Include `tvm/ffi/function.h` or `tvm/ffi/tvm_ffi.h` before expanding
+ * this macro so safe-call errors can be propagated as C++ exceptions.
+ *
+ * \param RegisteredKey The registered type key of the ABI object.
+ * \param TypeDepth The reflected depth of the type in the object hierarchy.
+ */
+#define TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP(RegisteredKey, TypeDepth)           
\
+  static constexpr const char* _type_key = RegisteredKey;                      
\
+  static constexpr int32_t _type_depth = TypeDepth;                            
\
+  static constexpr bool _type_mutable = true;                                  
\
+  static constexpr bool _type_final = false;                                   
\
+  static constexpr uint32_t _type_child_slots = 0;                             
\
+  static constexpr bool _type_child_slots_can_overflow = true;                 
\
+  static int32_t RuntimeTypeIndex() {                                          
\
+    static const int32_t type_index = []() {                                   
\
+      constexpr TVMFFIByteArray key{RegisteredKey, sizeof(RegisteredKey) - 1}; 
\
+      int32_t result = -1;                                                     
\
+      TVM_FFI_CHECK_SAFE_CALL(TVMFFITypeKeyToIndex(&key, &result));            
\
+      return result;                                                           
\
+    }();                                                                       
\
+    return type_index;                                                         
\
+  }
+
 #endif  // TVM_FFI_TYPE_TRAITS_H_
diff --git a/python/tvm_ffi/core.pyi b/python/tvm_ffi/core.pyi
index 082a7d85..525d7a40 100644
--- a/python/tvm_ffi/core.pyi
+++ b/python/tvm_ffi/core.pyi
@@ -282,6 +282,7 @@ class TypeSchema:
     def from_type_index(type_index: int, args: tuple[TypeSchema, ...] = ()) -> 
TypeSchema: ...
     @staticmethod
     def from_annotation(annotation: object) -> TypeSchema: ...
+    def is_subtype_of(self, target_cls: type) -> bool: ...
     def repr(self, ty_map: Callable[[str], str] | None = None) -> str: ...
     def input_repr(self, ty_map: Callable[[str], str] | None = None) -> str: 
...
     def output_repr(self, ty_map: Callable[[str], str] | None = None) -> str: 
...
@@ -293,7 +294,9 @@ class TypeField:
     name: str
     doc: str | None
     size: int
+    alignment: int
     offset: int
+    field_static_type_index: int
     frozen: bool
     metadata: dict[str, Any]
     getter: Any
@@ -329,6 +332,8 @@ class TypeInfo:
     fields: list[TypeField]
     methods: list[TypeMethod]
     parent_type_info: TypeInfo | None
+    total_size: int
+    _has_type_metadata: bool
     _decorator_args: dict[str, Any]
 
     def _register_fields(self, fields: list[Any], structure_kind: int | None = 
...) -> None: ...
diff --git a/python/tvm_ffi/cython/base.pxi b/python/tvm_ffi/cython/base.pxi
index a2492077..3f32547a 100644
--- a/python/tvm_ffi/cython/base.pxi
+++ b/python/tvm_ffi/cython/base.pxi
@@ -153,6 +153,7 @@ cdef extern from "tvm/ffi/c_api.h":
         kTVMFFIOpaquePyObject = 74
         kTVMFFIList = 75
         kTVMFFIDict = 76
+        kTVMFFIStaticObjectEnd
 
     ctypedef void* TVMFFIObjectHandle
 
diff --git a/python/tvm_ffi/cython/object.pxi b/python/tvm_ffi/cython/object.pxi
index f707ed4b..4a5b1d95 100644
--- a/python/tvm_ffi/cython/object.pxi
+++ b/python/tvm_ffi/cython/object.pxi
@@ -558,7 +558,9 @@ cdef _type_info_create_from_type_key(object type_cls, str 
type_key):
                 name=bytearray_to_str(&field.name),
                 doc=bytearray_to_str(&field.doc) if field.doc.size != 0 else 
None,
                 size=field.size,
+                alignment=field.alignment,
                 offset=field.offset,
+                field_static_type_index=field.field_static_type_index,
                 frozen=(field.flags & kTVMFFIFieldFlagBitMaskWritable) == 0,
                 metadata=metadata_obj,
                 getter=getter,
diff --git a/python/tvm_ffi/cython/type_info.pxi 
b/python/tvm_ffi/cython/type_info.pxi
index 806e6fc5..ffdb73ce 100644
--- a/python/tvm_ffi/cython/type_info.pxi
+++ b/python/tvm_ffi/cython/type_info.pxi
@@ -16,6 +16,7 @@
 # under the License.
 import dataclasses
 import json
+import sys
 import typing
 import collections.abc
 from functools import cached_property
@@ -205,6 +206,29 @@ class TypeSchema:
                 if tindex is not None:
                     self.origin_type_index = tindex
 
+    def is_subtype_of(self, target_cls: type) -> bool:
+        """Return whether this schema's origin is a subtype of ``target_cls``.
+
+        The check uses FFI object type metadata.  It returns ``False`` when
+        ``target_cls`` is not an FFI object class or when this schema has a
+        structural/POD origin instead of an object type index.
+        """
+        target_info = getattr(target_cls, "__tvm_ffi_type_info__", None)
+        target_type_index = None
+        if target_info is None:
+            try:
+                target_type_index = 
TypeSchema.from_annotation(target_cls).origin_type_index
+            except TypeError:
+                return False
+            if target_type_index == kTVMFFIObject:
+                return self.origin_type_index >= kTVMFFIStaticObjectBegin
+            return False
+        target_type_index = target_info.type_index
+        if self.origin_type_index == target_type_index:
+            return True
+        source_info = _type_index_to_type_info(self.origin_type_index)
+        return source_info is not None and target_type_index in 
source_info.type_ancestors
+
     @cached_property
     def _converter(self):
         """Lazily build the type converter on first use.
@@ -659,7 +683,9 @@ class TypeField:
     name: str
     doc: Optional[str]
     size: int
+    alignment: int
     offset: int
+    field_static_type_index: int
     frozen: bool
     metadata: dict[str, Any]
     getter: FieldGetter
@@ -841,6 +867,12 @@ class TypeInfo:
             end = max(end, f.offset + f.size)
         return (end + 7) & ~7  # align to 8 bytes
 
+    @property
+    def _has_type_metadata(self) -> bool:
+        """Whether the registry publishes native size metadata for this 
type."""
+        cdef const TVMFFITypeInfo* c_info = TVMFFIGetTypeInfo(self.type_index)
+        return c_info != NULL and c_info.metadata != NULL
+
     def _register_fields(self, fields, structure_kind=None):
         """Register Field descriptors and set up __ffi_new__/__ffi_init__.
 
@@ -928,6 +960,28 @@ _ORIGIN_NATIVE_LAYOUT = {
     "Union": (16, 8, -1),
 }
 
+
+cdef int64_t _get_subclass_offset(object type_info):
+    """Compute where C++ places a byte-aligned field in a direct subclass."""
+    cdef int64_t end
+    if type_info is None:
+        return sizeof(TVMFFIObject)
+    if sys.platform == "win32":
+        # The Microsoft C++ ABI does not reuse a base class's tail padding.
+        return type_info.total_size
+
+    # Itanium-family ABIs start derived fields at the unpadded end of the base.
+    # TypeInfo.fields contains only this type's own fields, so recurse through
+    # empty intermediate classes as well as ordinary inheritance.
+    if type_info.parent_type_info is None:
+        end = sizeof(TVMFFIObject)
+    else:
+        end = _get_subclass_offset(type_info.parent_type_info)
+    if type_info.fields is not None:
+        for field in type_info.fields:
+            end = max(end, field.offset + field.size)
+    return end
+
 cdef _register_one_field(
     int32_t type_index,
     object py_field,
@@ -1099,9 +1153,9 @@ def _register_fields(type_info, fields, 
structure_kind=None):
         The registered field descriptors.
     """
     cdef int32_t type_index = type_info.type_index
-    # Start field offsets AFTER all parent fields (not at fixed offset 24).
-    # This is critical for inheritance: child fields must not overlap parent 
memory.
-    cdef int64_t current_offset = type_info.parent_type_info.total_size
+    # C++ ABIs differ on whether a derived class can reuse parent tail padding.
+    # Derive the matching boundary from existing field offset/size metadata.
+    cdef int64_t current_offset = 
_get_subclass_offset(type_info.parent_type_info)
     cdef int64_t size, alignment
     cdef int32_t field_type_index
     cdef TVMFFIFieldGetter getter
@@ -1148,7 +1202,9 @@ def _register_fields(type_info, fields, 
structure_kind=None):
                 name=py_field.name,
                 doc=py_field.doc,
                 size=size,
+                alignment=alignment,
                 offset=field_offset,
+                field_static_type_index=field_type_index,
                 frozen=py_field.frozen,
                 metadata={"type_schema": py_field._ty_schema.to_json()},
                 getter=fgetter,
diff --git a/python/tvm_ffi/dataclasses/__init__.py 
b/python/tvm_ffi/dataclasses/__init__.py
index c629e669..2d36a876 100644
--- a/python/tvm_ffi/dataclasses/__init__.py
+++ b/python/tvm_ffi/dataclasses/__init__.py
@@ -22,6 +22,7 @@ from .c_class import c_class
 from .common import asdict, astuple, fields, is_dataclass, replace
 from .enum import Enum, EnumAttrMap, EnumState, IntEnum, StrEnum, auto, entry
 from .field import KW_ONLY, Field, field, init_property
+from .gen_abi_cpp import gen_abi_cpp
 from .py_class import py_class
 
 __all__ = [
@@ -41,6 +42,7 @@ __all__ = [
     "entry",
     "field",
     "fields",
+    "gen_abi_cpp",
     "init_property",
     "is_dataclass",
     "py_class",
diff --git a/python/tvm_ffi/dataclasses/c_class.py 
b/python/tvm_ffi/dataclasses/c_class.py
index b5cc5017..d9990a1f 100644
--- a/python/tvm_ffi/dataclasses/c_class.py
+++ b/python/tvm_ffi/dataclasses/c_class.py
@@ -35,7 +35,7 @@ from .field import Field, _field_converter, field
 _T = TypeVar("_T", bound=type)
 
 
-def _attach_field_objects(cls: type, type_info: Any) -> None:
+def _attach_field_objects(cls: type, type_info: Any, *, frozen: bool = False) 
-> None:
     """Populate ``TypeField.dataclass_field`` for every own reflected field.
 
     ``@c_class`` fields originate from C++ reflection, so there is no
@@ -48,6 +48,8 @@ def _attach_field_objects(cls: type, type_info: Any) -> None:
     except Exception:
         hints = {}
     for tf in type_info.fields:
+        if frozen:
+            tf.frozen = True
         f = Field(
             name=tf.name,
             _ty_schema=tf.ty,
@@ -66,6 +68,21 @@ def _attach_field_objects(cls: type, type_info: Any) -> None:
         tf.dataclass_field = f
 
 
+def _reinstall_field_properties(cls: type, type_info: Any, shadowed_names: 
set[str]) -> None:
+    """Reinstall reflected field descriptors after metadata changes.
+
+    ``register_object()`` installs field descriptors before ``@c_class`` can
+    apply decorator-level options.  When ``frozen=True`` updates
+    ``TypeField.frozen``, descriptors for non-shadowed fields must be recreated
+    so their public setters are removed.  User class attributes that shadow a
+    field remain untouched.
+    """
+    for tf in type_info.fields:
+        if tf.name in shadowed_names:
+            continue
+        setattr(cls, tf.name, tf.as_property(cls))
+
+
 @dataclass_transform(
     eq_default=False,
     order_default=False,
@@ -75,6 +92,7 @@ def _attach_field_objects(cls: type, type_info: Any) -> None:
 def c_class(
     type_key: str,
     *,
+    frozen: bool = False,
     init: bool = True,
     repr: bool = True,
     eq: bool = False,
@@ -108,6 +126,12 @@ def c_class(
         If True, install ``__eq__`` and ``__ne__`` using the C++ recursive
         structural comparison (``RecursiveEq``).  Returns ``NotImplemented``
         for unrelated types.  Defaults to False.
+    frozen
+        If True, fields owned by the decorated C++ type are read-only through
+        normal Python assignment.  Inherited fields keep the setting from their
+        declaring type.  Use ``type(obj).field_name.set(obj, value)`` as an
+        escape hatch when internal construction or translation code needs to
+        update a field.
     order
         If True, install ``__lt__``, ``__le__``, ``__gt__``, ``__ge__``
         using the C++ recursive comparators.  Returns ``NotImplemented``
diff --git a/python/tvm_ffi/dataclasses/gen_abi_cpp.py 
b/python/tvm_ffi/dataclasses/gen_abi_cpp.py
new file mode 100644
index 00000000..b7db91b2
--- /dev/null
+++ b/python/tvm_ffi/dataclasses/gen_abi_cpp.py
@@ -0,0 +1,682 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Generate lookup-only C++ ABI views from TVM-FFI reflection data."""
+
+from __future__ import annotations
+
+import fnmatch
+import json
+import re
+from collections.abc import Sequence
+from dataclasses import dataclass
+from itertools import groupby
+from typing import cast
+
+from ..core import (
+    TypeField,
+    TypeInfo,
+    TypeSchema,
+    _lookup_or_register_type_info_from_type_key,
+    _object_type_key_to_index,
+)
+from ..registry import get_registered_type_keys
+
+_IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*\Z")
+_ESCAPE_PREFIX = "__ffi_escape_"
+_OBJECT_SIZE = 24
+_OBJECT_ALIGNMENT = 8
+_OBJECT_TYPE_INDEX = 64
+_DYNAMIC_OBJECT_TYPE_INDEX_BEGIN = 128
+
+
+@dataclass(frozen=True)
+class _CppName:
+    namespaces: tuple[str, ...]
+    object_name: str
+
+    @property
+    def qualified(self) -> str:
+        return "::" + "::".join((*self.namespaces, self.object_name))
+
+
+@dataclass(frozen=True)
+class _BuiltinType:
+    object_type: str
+    value_type: str | None
+    size: int
+    alignment: int
+
+
+@dataclass(frozen=True)
+class _Carrier:
+    cpp_type: str
+    size: int
+    alignment: int
+
+
+_STATIC_CARRIERS = {
+    -1: _Carrier("::tvm::ffi::Any", 16, 8),
+    1: _Carrier("int64_t", 8, 8),
+    2: _Carrier("bool", 1, 1),
+    3: _Carrier("double", 8, 8),
+    4: _Carrier("void*", 8, 8),
+    5: _Carrier("DLDataType", 4, 2),
+    6: _Carrier("DLDevice", 8, 4),
+    _OBJECT_TYPE_INDEX: _Carrier("::tvm::ffi::ObjectPtr<::tvm::ffi::Object>", 
8, 8),
+}
+
+
+def _static_carrier(type_index: int) -> _Carrier | None:
+    carrier = _STATIC_CARRIERS.get(type_index)
+    if carrier is None and type_index >= _OBJECT_TYPE_INDEX:
+        return _Carrier("::tvm::ffi::ObjectPtr<::tvm::ffi::Object>", 8, 8)
+    return carrier
+
+
+@dataclass(frozen=True)
+class _FieldModel:
+    reflected_name: str
+    member_name: str
+    carrier: _Carrier
+    offset: int
+
+
+@dataclass(frozen=True)
+class _ClassModel:
+    info: TypeInfo
+    cpp_name: _CppName
+    base_cpp_type: str
+    alignment: int
+    total_size: int
+    fields: tuple[_FieldModel, ...]
+
+
+def _cpp_identifier(value: str) -> str:
+    if _IDENTIFIER_RE.fullmatch(value) and not 
value.startswith(_ESCAPE_PREFIX):
+        return value
+    return _ESCAPE_PREFIX + value.encode("utf-8").hex()
+
+
+def _cpp_string_literal(value: str) -> str:
+    """Encode UTF-8 bytes without depending on the compiler execution 
charset."""
+    chunks: list[str] = []
+    for byte in value.encode("utf-8"):
+        if byte == ord('"'):
+            chunks.append(r"\"")
+        elif byte == ord("\\"):
+            chunks.append(r"\\")
+        elif 0x20 <= byte <= 0x7E:
+            chunks.append(chr(byte))
+        else:
+            chunks.append(f"\\{byte:03o}")
+    return '"' + "".join(chunks) + '"'
+
+
+def _cpp_name(type_key: str) -> _CppName:
+    parts = type_key.split(".")
+    return _CppName(
+        namespaces=tuple(_cpp_identifier(part) for part in parts[:-1]),
+        object_name=f"{_cpp_identifier(parts[-1])}Obj",
+    )
+
+
+def _cpp_name_sort_key(info: TypeInfo) -> tuple[tuple[str, ...], str, str]:
+    name = _cpp_name(info.type_key)
+    return name.namespaces, name.object_name, info.type_key
+
+
+def _lineage(type_info: TypeInfo) -> list[TypeInfo]:
+    result: list[TypeInfo] = []
+    current = type_info
+    while current is not None:
+        result.append(current)
+        current = current.parent_type_info
+    result.reverse()
+    return result
+
+
+def _namespace_lines(
+    blocks: Sequence[tuple[tuple[str, ...], list[str]]],
+) -> list[str]:
+    lines: list[str] = []
+    for namespaces, group in groupby(blocks, key=lambda block: block[0]):
+        if lines:
+            lines.append("")
+        body: list[str] = []
+        for _, block in group:
+            if body:
+                body.append("")
+            body.extend(block)
+        lines.extend(f"namespace {namespace} {{" for namespace in namespaces)
+        if namespaces:
+            lines.append("")
+        lines.extend(body)
+        if namespaces:
+            lines.append("")
+        lines.extend(f"}}  // namespace {namespace}" for namespace in 
reversed(namespaces))
+    if lines:
+        lines.append("")
+    return lines
+
+
+def _validate_native_schema(schema: TypeSchema, raw: dict[str, object]) -> 
None:
+    """Reject normalized aliases whose native bytes have different 
semantics."""
+    origin = schema.origin
+    raw_origin = raw["type"]
+    canonical_origins = {
+        "int": {"int"},
+        "float": {"float"},
+        "bool": {"bool"},
+        "ctypes.c_void_p": {"void*"},
+        "dtype": {"DataType"},
+        "DataType": {"DataType"},
+        "Device": {"Device"},
+        "Any": {"Any"},
+        "str": {"ffi.String", "ffi.SmallStr"},
+        "bytes": {"ffi.Bytes", "ffi.SmallBytes"},
+        "Callable": {"ffi.Function"},
+        "Tensor": {"ffi.Tensor"},
+    }
+    allowed = canonical_origins.get(origin)
+    if allowed is not None:
+        if raw_origin not in allowed:
+            raise ValueError(
+                f"native schema origin {raw_origin!r} normalizes to {origin!r} 
but does not "
+                "have the canonical owning representation"
+            )
+        return
+
+    structural_origins = {
+        "Array": "ffi.Array",
+        "List": "ffi.List",
+        "Map": "ffi.Map",
+        "Dict": "ffi.Dict",
+        "tuple": "Tuple",
+        "Optional": "Optional",
+        "Union": "Variant",
+    }
+    expected_raw_origin = structural_origins.get(origin)
+    if expected_raw_origin is not None:
+        if raw_origin != expected_raw_origin:
+            raise ValueError(
+                f"native schema origin {raw_origin!r} normalizes to {origin!r} 
but is not "
+                f"the canonical {expected_raw_origin!r} carrier"
+            )
+        normalized_args = schema.args
+        raw_args = raw.get("args", ())
+        if not isinstance(raw_args, list) or len(raw_args) != 
len(normalized_args):
+            raise ValueError(f"raw schema {raw!r} does not match normalized 
schema {schema!r}")
+        for normalized_arg, raw_arg in zip(normalized_args, raw_args):
+            if not isinstance(raw_arg, dict) or not 
isinstance(raw_arg.get("type"), str):
+                raise ValueError(f"invalid nested raw schema {raw_arg!r}")
+            _validate_native_schema(normalized_arg, cast(dict[str, object], 
raw_arg))
+        return
+
+    if schema.origin_type_index >= _OBJECT_TYPE_INDEX:
+        expected = "ffi.Object" if origin == "Object" else origin
+        if raw_origin != expected:
+            raise ValueError(
+                f"native object schema origin {raw_origin!r} does not match 
{expected!r}"
+            )
+        return
+    raise ValueError(f"unsupported native raw schema {raw!r}")
+
+
+def _builtin_table() -> dict[int, _BuiltinType]:
+    specs = {
+        "ffi.Object": _BuiltinType("::tvm::ffi::Object", None, 8, 8),
+        "ffi.String": _BuiltinType("::tvm::ffi::details::StringObj", 
"::tvm::ffi::String", 16, 8),
+        "ffi.Bytes": _BuiltinType("::tvm::ffi::details::BytesObj", 
"::tvm::ffi::Bytes", 16, 8),
+        "ffi.Error": _BuiltinType("::tvm::ffi::ErrorObj", "::tvm::ffi::Error", 
16, 8),
+        "ffi.Function": _BuiltinType("::tvm::ffi::FunctionObj", 
"::tvm::ffi::Function", 8, 8),
+        "ffi.Shape": _BuiltinType("::tvm::ffi::ShapeObj", "::tvm::ffi::Shape", 
8, 8),
+        "ffi.Tensor": _BuiltinType("::tvm::ffi::TensorObj", 
"::tvm::ffi::Tensor", 8, 8),
+        "ffi.Array": _BuiltinType(
+            "::tvm::ffi::ArrayObj", "::tvm::ffi::Array<::tvm::ffi::Any>", 8, 8
+        ),
+        "ffi.Map": _BuiltinType(
+            "::tvm::ffi::MapObj", "::tvm::ffi::Map<::tvm::ffi::Any, 
::tvm::ffi::Any>", 8, 8
+        ),
+        "ffi.Module": _BuiltinType(
+            "::tvm::ffi::ModuleObj", 
"::tvm::ffi::ObjectPtr<::tvm::ffi::ModuleObj>", 8, 8
+        ),
+        # OpaquePyObject intentionally has no public C++ owning wrapper.
+        "ffi.List": _BuiltinType("::tvm::ffi::ListObj", 
"::tvm::ffi::List<::tvm::ffi::Any>", 8, 8),
+        "ffi.Dict": _BuiltinType(
+            "::tvm::ffi::DictObj",
+            "::tvm::ffi::Dict<::tvm::ffi::Any, ::tvm::ffi::Any>",
+            8,
+            8,
+        ),
+        "ffi.VisitInterrupt": _BuiltinType(
+            "::tvm::ffi::VisitInterruptObj",
+            "::tvm::ffi::ObjectPtr<::tvm::ffi::VisitInterruptObj>",
+            8,
+            8,
+        ),
+    }
+    result: dict[int, _BuiltinType] = {}
+    for type_key, spec in specs.items():
+        type_index = _object_type_key_to_index(type_key)
+        if type_index is not None:
+            result[type_index] = spec
+    return result
+
+
+def _select_type_infos(type_keys: str | Sequence[str]) -> list[TypeInfo]:
+    registered = sorted({str(key) for key in get_registered_type_keys()})
+    selected_keys: set[str] = set()
+    for selector in [type_keys] if isinstance(type_keys, str) else type_keys:
+        matches = [key for key in registered if fnmatch.fnmatchcase(key, 
selector)]
+        if not matches:
+            raise ValueError(f"Type-key selector {selector!r} did not match 
any registered type")
+        object_matches = []
+        for key in matches:
+            info = _lookup_or_register_type_info_from_type_key(key)
+            if info.type_index >= _OBJECT_TYPE_INDEX:
+                object_matches.append(key)
+        if not object_matches:
+            raise ValueError(
+                f"Type-key selector {selector!r} did not match any registered 
object type"
+            )
+        selected_keys.update(object_matches)
+
+    builtins = _builtin_table()
+    closure: dict[int, TypeInfo] = {}
+    for type_key in sorted(selected_keys):
+        info = _lookup_or_register_type_info_from_type_key(type_key)
+        if info.type_index < _DYNAMIC_OBJECT_TYPE_INDEX_BEGIN:
+            if info.type_index not in builtins:
+                raise ValueError(f"Static TVM-FFI type {info.type_key!r} is 
not supported")
+            continue
+        for ancestor in _lineage(info):
+            if ancestor.type_index >= _DYNAMIC_OBJECT_TYPE_INDEX_BEGIN:
+                closure[ancestor.type_index] = ancestor
+
+    return sorted(closure.values(), key=lambda info: 
(len(info.type_ancestors), info.type_key))
+
+
+class _Generator:
+    def __init__(self, emitted_infos: list[TypeInfo]) -> None:
+        self.emitted_infos = emitted_infos
+        self.builtins = _builtin_table()
+        self.dependencies: dict[int, TypeInfo] = {info.type_index: info for 
info in emitted_infos}
+
+    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)
+        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"
+                )
+            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
+        return _Carrier(f"::tvm::ffi::ObjectPtr<{object_type}>", 8, 8)
+
+    def _lower_value(
+        self,
+        schema: TypeSchema,
+        *,
+        container_argument: bool = False,
+        is_native_field: bool = False,
+    ) -> _Carrier:
+        origin = schema.origin
+        scalar = {
+            "int": _Carrier("int64_t", 8, 8),
+            "float": _Carrier("double", 8, 8),
+            "bool": _Carrier("bool", 1, 1),
+            "ctypes.c_void_p": _Carrier("void*", 8, 8),
+            "dtype": _Carrier("DLDataType", 4, 2),
+            "DataType": _Carrier("DLDataType", 4, 2),
+            "Device": _Carrier("DLDevice", 8, 4),
+            "Any": _Carrier("::tvm::ffi::Any", 16, 8),
+            "str": _Carrier("::tvm::ffi::String", 16, 8),
+            "bytes": _Carrier("::tvm::ffi::Bytes", 16, 8),
+            "Callable": _Carrier("::tvm::ffi::Function", 8, 8),
+            "Tensor": _Carrier("::tvm::ffi::Tensor", 8, 8),
+        }
+        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)
+
+        container_origins = {
+            "Array": "::tvm::ffi::Array",
+            "List": "::tvm::ffi::List",
+            "Map": "::tvm::ffi::Map",
+            "Dict": "::tvm::ffi::Dict",
+        }
+        if origin in container_origins:
+            lowered = [
+                self._lower_value(
+                    arg,
+                    container_argument=True,
+                    is_native_field=is_native_field,
+                ).cpp_type
+                for arg in args
+            ]
+            return _Carrier(f"{container_origins[origin]}<{', 
'.join(lowered)}>", 8, 8)
+
+        if origin == "tuple":
+            lowered = [
+                self._lower_value(
+                    arg,
+                    container_argument=True,
+                    is_native_field=is_native_field,
+                ).cpp_type
+                for arg in args
+            ]
+            return _Carrier(f"::tvm::ffi::Tuple<{', '.join(lowered)}>", 8, 8)
+
+        if schema.origin_type_index >= _OBJECT_TYPE_INDEX:
+            return self._lower_object(schema)
+        raise ValueError(f"Unsupported TVM-FFI type schema {schema!r}")
+
+    def _lower_field(self, field: TypeField, owner: TypeInfo) -> _Carrier:  # 
noqa: PLR0912
+        schema = field.ty
+        is_python_field = hasattr(owner, "_decorator_args")
+        if schema is not None and not is_python_field:
+            raw_schema: object = field.metadata.get("type_schema")
+            if isinstance(raw_schema, str):
+                try:
+                    raw_schema = json.loads(raw_schema)
+                except json.JSONDecodeError as err:
+                    raise ValueError(
+                        f"Invalid raw type schema for 
{owner.type_key}.{field.name}: {raw_schema!r}"
+                    ) from err
+            if not isinstance(raw_schema, dict) or not 
isinstance(raw_schema.get("type"), str):
+                raise ValueError(
+                    f"Missing raw type schema for native field 
{owner.type_key}.{field.name}"
+                )
+            _validate_native_schema(schema, cast(dict[str, object], 
raw_schema))
+        if schema is None:
+            carrier = _static_carrier(field.field_static_type_index)
+            if carrier is None:
+                raise ValueError(
+                    f"Cannot determine carrier for 
{owner.type_key}.{field.name}: "
+                    f"missing type schema and static type index 
{field.field_static_type_index}"
+                )
+        elif schema.origin in ("Optional", "Union"):
+            if is_python_field:
+                carrier = _Carrier("::tvm::ffi::Any", 16, 8)
+            elif schema.origin == "Optional":
+                value_schema = schema.args[0]
+                if value_schema.origin in ("str", "bytes") and (field.size, 
field.alignment) == (
+                    16,
+                    8,
+                ):
+                    carrier = self._lower_value(value_schema)
+                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)
+                else:
+                    raise ValueError(
+                        f"Ambiguous native Optional carrier for 
{owner.type_key}.{field.name} "
+                        f"with layout ({field.size}, {field.alignment})"
+                    )
+            elif (
+                schema.origin == "Union"
+                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)
+            else:
+                raise ValueError(
+                    f"Ambiguous native {schema.origin} carrier for 
{owner.type_key}.{field.name} "
+                    f"with layout ({field.size}, {field.alignment})"
+                )
+        else:
+            carrier = self._lower_value(schema, is_native_field=not 
is_python_field)
+
+        if schema is not None:
+            builtin = self.builtins.get(schema.origin_type_index)
+            if builtin is not None and builtin.value_type == 
"::tvm::ffi::Error":
+                # Error also derives from std::exception, whose size is a C++
+                # library ABI detail.  Reflection supplies the dimensions and
+                # the generated static assertions verify the local wrapper.
+                carrier = _Carrier(builtin.value_type, field.size, 
field.alignment)
+
+        actual = (field.size, field.alignment)
+        expected = (carrier.size, carrier.alignment)
+        if (
+            actual != expected
+            and actual == (8, 8)
+            and schema is not None
+            and schema.origin_type_index >= _OBJECT_TYPE_INDEX
+            and (builtin := self.builtins.get(schema.origin_type_index)) is 
not None
+        ):
+            # A few canonical reference wrappers (notably Error, which also
+            # 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)
+            expected = (8, 8)
+        if actual != expected:
+            raise ValueError(
+                f"Carrier {carrier.cpp_type} for {owner.type_key}.{field.name} 
has layout "
+                f"{expected}, but reflection reports {actual} at offset 
{field.offset}"
+            )
+        return carrier
+
+    def _build_class(self, info: TypeInfo) -> _ClassModel:
+        if not info._has_type_metadata and not hasattr(info, 
"_decorator_args"):
+            raise ValueError(
+                f"Native type {info.type_key!r} does not expose fixed 
total-size metadata"
+            )
+        parent = info.parent_type_info
+        if parent is None:
+            raise ValueError(f"Object type {info.type_key!r} does not expose 
its parent type")
+        if parent.type_index >= _DYNAMIC_OBJECT_TYPE_INDEX_BEGIN:
+            base_cpp_type = _cpp_name(parent.type_key).qualified
+        elif (builtin := self.builtins.get(parent.type_index)) is not None:
+            base_cpp_type = builtin.object_type
+        else:
+            raise ValueError(
+                f"Parent type {parent.type_key!r} of {info.type_key!r} is not 
supported"
+            )
+
+        total_size = int(info.total_size)
+        fields = tuple(
+            _FieldModel(
+                reflected_name=field.name,
+                member_name=_cpp_identifier(field.name),
+                carrier=self._lower_field(field, info),
+                offset=field.offset,
+            )
+            for field in sorted(info.fields or (), key=lambda field: 
field.offset)
+        )
+        alignment = max(
+            [_OBJECT_ALIGNMENT]
+            + [field.alignment for owner in _lineage(info) for field in 
(owner.fields or ())]
+        )
+        return _ClassModel(
+            info=info,
+            cpp_name=_cpp_name(info.type_key),
+            base_cpp_type=base_cpp_type,
+            alignment=alignment,
+            total_size=total_size,
+            fields=fields,
+        )
+
+    def build(self) -> str:
+        classes = [self._build_class(info) for info in self.emitted_infos]
+        lines = [
+            "#pragma once",
+            "",
+            "#include <tvm/ffi/tvm_ffi.h>",
+            "",
+        ]
+
+        dependencies = sorted(
+            self.dependencies.values(),
+            key=_cpp_name_sort_key,
+        )
+        class_indices = {model.info.type_index for model in classes}
+        opaque_dependencies = [
+            info for info in dependencies if info.type_index not in 
class_indices
+        ]
+        forward_blocks = []
+        for info in dependencies:
+            name = _cpp_name(info.type_key)
+            forward_blocks.append((name.namespaces, [f"struct 
{name.object_name};"]))
+        lines.extend(_namespace_lines(forward_blocks))
+        for info in dependencies:
+            name = _cpp_name(info.type_key)
+            lines.extend(
+                [
+                    "template <>",
+                    "inline constexpr bool "
+                    f"tvm::ffi::is_object_subclass_v<{name.qualified}> = 
true;",
+                ]
+            )
+        lines.append("")
+
+        opaque_blocks = []
+        for info in opaque_dependencies:
+            name = _cpp_name(info.type_key)
+            opaque_blocks.append(
+                (
+                    name.namespaces,
+                    [
+                        f"struct {name.object_name} : public 
::tvm::ffi::Object {{",
+                        "  TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("
+                        f"{_cpp_string_literal(info.type_key)}, 
{len(info.type_ancestors)});",
+                        "};",
+                    ],
+                )
+            )
+        lines.extend(_namespace_lines(opaque_blocks))
+
+        lines.append(f"static_assert(sizeof(::tvm::ffi::Object) == 
{_OBJECT_SIZE});")
+        lines.append(f"static_assert(alignof(::tvm::ffi::Object) == 
{_OBJECT_ALIGNMENT});")
+        lines.append("")
+        lines.extend(
+            [
+                "#if defined(__clang__) || defined(__GNUC__)",
+                "#pragma GCC diagnostic push",
+                '#pragma GCC diagnostic ignored "-Winvalid-offsetof"',
+                "#elif defined(_MSC_VER)",
+                "#pragma warning(push)",
+                "#pragma warning(disable : 4749)",
+                "#endif",
+                "",
+            ]
+        )
+        class_blocks = []
+        for model in classes:
+            body = [
+                f"struct alignas({model.alignment}) 
{model.cpp_name.object_name} "
+                f": public {model.base_cpp_type} {{",
+                "  TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("
+                f"{_cpp_string_literal(model.info.type_key)}, "
+                f"{len(model.info.type_ancestors)});",
+            ]
+            if model.fields:
+                body.append("")
+            for field in model.fields:
+                comment = ""
+                if field.member_name != field.reflected_name:
+                    comment = f", reflected name={field.reflected_name!r}"
+                body.append(
+                    f"  {field.carrier.cpp_type} {field.member_name};  "
+                    f"// offset={field.offset}, size={field.carrier.size}, "
+                    f"align={field.carrier.alignment}{comment}"
+                )
+            body.append("};")
+            body.append("")
+            body.append(
+                f"static_assert(sizeof({model.cpp_name.object_name}) == 
{model.total_size});"
+            )
+            body.append(
+                f"static_assert(alignof({model.cpp_name.object_name}) == 
{model.alignment});"
+            )
+            for field in model.fields:
+                body.extend(
+                    [
+                        
f"static_assert(sizeof(decltype({model.cpp_name.object_name}::{field.member_name}))
 "
+                        f"== {field.carrier.size});",
+                        
f"static_assert(alignof(decltype({model.cpp_name.object_name}::{field.member_name}))
 "
+                        f"== {field.carrier.alignment});",
+                        f"static_assert(offsetof({model.cpp_name.object_name}, 
"
+                        f"{field.member_name}) == {field.offset});",
+                    ]
+                )
+            class_blocks.append((model.cpp_name.namespaces, body))
+        lines.extend(_namespace_lines(class_blocks))
+        lines.extend(
+            [
+                "#if defined(__clang__) || defined(__GNUC__)",
+                "#pragma GCC diagnostic pop",
+                "#elif defined(_MSC_VER)",
+                "#pragma warning(pop)",
+                "#endif",
+                "",
+            ]
+        )
+        return "\n".join(lines)
+
+
+def gen_abi_cpp(type_keys: str | Sequence[str]) -> str:
+    """Generate one C++ header containing inheritance-preserving ABI views.
+
+    Parameters
+    ----------
+    type_keys
+        An exact registered type key, a shell-style pattern, or a sequence of
+        exact keys and patterns.
+
+    Returns
+    -------
+    str
+        Deterministic C++17 header source.  The source performs lookup only;
+        it does not register, compile, load, or allocate any type.
+
+    """
+    return _Generator(_select_type_infos(type_keys)).build()
+
+
+__all__ = ["gen_abi_cpp"]
diff --git a/python/tvm_ffi/testing/__init__.py 
b/python/tvm_ffi/testing/__init__.py
index 9ac65d73..4d1e027b 100644
--- a/python/tvm_ffi/testing/__init__.py
+++ b/python/tvm_ffi/testing/__init__.py
@@ -23,6 +23,7 @@ from .testing import (
     TestCustomCompare,
     TestCustomHash,
     TestEqWithoutHash,
+    TestFrozenCxx,
     TestHash,
     TestIntPair,
     TestNonCopyable,
diff --git a/python/tvm_ffi/testing/testing.py 
b/python/tvm_ffi/testing/testing.py
index 3c9dd624..e28af64c 100644
--- a/python/tvm_ffi/testing/testing.py
+++ b/python/tvm_ffi/testing/testing.py
@@ -92,6 +92,23 @@ class TestIntPair(Object):
     # tvm-ffi-stubgen(end)
 
 
+@c_class("testing.TestFrozenCxx", frozen=True)
+class TestFrozenCxx(Object):
+    """C++ object with writable reflection fields frozen by ``@c_class``."""
+
+    __test__: ClassVar[bool] = False
+
+    # tvm-ffi-stubgen(begin): object/testing.TestFrozenCxx
+    # fmt: off
+    value: int
+    tag: str
+    if TYPE_CHECKING:
+        def __init__(self, value: int, tag: str) -> None: ...
+        def __ffi_init__(self, value: int, tag: str) -> None: ...  # ty: 
ignore[invalid-method-override]
+    # fmt: on
+    # tvm-ffi-stubgen(end)
+
+
 @c_class("testing.TestObjectDerived")
 class TestObjectDerived(TestObjectBase):
     """Test object derived class."""
diff --git a/src/ffi/extra/structural_visit.cc 
b/src/ffi/extra/structural_visit.cc
index 4002db7a..34967f5e 100644
--- a/src/ffi/extra/structural_visit.cc
+++ b/src/ffi/extra/structural_visit.cc
@@ -133,9 +133,8 @@ TVMFFIAny VisitDict(StructuralVisitorObj* visitor, AnyView 
value) noexcept {
 
 TVM_FFI_STATIC_INIT_BLOCK() {
   namespace refl = tvm::ffi::reflection;
-  refl::ObjectDef<VisitInterruptObj>()
-      .def(refl::init<Any>(), "Constructor that creates a structural visit 
interrupt")
-      .def_ro("value", &VisitInterruptObj::value);
+  refl::ObjectDef<VisitInterruptObj>().def_ro("value", 
&VisitInterruptObj::value,
+                                              refl::default_value(nullptr));
   refl::ObjectDef<StructuralVisitorObj>().def(
       refl::init<>(), "Constructor that creates a default structural visitor");
   refl::GlobalDef()
diff --git a/src/ffi/testing/testing.cc b/src/ffi/testing/testing.cc
index 501f71a9..7c9e47a4 100644
--- a/src/ffi/testing/testing.cc
+++ b/src/ffi/testing/testing.cc
@@ -71,6 +71,17 @@ class TestIntPair : public tvm::ffi::ObjectRef {
   TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(TestIntPair, tvm::ffi::ObjectRef, 
TestIntPairObj);
 };
 
+class TestFrozenCxxObj : public tvm::ffi::Object {
+ public:
+  static constexpr bool _type_mutable = true;
+  int64_t value;
+  String tag;
+
+  TestFrozenCxxObj() = default;
+
+  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("testing.TestFrozenCxx", TestFrozenCxxObj, 
tvm::ffi::Object);
+};
+
 TVM_FFI_STATIC_INIT_BLOCK() {
   namespace refl = tvm::ffi::reflection;
   refl::ObjectDef<TestIntPairObj>()
diff --git a/tests/cpp/test_abi_object.cc b/tests/cpp/test_abi_object.cc
new file mode 100644
index 00000000..78badca8
--- /dev/null
+++ b/tests/cpp/test_abi_object.cc
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include <tvm/ffi/tvm_ffi.h>
+
+#include <type_traits>
+
+namespace abi_object_test {
+
+struct BaseObj;
+struct DerivedObj;
+struct GrandchildObj;
+struct RecursiveObj;
+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.
+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>>);
+
+namespace abi_object_test {
+
+struct BaseObj : public ::tvm::ffi::Object {
+  TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.abi_object.Base", 1);
+};
+
+struct DerivedObj : public BaseObj {
+  TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.abi_object.Derived", 2);
+};
+
+struct GrandchildObj : public DerivedObj {
+  TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.abi_object.Grandchild", 3);
+};
+
+struct RecursiveObj : public ::tvm::ffi::Object {
+  ::tvm::ffi::List<::tvm::ffi::ObjectPtr<RecursiveObj>> children;
+
+  TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.abi_object.Recursive", 1);
+};
+
+struct UnrelatedObj : public ::tvm::ffi::Object {
+  TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.abi_object.Unrelated", 1);
+};
+
+template <typename T, typename = void>
+struct HasRuntimeTypeInfo : std::false_type {};
+
+template <typename T>
+struct HasRuntimeTypeInfo<T, std::void_t<decltype(T::RuntimeTypeInfo())>> : 
std::true_type {};
+
+static_assert(!HasRuntimeTypeInfo<BaseObj>::value);
+static_assert(std::is_base_of_v<::tvm::ffi::Object, BaseObj> &&
+              std::is_base_of_v<BaseObj, DerivedObj> &&
+              std::is_base_of_v<DerivedObj, GrandchildObj>);
+static_assert(std::is_convertible_v<GrandchildObj*, BaseObj*>);
+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(::tvm::ffi::type_subsumes_v<::tvm::ffi::ObjectPtr<BaseObj>,
+                                          
::tvm::ffi::ObjectPtr<GrandchildObj>>);
+static_assert(!::tvm::ffi::type_subsumes_v<::tvm::ffi::ObjectPtr<UnrelatedObj>,
+                                           ::tvm::ffi::ObjectPtr<DerivedObj>>);
+
+}  // namespace abi_object_test
diff --git a/tests/cpp/test_any.cc b/tests/cpp/test_any.cc
index aeaaf506..a82ad8b6 100644
--- a/tests/cpp/test_any.cc
+++ b/tests/cpp/test_any.cc
@@ -40,8 +40,8 @@ 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(is_object_subclass_v<TIntObj>);
+static_assert(!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>>);
diff --git a/tests/cpp/test_dict.cc b/tests/cpp/test_dict.cc
index 6e6acaaf..ffdf66c9 100644
--- a/tests/cpp/test_dict.cc
+++ b/tests/cpp/test_dict.cc
@@ -164,6 +164,7 @@ TEST(Dict, AnyConversion) {
   Any any_d = d;
   auto d2 = any_d.cast<Dict<Any, Any>>();
   EXPECT_EQ(d2.size(), 1);
+  EXPECT_TRUE(d2.same_as(d));
 }
 
 TEST(Dict, InitializerList) {
diff --git a/tests/cpp/test_list.cc b/tests/cpp/test_list.cc
index c17dc53c..a96bfee2 100644
--- a/tests/cpp/test_list.cc
+++ b/tests/cpp/test_list.cc
@@ -140,6 +140,13 @@ TEST(List, AnyImplicitConversionFromArray) {
   AnyView list_view = list_any;
   List<Any> list_any_roundtrip = list_view.cast<List<Any>>();
   EXPECT_TRUE(list_any_roundtrip.same_as(list_any));
+
+  Any owned_list = list_any;
+  List<Any> moved_roundtrip = std::move(owned_list).cast<List<Any>>();
+  EXPECT_TRUE(moved_roundtrip.same_as(list_any));
+  ASSERT_EQ(moved_roundtrip.size(), 2);
+  EXPECT_EQ(moved_roundtrip[0].cast<int>(), 1);
+  EXPECT_EQ(moved_roundtrip[1].cast<int>(), 2);
 }
 
 TEST(List, AnyConvertCheck) {
diff --git a/tests/cpp/test_object_ptr.cc b/tests/cpp/test_object_ptr.cc
new file mode 100644
index 00000000..c8811bd1
--- /dev/null
+++ b/tests/cpp/test_object_ptr.cc
@@ -0,0 +1,442 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include <gtest/gtest.h>
+#include <tvm/ffi/container/dict.h>
+#include <tvm/ffi/tvm_ffi.h>
+#include <tvm/ffi/type_traits.h>
+
+#include <type_traits>
+#include <utility>
+
+namespace tvm {
+namespace ffi {
+namespace testing {
+
+class GeneratedBaseObj;
+class GeneratedDerivedObj;
+class GeneratedUnrelatedObj;
+class MutualLeftObj;
+class MutualRightObj;
+
+}  // namespace testing
+}  // namespace ffi
+}  // namespace tvm
+
+template <>
+inline constexpr bool 
tvm::ffi::is_object_subclass_v<::tvm::ffi::testing::MutualLeftObj> = true;
+template <>
+inline constexpr bool 
tvm::ffi::is_object_subclass_v<::tvm::ffi::testing::MutualRightObj> = true;
+
+static_assert(::tvm::ffi::details::storage_enabled_v<
+              ::tvm::ffi::ObjectPtr<::tvm::ffi::testing::MutualLeftObj>>);
+static_assert(::tvm::ffi::details::storage_enabled_v<
+              ::tvm::ffi::ObjectPtr<::tvm::ffi::testing::MutualRightObj>>);
+
+namespace tvm {
+namespace ffi {
+namespace testing {
+
+class GeneratedBaseObj : public Object {
+ public:
+  TVM_FFI_DECLARE_OBJECT_INFO("testing.GeneratedBase", GeneratedBaseObj, 
Object);
+};
+
+class GeneratedDerivedObj : public GeneratedBaseObj {
+ public:
+  int64_t value;
+
+  explicit GeneratedDerivedObj(int64_t value) : value(value) {}
+
+  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("testing.GeneratedDerived", 
GeneratedDerivedObj,
+                                    GeneratedBaseObj);
+};
+
+class GeneratedUnrelatedObj : public Object {
+ public:
+  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("testing.GeneratedUnrelated", 
GeneratedUnrelatedObj, Object);
+};
+
+class MutualLeftObj : public Object {
+ public:
+  List<ObjectPtr<MutualRightObj>> right;
+
+  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("testing.MutualLeft", MutualLeftObj, 
Object);
+};
+
+class MutualRightObj : public Object {
+ public:
+  List<ObjectPtr<MutualLeftObj>> left;
+
+  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("testing.MutualRight", MutualRightObj, 
Object);
+};
+
+class CxxBaseObj : public Object {
+ public:
+  TVM_FFI_DECLARE_OBJECT_INFO("testing.CxxBase", CxxBaseObj, Object);
+};
+
+class CxxDerivedObj : public CxxBaseObj {
+ public:
+  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("testing.CxxDerived", CxxDerivedObj, 
CxxBaseObj);
+};
+
+class PointerAdjustmentPad {
+ public:
+  int64_t padding[4];
+};
+
+class PointerAdjustedObj : public PointerAdjustmentPad, public CxxBaseObj {
+ public:
+  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("testing.PointerAdjusted", 
PointerAdjustedObj, CxxBaseObj);
+};
+
+}  // namespace testing
+}  // namespace ffi
+}  // namespace tvm
+
+namespace {
+
+using tvm::ffi::Any;
+using tvm::ffi::Array;
+using tvm::ffi::Dict;
+using tvm::ffi::List;
+using tvm::ffi::make_object;
+using tvm::ffi::Map;
+using tvm::ffi::Object;
+using tvm::ffi::ObjectPtr;
+using tvm::ffi::ObjectRef;
+using tvm::ffi::String;
+using tvm::ffi::TypeTraits;
+using tvm::ffi::UnsafeInit;
+using tvm::ffi::testing::CxxBaseObj;
+using tvm::ffi::testing::CxxDerivedObj;
+using tvm::ffi::testing::GeneratedBaseObj;
+using tvm::ffi::testing::GeneratedDerivedObj;
+using tvm::ffi::testing::GeneratedUnrelatedObj;
+using tvm::ffi::testing::MutualLeftObj;
+using tvm::ffi::testing::MutualRightObj;
+using tvm::ffi::testing::PointerAdjustedObj;
+
+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*>);
+static_assert(std::is_constructible_v<ObjectPtr<GeneratedBaseObj>, 
ObjectPtr<GeneratedDerivedObj>>);
+static_assert(std::is_assignable_v<ObjectPtr<GeneratedBaseObj>&, 
ObjectPtr<GeneratedDerivedObj>>);
+static_assert(tvm::ffi::is_object_subclass_v<CxxBaseObj>);
+static_assert(tvm::ffi::is_object_subclass_v<CxxDerivedObj>);
+static_assert(std::is_convertible_v<CxxDerivedObj*, CxxBaseObj*>);
+static_assert(std::is_constructible_v<ObjectPtr<CxxBaseObj>, 
ObjectPtr<CxxDerivedObj>>);
+static_assert(std::is_assignable_v<ObjectPtr<CxxBaseObj>&, 
ObjectPtr<CxxDerivedObj>>);
+static_assert(!TypeTraits<ObjectPtr<int>>::storage_enabled);
+static_assert(!tvm::ffi::details::storage_enabled_v<ObjectPtr<int>>);
+static_assert(
+    tvm::ffi::type_subsumes_v<ObjectPtr<GeneratedBaseObj>, 
ObjectPtr<GeneratedDerivedObj>>);
+static_assert(
+    !tvm::ffi::type_subsumes_v<ObjectPtr<GeneratedDerivedObj>, 
ObjectPtr<GeneratedBaseObj>>);
+
+static_assert(std::is_same_v<Array<ObjectPtr<GeneratedDerivedObj>>::value_type,
+                             ObjectPtr<GeneratedDerivedObj>>);
+static_assert(std::is_same_v<List<ObjectPtr<GeneratedDerivedObj>>::value_type,
+                             ObjectPtr<GeneratedDerivedObj>>);
+static_assert(std::is_same_v<Map<String, 
ObjectPtr<GeneratedDerivedObj>>::mapped_type,
+                             ObjectPtr<GeneratedDerivedObj>>);
+static_assert(std::is_same_v<Dict<String, 
ObjectPtr<GeneratedDerivedObj>>::mapped_type,
+                             ObjectPtr<GeneratedDerivedObj>>);
+static_assert(std::is_same_v<Map<ObjectPtr<GeneratedDerivedObj>, 
String>::key_type,
+                             ObjectPtr<GeneratedDerivedObj>>);
+static_assert(std::is_same_v<Dict<ObjectPtr<GeneratedDerivedObj>, 
String>::key_type,
+                             ObjectPtr<GeneratedDerivedObj>>);
+static_assert(
+    std::is_same_v<List<ObjectPtr<MutualRightObj>>::value_type, 
ObjectPtr<MutualRightObj>>);
+static_assert(std::is_same_v<List<ObjectPtr<MutualLeftObj>>::value_type, 
ObjectPtr<MutualLeftObj>>);
+static_assert(std::is_constructible_v<Array<ObjectPtr<GeneratedBaseObj>>,
+                                      Array<ObjectPtr<GeneratedDerivedObj>>>);
+static_assert(std::is_constructible_v<List<ObjectPtr<GeneratedBaseObj>>,
+                                      List<ObjectPtr<GeneratedDerivedObj>>>);
+static_assert(std::is_constructible_v<Map<String, ObjectPtr<GeneratedBaseObj>>,
+                                      Map<String, 
ObjectPtr<GeneratedDerivedObj>>>);
+static_assert(std::is_constructible_v<Dict<String, 
ObjectPtr<GeneratedBaseObj>>,
+                                      Dict<String, 
ObjectPtr<GeneratedDerivedObj>>>);
+static_assert(!std::is_constructible_v<Array<ObjectPtr<GeneratedDerivedObj>>,
+                                       Array<ObjectPtr<GeneratedBaseObj>>>);
+static_assert(sizeof(Array<ObjectPtr<GeneratedDerivedObj>>) == 
sizeof(ObjectPtr<Object>));
+static_assert(sizeof(List<ObjectPtr<GeneratedDerivedObj>>) == 
sizeof(ObjectPtr<Object>));
+static_assert(sizeof(Map<String, ObjectPtr<GeneratedDerivedObj>>) == 
sizeof(ObjectPtr<Object>));
+static_assert(sizeof(Dict<String, ObjectPtr<GeneratedDerivedObj>>) == 
sizeof(ObjectPtr<Object>));
+
+TEST(ObjectPtr, NativeUpcastPreservesOwnershipAndPointer) {
+  ObjectPtr<GeneratedDerivedObj> derived = 
make_object<GeneratedDerivedObj>(42);
+  EXPECT_EQ(derived.use_count(), 1);
+
+  ObjectPtr<GeneratedBaseObj> base = derived;
+  EXPECT_EQ(derived.use_count(), 2);
+  EXPECT_EQ(reinterpret_cast<const void*>(derived.get()),
+            reinterpret_cast<const void*>(base.get()));
+
+  ObjectPtr<GeneratedDerivedObj> move_source = 
make_object<GeneratedDerivedObj>(43);
+  const void* move_source_address = move_source.get();
+  ObjectPtr<GeneratedBaseObj> moved = std::move(move_source);
+  // ObjectPtr documents a null moved-from state, so inspecting it here is 
intentional.
+  EXPECT_TRUE(move_source ==  // 
NOLINT(bugprone-use-after-move,clang-analyzer-cplusplus.Move)
+              nullptr);
+  EXPECT_EQ(moved.use_count(), 1);
+  EXPECT_EQ(reinterpret_cast<const void*>(moved.get()), move_source_address);
+
+  ObjectPtr<GeneratedBaseObj> assigned;
+  assigned = derived;
+  EXPECT_EQ(derived.use_count(), 3);
+  EXPECT_EQ(reinterpret_cast<const void*>(derived.get()),
+            reinterpret_cast<const void*>(assigned.get()));
+
+  EXPECT_EQ(static_cast<GeneratedBaseObj*>(derived.get()), base.get());
+  const GeneratedDerivedObj* const_derived = derived.get();
+  EXPECT_EQ(static_cast<const GeneratedBaseObj*>(const_derived), base.get());
+  
EXPECT_EQ(static_cast<GeneratedBaseObj*>(static_cast<GeneratedDerivedObj*>(nullptr)),
 nullptr);
+  EXPECT_EQ(derived.use_count(), 3);
+}
+
+TEST(ObjectPtr, PhysicalUpcastConstructorsPreserveOwnership) {
+  ObjectPtr<CxxDerivedObj> derived = make_object<CxxDerivedObj>();
+  ObjectPtr<CxxBaseObj> copied = derived;
+  EXPECT_EQ(derived.use_count(), 2);
+
+  ObjectPtr<CxxBaseObj> moved = std::move(derived);
+  EXPECT_TRUE(derived ==  // 
NOLINT(bugprone-use-after-move,clang-analyzer-cplusplus.Move)
+              nullptr);
+  EXPECT_EQ(copied.use_count(), 2);
+  EXPECT_EQ(moved.use_count(), 2);
+
+  ObjectPtr<PointerAdjustedObj> adjusted = make_object<PointerAdjustedObj>();
+  PointerAdjustedObj* adjusted_raw = adjusted.get();
+  CxxBaseObj* adjusted_base_raw = adjusted_raw;
+  EXPECT_NE(static_cast<const void*>(adjusted_raw), static_cast<const 
void*>(adjusted_base_raw));
+
+  ObjectPtr<CxxBaseObj> adjusted_base = adjusted;
+  EXPECT_EQ(adjusted_base.get(), adjusted_base_raw);
+  EXPECT_EQ(adjusted.use_count(), 2);
+}
+
+TEST(ObjectPtr, AnyRoundTripUsesRuntimeAncestry) {
+  ObjectPtr<GeneratedDerivedObj> derived = make_object<GeneratedDerivedObj>(7);
+  Any value = derived;
+
+  ObjectPtr<GeneratedBaseObj> base = value.cast<ObjectPtr<GeneratedBaseObj>>();
+  EXPECT_EQ(reinterpret_cast<const void*>(derived.get()),
+            reinterpret_cast<const void*>(base.get()));
+  EXPECT_FALSE(value.try_cast<ObjectPtr<GeneratedUnrelatedObj>>().has_value());
+
+  Any null_value = ObjectPtr<GeneratedDerivedObj>(nullptr);
+  EXPECT_EQ(null_value.type_index(), tvm::ffi::TypeIndex::kTVMFFINone);
+  EXPECT_EQ(null_value.cast<ObjectPtr<GeneratedBaseObj>>(), nullptr);
+}
+
+TEST(ObjectPtr, ContainerCovariance) {
+  ObjectPtr<GeneratedDerivedObj> first = make_object<GeneratedDerivedObj>(1);
+  ObjectPtr<GeneratedDerivedObj> second = make_object<GeneratedDerivedObj>(2);
+
+  Array<ObjectPtr<GeneratedDerivedObj>> derived_array{first};
+  Array<ObjectPtr<GeneratedBaseObj>> base_array = derived_array;
+  EXPECT_TRUE(base_array.same_as(derived_array));
+  EXPECT_EQ(reinterpret_cast<const void*>(base_array[0].get()),
+            reinterpret_cast<const void*>(first.get()));
+  base_array.push_back(second);
+  EXPECT_FALSE(base_array.same_as(derived_array));
+  EXPECT_EQ(derived_array.size(), 1);
+
+  Map<String, ObjectPtr<GeneratedDerivedObj>> derived_map{{"first", first}};
+  Map<String, ObjectPtr<GeneratedBaseObj>> base_map = derived_map;
+  EXPECT_TRUE(base_map.same_as(derived_map));
+  base_map.Set("second", second);
+  EXPECT_FALSE(base_map.same_as(derived_map));
+  EXPECT_EQ(derived_map.count("second"), 0);
+
+  List<ObjectPtr<GeneratedDerivedObj>> derived_list{first};
+  List<ObjectPtr<GeneratedBaseObj>> base_list = derived_list;
+  EXPECT_TRUE(base_list.same_as(derived_list));
+  base_list.push_back(second);
+  EXPECT_EQ(derived_list.size(), 2);
+
+  Dict<String, ObjectPtr<GeneratedDerivedObj>> derived_dict{{"first", first}};
+  Dict<String, ObjectPtr<GeneratedBaseObj>> base_dict = derived_dict;
+  EXPECT_TRUE(base_dict.same_as(derived_dict));
+  base_dict.Set("second", second);
+  EXPECT_EQ(derived_dict.count("second"), 1);
+
+  List<ObjectPtr<GeneratedDerivedObj>> move_list_source{first};
+  ObjectRef move_list_storage = move_list_source;
+  List<ObjectPtr<GeneratedBaseObj>> moved_list = std::move(move_list_source);
+  EXPECT_TRUE(moved_list.same_as(move_list_storage));
+  EXPECT_FALSE(move_list_source.defined());  // NOLINT(bugprone-use-after-move)
+  EXPECT_EQ(moved_list.size(), 1);
+
+  Dict<String, ObjectPtr<GeneratedDerivedObj>> move_dict_source{{"first", 
first}};
+  ObjectRef move_dict_storage = move_dict_source;
+  Dict<String, ObjectPtr<GeneratedBaseObj>> moved_dict = 
std::move(move_dict_source);
+  EXPECT_TRUE(moved_dict.same_as(move_dict_storage));
+  EXPECT_FALSE(move_dict_source.defined());  // NOLINT(bugprone-use-after-move)
+  EXPECT_EQ(moved_dict.size(), 1);
+
+  Map<ObjectPtr<GeneratedDerivedObj>, String> derived_key_map{{first, 
"first"}};
+  Map<ObjectPtr<GeneratedBaseObj>, String> base_key_map = derived_key_map;
+  EXPECT_TRUE(base_key_map.same_as(derived_key_map));
+  EXPECT_EQ(base_key_map.at(first), "first");
+
+  Dict<ObjectPtr<GeneratedDerivedObj>, String> derived_key_dict{{first, 
"first"}};
+  Dict<ObjectPtr<GeneratedBaseObj>, String> base_key_dict = derived_key_dict;
+  EXPECT_TRUE(base_key_dict.same_as(derived_key_dict));
+  EXPECT_EQ(base_key_dict.at(first), "first");
+
+  Array<ObjectPtr<GeneratedDerivedObj>> nullable_array{nullptr, first};
+  auto iterator = nullable_array.begin();
+  EXPECT_EQ(*iterator, nullptr);
+  ++iterator;
+  EXPECT_EQ(*iterator, first);
+}
+
+TEST(ObjectPtr, ExplicitPointerContainerSchemas) {
+  ObjectPtr<GeneratedDerivedObj> value = make_object<GeneratedDerivedObj>(3);
+  Array<ObjectPtr<GeneratedDerivedObj>> derived_array{value};
+  Array<ObjectPtr<GeneratedBaseObj>> base_array = derived_array;
+  EXPECT_TRUE(derived_array.same_as(base_array));
+
+  EXPECT_EQ(
+      TypeTraits<Array<ObjectPtr<GeneratedDerivedObj>>>::TypeSchema(),
+      
R"({"type":"ffi.Array","args":[{"type":"Optional","args":[{"type":"testing.GeneratedDerived"}]}]})");
+  EXPECT_EQ(
+      TypeTraits<List<ObjectPtr<GeneratedDerivedObj>>>::TypeSchema(),
+      
R"({"type":"ffi.List","args":[{"type":"Optional","args":[{"type":"testing.GeneratedDerived"}]}]})");
+
+  Array<Any> left{1};
+  Array<Any> right{2};
+  Array<Any> concatenated = tvm::ffi::Concat(left, right);
+  ASSERT_EQ(concatenated.size(), 2);
+  EXPECT_EQ(concatenated[0].cast<int64_t>(), 1);
+  EXPECT_EQ(concatenated[1].cast<int64_t>(), 2);
+}
+
+TEST(ObjectPtr, NullMutableContainerConversionsStayNull) {
+  List<int64_t> list_copy_source(UnsafeInit{});
+  List<Any> list_copy(list_copy_source);
+  EXPECT_FALSE(list_copy.defined());
+
+  List<int64_t> list_move_source(UnsafeInit{});
+  List<Any> list_move(std::move(list_move_source));
+  EXPECT_FALSE(list_move.defined());
+
+  List<Any> list_copy_assignment;
+  list_copy_assignment = list_copy_source;
+  EXPECT_FALSE(list_copy_assignment.defined());
+
+  List<int64_t> list_move_assignment_source(UnsafeInit{});
+  List<Any> list_move_assignment;
+  list_move_assignment = std::move(list_move_assignment_source);
+  EXPECT_FALSE(list_move_assignment.defined());
+
+  Dict<String, int64_t> dict_copy_source(UnsafeInit{});
+  Dict<String, Any> dict_copy(dict_copy_source);
+  EXPECT_FALSE(dict_copy.defined());
+
+  Dict<String, int64_t> dict_move_source(UnsafeInit{});
+  Dict<String, Any> dict_move(std::move(dict_move_source));
+  EXPECT_FALSE(dict_move.defined());
+
+  Dict<String, Any> dict_copy_assignment;
+  dict_copy_assignment = dict_copy_source;
+  EXPECT_FALSE(dict_copy_assignment.defined());
+
+  Dict<String, int64_t> dict_move_assignment_source(UnsafeInit{});
+  Dict<String, Any> dict_move_assignment;
+  dict_move_assignment = std::move(dict_move_assignment_source);
+  EXPECT_FALSE(dict_move_assignment.defined());
+}
+
+TEST(ObjectPtr, ErasedContainerUpcastsShareBacking) {
+  ObjectPtr<GeneratedDerivedObj> derived = make_object<GeneratedDerivedObj>(1);
+
+  Array<ObjectPtr<GeneratedDerivedObj>> narrow_array{derived};
+  Any erased_array = narrow_array;
+  Array<ObjectPtr<GeneratedBaseObj>> wide_array =
+      erased_array.cast<Array<ObjectPtr<GeneratedBaseObj>>>();
+  EXPECT_TRUE(wide_array.same_as(narrow_array));
+
+  Map<String, ObjectPtr<GeneratedDerivedObj>> narrow_map{{"derived", derived}};
+  Any erased_map = narrow_map;
+  Map<String, ObjectPtr<GeneratedBaseObj>> wide_map =
+      erased_map.cast<Map<String, ObjectPtr<GeneratedBaseObj>>>();
+  EXPECT_TRUE(wide_map.same_as(narrow_map));
+
+  List<ObjectPtr<GeneratedDerivedObj>> narrow_list{derived};
+  Any erased_list = narrow_list;
+  List<ObjectPtr<GeneratedBaseObj>> wide_list =
+      erased_list.cast<List<ObjectPtr<GeneratedBaseObj>>>();
+  EXPECT_TRUE(wide_list.same_as(narrow_list));
+  wide_list.push_back(derived);
+  EXPECT_EQ(narrow_list.size(), 2);
+
+  Any moved_list = narrow_list;
+  List<ObjectPtr<GeneratedBaseObj>> moved_wide_list =
+      std::move(moved_list).cast<List<ObjectPtr<GeneratedBaseObj>>>();
+  EXPECT_TRUE(moved_wide_list.same_as(narrow_list));
+  ASSERT_EQ(moved_wide_list.size(), 2);
+  moved_wide_list.push_back(derived);
+  EXPECT_EQ(narrow_list.size(), 3);
+
+  const ObjectRef& erased_list_ref = narrow_list;
+  std::optional<List<ObjectPtr<GeneratedBaseObj>>> list_from_ref =
+      erased_list_ref.as<List<ObjectPtr<GeneratedBaseObj>>>();
+  ASSERT_TRUE(list_from_ref.has_value());
+  EXPECT_TRUE(list_from_ref->same_as(  // 
NOLINT(bugprone-unchecked-optional-access)
+      narrow_list));
+  list_from_ref->push_back(derived);  // 
NOLINT(bugprone-unchecked-optional-access)
+  EXPECT_EQ(narrow_list.size(), 4);
+
+  const ObjectRef& throwing_list_ref = narrow_list;
+  List<ObjectPtr<GeneratedBaseObj>> throwing_list =
+      throwing_list_ref.as_or_throw<List<ObjectPtr<GeneratedBaseObj>>>();
+  EXPECT_TRUE(throwing_list.same_as(narrow_list));
+  throwing_list.push_back(derived);
+  EXPECT_EQ(narrow_list.size(), 5);
+
+  Dict<String, ObjectPtr<GeneratedDerivedObj>> narrow_dict{{"derived", 
derived}};
+  Any erased_dict = narrow_dict;
+  Dict<String, ObjectPtr<GeneratedBaseObj>> wide_dict =
+      erased_dict.cast<Dict<String, ObjectPtr<GeneratedBaseObj>>>();
+  EXPECT_TRUE(wide_dict.same_as(narrow_dict));
+  wide_dict.Set("wide", derived);
+  EXPECT_EQ(narrow_dict.count("wide"), 1);
+
+  const ObjectRef& erased_dict_ref = narrow_dict;
+  std::optional<Dict<String, ObjectPtr<GeneratedBaseObj>>> dict_from_ref =
+      erased_dict_ref.as<Dict<String, ObjectPtr<GeneratedBaseObj>>>();
+  ASSERT_TRUE(dict_from_ref.has_value());
+  EXPECT_TRUE(dict_from_ref->same_as(  // 
NOLINT(bugprone-unchecked-optional-access)
+      narrow_dict));
+  dict_from_ref->Set("ref", derived);  // 
NOLINT(bugprone-unchecked-optional-access)
+  EXPECT_EQ(narrow_dict.count("ref"), 1);
+
+  ObjectRef throwing_dict_ref = narrow_dict;
+  Dict<String, ObjectPtr<GeneratedBaseObj>> throwing_dict =
+      std::move(throwing_dict_ref).as_or_throw<Dict<String, 
ObjectPtr<GeneratedBaseObj>>>();
+  EXPECT_TRUE(throwing_dict.same_as(narrow_dict));
+  throwing_dict.Set("throwing", derived);
+  EXPECT_EQ(narrow_dict.count("throwing"), 1);
+}
+
+}  // namespace
diff --git a/tests/python/test_dataclass_gen_abi_cpp.py 
b/tests/python/test_dataclass_gen_abi_cpp.py
new file mode 100644
index 00000000..edf8c70a
--- /dev/null
+++ b/tests/python/test_dataclass_gen_abi_cpp.py
@@ -0,0 +1,1020 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Tests for C++ ABI-view generation from reflected dataclasses."""
+
+from __future__ import annotations
+
+import ctypes
+import sys
+from pathlib import Path
+from types import SimpleNamespace
+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.dataclasses import gen_abi_cpp, py_class
+from tvm_ffi.dataclasses.gen_abi_cpp import _Generator
+
+
+@py_class("testing.gen_abi_cpp.Dependency")
+class _Dependency(Object):
+    value: int
+
+
+@py_class("testing.gen_abi_cpp.Base")
+class _Base(Object):
+    base_flag: bool
+    base_value: int
+
+
+@py_class("testing.gen_abi_cpp.empty.Empty")
+class _Empty(_Base):
+    pass
+
+
+@py_class("testing.gen_abi_cpp.other.Child")
+class _Child(_Empty):
+    child_flag: bool
+    dependency: _Dependency
+
+
+@py_class("testing.gen_abi_cpp.other.Sibling")
+class _Sibling(_Empty):
+    sibling_value: float
+
+
+@py_class("testing.gen_abi_cpp.tail.Base")
+class _TailBase(Object):
+    parent_flag: bool
+
+
+@py_class("testing.gen_abi_cpp.tail.Empty")
+class _TailEmpty(_TailBase):
+    pass
+
+
+@py_class("testing.gen_abi_cpp.tail.Child")
+class _TailChild(_TailEmpty):
+    child_flag: bool
+
+
+@py_class("testing.gen_abi_cpp.Mixed")
+class _Mixed(Object):
+    ready: bool
+    sequence: int
+    ratio: float
+    pointer: ctypes.c_void_p
+    dtype: DataType
+    device: Device
+    anything: Any
+    title: str
+    payload: bytes
+    callback: Callable[[int], str]
+    dependency: _Dependency
+    array_items: Array[_Dependency]
+    list_items: list[_Dependency]
+    mapping: Map[str, _Dependency]
+    dictionary: dict[str, _Dependency]
+    # py_class resolves annotations at runtime, where PEP 604 unions are 
unavailable on Python 3.9.
+    optional: Optional[int]  # noqa: UP045
+    choice: Union[int, float]  # noqa: UP007
+
+
+@py_class("testing.gen_abi_cpp.NestedStructural")
+class _NestedStructural(Object):
+    optionals: list[Optional[int]]  # noqa: UP045
+    unions: dict[str, Union[int, float]]  # noqa: UP007
+    optional_objects: list[Optional[_Dependency]]  # noqa: UP045
+    union_objects: dict[str, Union[_Dependency, _Sibling]]  # noqa: UP007
+
+
+@py_class("testing.gen_abi_cpp.ExtraObjects")
+class _ExtraObjects(Object):
+    module: tvm_ffi.Module
+    interrupt: tvm_ffi.VisitInterrupt
+
+
+@py_class("testing.gen_abi_cpp.ΔNode")
+class _UnicodeType(Object):
+    value: int
+
+
+@py_class("testing.gen_abi_cpp.Recursive")
+class _Recursive(Object):
+    children: list[_Recursive]
+
+
+@py_class("testing.gen_abi_cpp.MutualLeft")
+class _MutualLeft(Object):
+    rights: list[_MutualRight]
+
+
+@py_class("testing.gen_abi_cpp.MutualRight")
+class _MutualRight(Object):
+    lefts: list[_MutualLeft]
+
+
+@py_class("testing.gen_abi_cpp.Alpha")
+class _SortAlpha(Object):
+    pass
+
+
+@py_class("testing.gen_abi_cpp.beta.Dependency")
+class _SortNested(Object):
+    pass
+
+
+@py_class("testing.gen_abi_cpp.zulu")
+class _SortZulu(Object):
+    pass
+
+
+@py_class("testing.gen_abi_cpp.SortOrder")
+class _SortOrder(Object):
+    alpha: _SortAlpha
+    nested: _SortNested
+    zulu: _SortZulu
+
+
[email protected](scope="module")
+def native_layout_probes(tmp_path_factory: pytest.TempPathFactory) -> 
tvm_ffi.Module:
+    source = r"""
+        #include <tvm/ffi/container/dict.h>
+        #include <tvm/ffi/container/list.h>
+        #include <tvm/ffi/reflection/registry.h>
+
+        namespace tvm {
+        namespace ffi {
+        namespace testing {
+
+        struct ABIRawTensorObj : public Object {
+          static constexpr bool _type_mutable = true;
+          DLTensor* value{nullptr};
+          TVM_FFI_DECLARE_OBJECT_INFO_FINAL(
+              "testing.gen_abi_cpp.native.RawTensor", ABIRawTensorObj, Object);
+        };
+
+        struct ABIObjectContainersObj : public Object {
+          Array<ABIRawTensorObj*> array_items;
+          List<ABIRawTensorObj*> list_items;
+          Map<String, ABIRawTensorObj*> mapping;
+          Dict<String, ABIRawTensorObj*> dictionary;
+          TVM_FFI_DECLARE_OBJECT_INFO_FINAL(
+              "testing.gen_abi_cpp.native.ObjectContainers",
+              ABIObjectContainersObj,
+              Object);
+        };
+
+        struct ABINoMetadataObj : public Object {
+          int64_t hidden[8];
+          TVM_FFI_DECLARE_OBJECT_INFO_FINAL(
+              "testing.gen_abi_cpp.native.NoMetadata", ABINoMetadataObj, 
Object);
+        };
+
+        TVM_FFI_STATIC_INIT_BLOCK() {
+          namespace refl = ::tvm::ffi::reflection;
+          refl::ObjectDef<ABIRawTensorObj>().def_ro("value", 
&ABIRawTensorObj::value);
+          refl::ObjectDef<ABIObjectContainersObj>()
+              .def_ro("array_items", &ABIObjectContainersObj::array_items)
+              .def_ro("list_items", &ABIObjectContainersObj::list_items)
+              .def_ro("mapping", &ABIObjectContainersObj::mapping)
+              .def_ro("dictionary", &ABIObjectContainersObj::dictionary);
+        }
+
+        }  // namespace testing
+        }  // namespace ffi
+        }  // namespace tvm
+
+        int32_t gen_abi_cpp_register_no_metadata() {
+          return ::tvm::ffi::testing::ABINoMetadataObj::RuntimeTypeIndex();
+        }
+    """
+    module = tvm_ffi.cpp.load_inline(
+        name="test_dataclass_gen_abi_cpp_native_probes",
+        cpp_sources=source,
+        functions=["gen_abi_cpp_register_no_metadata"],
+        
build_directory=str(tmp_path_factory.mktemp("gen_abi_cpp_native_probes")),
+    )
+    module.gen_abi_cpp_register_no_metadata()
+    return module
+
+
+_GENERATED_TYPE_KEYS = [
+    "ffi.Function",
+    "testing.gen_abi_cpp.Mixed",
+    "testing.gen_abi_cpp.Mutual*",
+    "testing.gen_abi_cpp.NestedStructural",
+    "testing.gen_abi_cpp.Recursive",
+    "testing.gen_abi_cpp.other.*",
+    "testing.gen_abi_cpp.tail.Child",
+    "testing.gen_abi_cpp.ΔNode",
+]
+
+_EXPECTED_PROGRAM = r"""#pragma once
+
+#include <tvm/ffi/tvm_ffi.h>
+
+namespace testing {
+namespace gen_abi_cpp {
+
+struct BaseObj;
+
+struct DependencyObj;
+
+struct MixedObj;
+
+struct MutualLeftObj;
+
+struct MutualRightObj;
+
+struct NestedStructuralObj;
+
+struct RecursiveObj;
+
+struct __ffi_escape_ce944e6f6465Obj;
+
+}  // namespace gen_abi_cpp
+}  // namespace testing
+
+namespace testing {
+namespace gen_abi_cpp {
+namespace empty {
+
+struct EmptyObj;
+
+}  // namespace empty
+}  // namespace gen_abi_cpp
+}  // namespace testing
+
+namespace testing {
+namespace gen_abi_cpp {
+namespace other {
+
+struct ChildObj;
+
+struct SiblingObj;
+
+}  // namespace other
+}  // namespace gen_abi_cpp
+}  // namespace testing
+
+namespace testing {
+namespace gen_abi_cpp {
+namespace tail {
+
+struct BaseObj;
+
+struct ChildObj;
+
+struct EmptyObj;
+
+}  // namespace tail
+}  // namespace gen_abi_cpp
+}  // namespace testing
+
+template <>
+inline constexpr bool 
tvm::ffi::is_object_subclass_v<::testing::gen_abi_cpp::BaseObj> = true;
+template <>
+inline constexpr bool 
tvm::ffi::is_object_subclass_v<::testing::gen_abi_cpp::DependencyObj> = true;
+template <>
+inline constexpr bool 
tvm::ffi::is_object_subclass_v<::testing::gen_abi_cpp::MixedObj> = true;
+template <>
+inline constexpr bool 
tvm::ffi::is_object_subclass_v<::testing::gen_abi_cpp::MutualLeftObj> = true;
+template <>
+inline constexpr bool 
tvm::ffi::is_object_subclass_v<::testing::gen_abi_cpp::MutualRightObj> = true;
+template <>
+inline constexpr bool 
tvm::ffi::is_object_subclass_v<::testing::gen_abi_cpp::NestedStructuralObj> = 
true;
+template <>
+inline constexpr bool 
tvm::ffi::is_object_subclass_v<::testing::gen_abi_cpp::RecursiveObj> = true;
+template <>
+inline constexpr bool 
tvm::ffi::is_object_subclass_v<::testing::gen_abi_cpp::__ffi_escape_ce944e6f6465Obj>
 = true;
+template <>
+inline constexpr bool 
tvm::ffi::is_object_subclass_v<::testing::gen_abi_cpp::empty::EmptyObj> = true;
+template <>
+inline constexpr bool 
tvm::ffi::is_object_subclass_v<::testing::gen_abi_cpp::other::ChildObj> = true;
+template <>
+inline constexpr bool 
tvm::ffi::is_object_subclass_v<::testing::gen_abi_cpp::other::SiblingObj> = 
true;
+template <>
+inline constexpr bool 
tvm::ffi::is_object_subclass_v<::testing::gen_abi_cpp::tail::BaseObj> = true;
+template <>
+inline constexpr bool 
tvm::ffi::is_object_subclass_v<::testing::gen_abi_cpp::tail::ChildObj> = true;
+template <>
+inline constexpr bool 
tvm::ffi::is_object_subclass_v<::testing::gen_abi_cpp::tail::EmptyObj> = true;
+
+namespace testing {
+namespace gen_abi_cpp {
+
+struct DependencyObj : public ::tvm::ffi::Object {
+  TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.gen_abi_cpp.Dependency", 1);
+};
+
+}  // namespace gen_abi_cpp
+}  // namespace testing
+
+static_assert(sizeof(::tvm::ffi::Object) == 24);
+static_assert(alignof(::tvm::ffi::Object) == 8);
+
+#if defined(__clang__) || defined(__GNUC__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Winvalid-offsetof"
+#elif defined(_MSC_VER)
+#pragma warning(push)
+#pragma warning(disable : 4749)
+#endif
+
+namespace testing {
+namespace gen_abi_cpp {
+
+struct alignas(8) BaseObj : public ::tvm::ffi::Object {
+  TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.gen_abi_cpp.Base", 1);
+
+  bool base_flag;  // offset=24, size=1, align=1
+  int64_t base_value;  // offset=32, size=8, align=8
+};
+
+static_assert(sizeof(BaseObj) == 40);
+static_assert(alignof(BaseObj) == 8);
+static_assert(sizeof(decltype(BaseObj::base_flag)) == 1);
+static_assert(alignof(decltype(BaseObj::base_flag)) == 1);
+static_assert(offsetof(BaseObj, base_flag) == 24);
+static_assert(sizeof(decltype(BaseObj::base_value)) == 8);
+static_assert(alignof(decltype(BaseObj::base_value)) == 8);
+static_assert(offsetof(BaseObj, base_value) == 32);
+
+struct alignas(8) MixedObj : public ::tvm::ffi::Object {
+  TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.gen_abi_cpp.Mixed", 1);
+
+  bool ready;  // offset=24, size=1, align=1
+  int64_t sequence;  // offset=32, size=8, align=8
+  double ratio;  // offset=40, size=8, align=8
+  void* pointer;  // offset=48, size=8, align=8
+  DLDataType dtype;  // offset=56, size=4, align=2
+  DLDevice device;  // offset=60, size=8, align=4
+  ::tvm::ffi::Any anything;  // offset=72, size=16, align=8
+  ::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::Any optional;  // offset=168, size=16, align=8
+  ::tvm::ffi::Any choice;  // offset=184, size=16, align=8
+};
+
+static_assert(sizeof(MixedObj) == 200);
+static_assert(alignof(MixedObj) == 8);
+static_assert(sizeof(decltype(MixedObj::ready)) == 1);
+static_assert(alignof(decltype(MixedObj::ready)) == 1);
+static_assert(offsetof(MixedObj, ready) == 24);
+static_assert(sizeof(decltype(MixedObj::sequence)) == 8);
+static_assert(alignof(decltype(MixedObj::sequence)) == 8);
+static_assert(offsetof(MixedObj, sequence) == 32);
+static_assert(sizeof(decltype(MixedObj::ratio)) == 8);
+static_assert(alignof(decltype(MixedObj::ratio)) == 8);
+static_assert(offsetof(MixedObj, ratio) == 40);
+static_assert(sizeof(decltype(MixedObj::pointer)) == 8);
+static_assert(alignof(decltype(MixedObj::pointer)) == 8);
+static_assert(offsetof(MixedObj, pointer) == 48);
+static_assert(sizeof(decltype(MixedObj::dtype)) == 4);
+static_assert(alignof(decltype(MixedObj::dtype)) == 2);
+static_assert(offsetof(MixedObj, dtype) == 56);
+static_assert(sizeof(decltype(MixedObj::device)) == 8);
+static_assert(alignof(decltype(MixedObj::device)) == 4);
+static_assert(offsetof(MixedObj, device) == 60);
+static_assert(sizeof(decltype(MixedObj::anything)) == 16);
+static_assert(alignof(decltype(MixedObj::anything)) == 8);
+static_assert(offsetof(MixedObj, anything) == 72);
+static_assert(sizeof(decltype(MixedObj::title)) == 16);
+static_assert(alignof(decltype(MixedObj::title)) == 8);
+static_assert(offsetof(MixedObj, title) == 88);
+static_assert(sizeof(decltype(MixedObj::payload)) == 16);
+static_assert(alignof(decltype(MixedObj::payload)) == 8);
+static_assert(offsetof(MixedObj, payload) == 104);
+static_assert(sizeof(decltype(MixedObj::callback)) == 8);
+static_assert(alignof(decltype(MixedObj::callback)) == 8);
+static_assert(offsetof(MixedObj, callback) == 120);
+static_assert(sizeof(decltype(MixedObj::dependency)) == 8);
+static_assert(alignof(decltype(MixedObj::dependency)) == 8);
+static_assert(offsetof(MixedObj, dependency) == 128);
+static_assert(sizeof(decltype(MixedObj::array_items)) == 8);
+static_assert(alignof(decltype(MixedObj::array_items)) == 8);
+static_assert(offsetof(MixedObj, array_items) == 136);
+static_assert(sizeof(decltype(MixedObj::list_items)) == 8);
+static_assert(alignof(decltype(MixedObj::list_items)) == 8);
+static_assert(offsetof(MixedObj, list_items) == 144);
+static_assert(sizeof(decltype(MixedObj::mapping)) == 8);
+static_assert(alignof(decltype(MixedObj::mapping)) == 8);
+static_assert(offsetof(MixedObj, mapping) == 152);
+static_assert(sizeof(decltype(MixedObj::dictionary)) == 8);
+static_assert(alignof(decltype(MixedObj::dictionary)) == 8);
+static_assert(offsetof(MixedObj, dictionary) == 160);
+static_assert(sizeof(decltype(MixedObj::optional)) == 16);
+static_assert(alignof(decltype(MixedObj::optional)) == 8);
+static_assert(offsetof(MixedObj, optional) == 168);
+static_assert(sizeof(decltype(MixedObj::choice)) == 16);
+static_assert(alignof(decltype(MixedObj::choice)) == 8);
+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
+};
+
+static_assert(sizeof(MutualLeftObj) == 32);
+static_assert(alignof(MutualLeftObj) == 8);
+static_assert(sizeof(decltype(MutualLeftObj::rights)) == 8);
+static_assert(alignof(decltype(MutualLeftObj::rights)) == 8);
+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
+};
+
+static_assert(sizeof(MutualRightObj) == 32);
+static_assert(alignof(MutualRightObj) == 8);
+static_assert(sizeof(decltype(MutualRightObj::lefts)) == 8);
+static_assert(alignof(decltype(MutualRightObj::lefts)) == 8);
+static_assert(offsetof(MutualRightObj, lefts) == 24);
+
+struct alignas(8) NestedStructuralObj : public ::tvm::ffi::Object {
+  TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.gen_abi_cpp.NestedStructural", 
1);
+
+  ::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
+};
+
+static_assert(sizeof(NestedStructuralObj) == 56);
+static_assert(alignof(NestedStructuralObj) == 8);
+static_assert(sizeof(decltype(NestedStructuralObj::optionals)) == 8);
+static_assert(alignof(decltype(NestedStructuralObj::optionals)) == 8);
+static_assert(offsetof(NestedStructuralObj, optionals) == 24);
+static_assert(sizeof(decltype(NestedStructuralObj::unions)) == 8);
+static_assert(alignof(decltype(NestedStructuralObj::unions)) == 8);
+static_assert(offsetof(NestedStructuralObj, unions) == 32);
+static_assert(sizeof(decltype(NestedStructuralObj::optional_objects)) == 8);
+static_assert(alignof(decltype(NestedStructuralObj::optional_objects)) == 8);
+static_assert(offsetof(NestedStructuralObj, optional_objects) == 40);
+static_assert(sizeof(decltype(NestedStructuralObj::union_objects)) == 8);
+static_assert(alignof(decltype(NestedStructuralObj::union_objects)) == 8);
+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
+};
+
+static_assert(sizeof(RecursiveObj) == 32);
+static_assert(alignof(RecursiveObj) == 8);
+static_assert(sizeof(decltype(RecursiveObj::children)) == 8);
+static_assert(alignof(decltype(RecursiveObj::children)) == 8);
+static_assert(offsetof(RecursiveObj, children) == 24);
+
+}  // namespace gen_abi_cpp
+}  // namespace testing
+
+namespace testing {
+namespace gen_abi_cpp {
+namespace tail {
+
+struct alignas(8) BaseObj : public ::tvm::ffi::Object {
+  TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.gen_abi_cpp.tail.Base", 1);
+
+  bool parent_flag;  // offset=24, size=1, align=1
+};
+
+static_assert(sizeof(BaseObj) == 32);
+static_assert(alignof(BaseObj) == 8);
+static_assert(sizeof(decltype(BaseObj::parent_flag)) == 1);
+static_assert(alignof(decltype(BaseObj::parent_flag)) == 1);
+static_assert(offsetof(BaseObj, parent_flag) == 24);
+
+}  // namespace tail
+}  // namespace gen_abi_cpp
+}  // namespace testing
+
+namespace testing {
+namespace gen_abi_cpp {
+
+struct alignas(8) __ffi_escape_ce944e6f6465Obj : public ::tvm::ffi::Object {
+  TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.gen_abi_cpp.\316\224Node", 1);
+
+  int64_t value;  // offset=24, size=8, align=8
+};
+
+static_assert(sizeof(__ffi_escape_ce944e6f6465Obj) == 32);
+static_assert(alignof(__ffi_escape_ce944e6f6465Obj) == 8);
+static_assert(sizeof(decltype(__ffi_escape_ce944e6f6465Obj::value)) == 8);
+static_assert(alignof(decltype(__ffi_escape_ce944e6f6465Obj::value)) == 8);
+static_assert(offsetof(__ffi_escape_ce944e6f6465Obj, value) == 24);
+
+}  // namespace gen_abi_cpp
+}  // namespace testing
+
+namespace testing {
+namespace gen_abi_cpp {
+namespace empty {
+
+struct alignas(8) EmptyObj : public ::testing::gen_abi_cpp::BaseObj {
+  TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.gen_abi_cpp.empty.Empty", 2);
+};
+
+static_assert(sizeof(EmptyObj) == 40);
+static_assert(alignof(EmptyObj) == 8);
+
+}  // namespace empty
+}  // namespace gen_abi_cpp
+}  // namespace testing
+
+namespace testing {
+namespace gen_abi_cpp {
+namespace tail {
+
+struct alignas(8) EmptyObj : public ::testing::gen_abi_cpp::tail::BaseObj {
+  TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.gen_abi_cpp.tail.Empty", 2);
+};
+
+static_assert(sizeof(EmptyObj) == 32);
+static_assert(alignof(EmptyObj) == 8);
+
+}  // namespace tail
+}  // namespace gen_abi_cpp
+}  // namespace testing
+
+namespace testing {
+namespace gen_abi_cpp {
+namespace other {
+
+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
+};
+
+static_assert(sizeof(ChildObj) == 56);
+static_assert(alignof(ChildObj) == 8);
+static_assert(sizeof(decltype(ChildObj::child_flag)) == 1);
+static_assert(alignof(decltype(ChildObj::child_flag)) == 1);
+static_assert(offsetof(ChildObj, child_flag) == 40);
+static_assert(sizeof(decltype(ChildObj::dependency)) == 8);
+static_assert(alignof(decltype(ChildObj::dependency)) == 8);
+static_assert(offsetof(ChildObj, dependency) == 48);
+
+struct alignas(8) SiblingObj : public ::testing::gen_abi_cpp::empty::EmptyObj {
+  TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.gen_abi_cpp.other.Sibling", 3);
+
+  double sibling_value;  // offset=40, size=8, align=8
+};
+
+static_assert(sizeof(SiblingObj) == 48);
+static_assert(alignof(SiblingObj) == 8);
+static_assert(sizeof(decltype(SiblingObj::sibling_value)) == 8);
+static_assert(alignof(decltype(SiblingObj::sibling_value)) == 8);
+static_assert(offsetof(SiblingObj, sibling_value) == 40);
+
+}  // namespace other
+}  // namespace gen_abi_cpp
+}  // namespace testing
+
+namespace testing {
+namespace gen_abi_cpp {
+namespace tail {
+
+struct alignas(8) ChildObj : public ::testing::gen_abi_cpp::tail::EmptyObj {
+  TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.gen_abi_cpp.tail.Child", 3);
+
+  bool child_flag;  // offset=25, size=1, align=1
+};
+
+static_assert(sizeof(ChildObj) == 32);
+static_assert(alignof(ChildObj) == 8);
+static_assert(sizeof(decltype(ChildObj::child_flag)) == 1);
+static_assert(alignof(decltype(ChildObj::child_flag)) == 1);
+static_assert(offsetof(ChildObj, child_flag) == 25);
+
+}  // namespace tail
+}  // namespace gen_abi_cpp
+}  // namespace testing
+
+#if defined(__clang__) || defined(__GNUC__)
+#pragma GCC diagnostic pop
+#elif defined(_MSC_VER)
+#pragma warning(pop)
+#endif
+"""
+
+if sys.platform == "win32":
+    # The Microsoft C++ ABI starts direct-subclass fields after the complete
+    # base object instead of reusing its tail padding.
+    _EXPECTED_PROGRAM = _EXPECTED_PROGRAM.replace(
+        r"""struct alignas(8) ChildObj : public 
::testing::gen_abi_cpp::tail::EmptyObj {
+  TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.gen_abi_cpp.tail.Child", 3);
+
+  bool child_flag;  // offset=25, size=1, align=1
+};
+
+static_assert(sizeof(ChildObj) == 32);
+static_assert(alignof(ChildObj) == 8);
+static_assert(sizeof(decltype(ChildObj::child_flag)) == 1);
+static_assert(alignof(decltype(ChildObj::child_flag)) == 1);
+static_assert(offsetof(ChildObj, child_flag) == 25);""",
+        r"""struct alignas(8) ChildObj : public 
::testing::gen_abi_cpp::tail::EmptyObj {
+  TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.gen_abi_cpp.tail.Child", 3);
+
+  bool child_flag;  // offset=32, size=1, align=1
+};
+
+static_assert(sizeof(ChildObj) == 40);
+static_assert(alignof(ChildObj) == 8);
+static_assert(sizeof(decltype(ChildObj::child_flag)) == 1);
+static_assert(alignof(decltype(ChildObj::child_flag)) == 1);
+static_assert(offsetof(ChildObj, child_flag) == 32);""",
+    )
+
+_EXPECTED_NATIVE_PROGRAM = r"""#pragma once
+
+#include <tvm/ffi/tvm_ffi.h>
+
+namespace testing {
+namespace gen_abi_cpp {
+namespace native {
+
+struct ObjectContainersObj;
+
+struct RawTensorObj;
+
+}  // namespace native
+}  // namespace gen_abi_cpp
+}  // namespace testing
+
+template <>
+inline constexpr bool 
tvm::ffi::is_object_subclass_v<::testing::gen_abi_cpp::native::ObjectContainersObj>
 = true;
+template <>
+inline constexpr bool 
tvm::ffi::is_object_subclass_v<::testing::gen_abi_cpp::native::RawTensorObj> = 
true;
+
+namespace testing {
+namespace gen_abi_cpp {
+namespace native {
+
+struct RawTensorObj : public ::tvm::ffi::Object {
+  TVM_FFI_DECLARE_OBJECT_INFO_LOOKUP("testing.gen_abi_cpp.native.RawTensor", 
1);
+};
+
+}  // namespace native
+}  // namespace gen_abi_cpp
+}  // namespace testing
+
+static_assert(sizeof(::tvm::ffi::Object) == 24);
+static_assert(alignof(::tvm::ffi::Object) == 8);
+
+#if defined(__clang__) || defined(__GNUC__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Winvalid-offsetof"
+#elif defined(_MSC_VER)
+#pragma warning(push)
+#pragma warning(disable : 4749)
+#endif
+
+namespace testing {
+namespace gen_abi_cpp {
+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
+};
+
+static_assert(sizeof(ObjectContainersObj) == 56);
+static_assert(alignof(ObjectContainersObj) == 8);
+static_assert(sizeof(decltype(ObjectContainersObj::array_items)) == 8);
+static_assert(alignof(decltype(ObjectContainersObj::array_items)) == 8);
+static_assert(offsetof(ObjectContainersObj, array_items) == 24);
+static_assert(sizeof(decltype(ObjectContainersObj::list_items)) == 8);
+static_assert(alignof(decltype(ObjectContainersObj::list_items)) == 8);
+static_assert(offsetof(ObjectContainersObj, list_items) == 32);
+static_assert(sizeof(decltype(ObjectContainersObj::mapping)) == 8);
+static_assert(alignof(decltype(ObjectContainersObj::mapping)) == 8);
+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);
+
+}  // namespace native
+}  // namespace gen_abi_cpp
+}  // namespace testing
+
+#if defined(__clang__) || defined(__GNUC__)
+#pragma GCC diagnostic pop
+#elif defined(_MSC_VER)
+#pragma warning(pop)
+#endif
+"""
+
+_EXPECTED_EXTRA_PROGRAM = r"""#pragma once
+
+#include <tvm/ffi/tvm_ffi.h>
+
+namespace testing {
+namespace gen_abi_cpp {
+
+struct ExtraObjectsObj;
+
+}  // namespace gen_abi_cpp
+}  // namespace testing
+
+template <>
+inline constexpr bool 
tvm::ffi::is_object_subclass_v<::testing::gen_abi_cpp::ExtraObjectsObj> = true;
+
+static_assert(sizeof(::tvm::ffi::Object) == 24);
+static_assert(alignof(::tvm::ffi::Object) == 8);
+
+#if defined(__clang__) || defined(__GNUC__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Winvalid-offsetof"
+#elif defined(_MSC_VER)
+#pragma warning(push)
+#pragma warning(disable : 4749)
+#endif
+
+namespace testing {
+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
+};
+
+static_assert(sizeof(ExtraObjectsObj) == 40);
+static_assert(alignof(ExtraObjectsObj) == 8);
+static_assert(sizeof(decltype(ExtraObjectsObj::module)) == 8);
+static_assert(alignof(decltype(ExtraObjectsObj::module)) == 8);
+static_assert(offsetof(ExtraObjectsObj, module) == 24);
+static_assert(sizeof(decltype(ExtraObjectsObj::interrupt)) == 8);
+static_assert(alignof(decltype(ExtraObjectsObj::interrupt)) == 8);
+static_assert(offsetof(ExtraObjectsObj, interrupt) == 32);
+
+}  // namespace gen_abi_cpp
+}  // namespace testing
+
+#if defined(__clang__) || defined(__GNUC__)
+#pragma GCC diagnostic pop
+#elif defined(_MSC_VER)
+#pragma warning(pop)
+#endif
+"""
+
+_EXPECTED_EMPTY_PROGRAM = r"""#pragma once
+
+#include <tvm/ffi/tvm_ffi.h>
+
+
+static_assert(sizeof(::tvm::ffi::Object) == 24);
+static_assert(alignof(::tvm::ffi::Object) == 8);
+
+#if defined(__clang__) || defined(__GNUC__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Winvalid-offsetof"
+#elif defined(_MSC_VER)
+#pragma warning(push)
+#pragma warning(disable : 4749)
+#endif
+
+#if defined(__clang__) || defined(__GNUC__)
+#pragma GCC diagnostic pop
+#elif defined(_MSC_VER)
+#pragma warning(pop)
+#endif
+"""
+
+
+def test_generated_program() -> None:
+    assert gen_abi_cpp(_GENERATED_TYPE_KEYS) == _EXPECTED_PROGRAM
+    assert gen_abi_cpp([*_GENERATED_TYPE_KEYS, "testing.gen_abi_cpp.Mix*"]) == 
_EXPECTED_PROGRAM
+
+
+def test_native_generated_program(native_layout_probes: tvm_ffi.Module) -> 
None:
+    del native_layout_probes
+    assert gen_abi_cpp("testing.gen_abi_cpp.native.ObjectContainers") == 
_EXPECTED_NATIVE_PROGRAM
+
+
+def test_builtin_generated_program() -> None:
+    assert gen_abi_cpp("ffi.Function") == _EXPECTED_EMPTY_PROGRAM
+    with pytest.raises(ValueError):
+        gen_abi_cpp("ffi.OpaquePyObject")
+
+
[email protected]("selector", ["testing.gen_abi_cpp.DoesNotExist", 
"no.match.*"])
+def test_unmatched_selector_rejected(selector: str) -> None:
+    with pytest.raises(ValueError, match="did not match"):
+        gen_abi_cpp(selector)
+
+
+def test_dependencies_are_grouped_by_namespace() -> None:
+    header = gen_abi_cpp("testing.gen_abi_cpp.SortOrder")
+    forward_declarations = header.split("template <>\ninline constexpr bool", 
maxsplit=1)[0]
+    assert forward_declarations.count("namespace gen_abi_cpp {") == 2
+    assert forward_declarations.index("struct zuluObj;") < 
forward_declarations.index(
+        "namespace beta {"
+    )
+
+
+def test_schema_less_custom_object_uses_object_ptr_carrier() -> None:
+    field = cast(
+        TypeField,
+        SimpleNamespace(
+            name="value",
+            size=8,
+            alignment=8,
+            offset=24,
+            field_static_type_index=128,
+            ty=None,
+        ),
+    )
+    owner = cast(
+        TypeInfo,
+        SimpleNamespace(type_key="testing.gen_abi_cpp.SchemaLess"),
+    )
+
+    carrier = _Generator([])._lower_field(field, owner)
+
+    assert carrier.cpp_type == "::tvm::ffi::ObjectPtr<::tvm::ffi::Object>"
+    assert (carrier.size, carrier.alignment) == (8, 8)
+
+
+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
+
+    header_path = tmp_path / "extra_objects.h"
+    source_path = tmp_path / "extra_objects_test.cc"
+    header_path.write_text(header)
+    source_path.write_text(
+        "#include <tvm/ffi/extra/module.h>\n"
+        "#include <tvm/ffi/extra/structural_visit.h>\n"
+        '#include "extra_objects.h"\n'
+    )
+    object_path = tvm_ffi.cpp.build(
+        name="test_dataclass_gen_abi_cpp_extra_objects",
+        sources=str(source_path),
+        extra_include_paths=[str(tmp_path)],
+        build_directory=str(tmp_path / "build"),
+        output="extra_objects_test.o",
+    )
+    assert Path(object_path).is_file()
+
+
+def test_non_object_registered_key_is_rejected() -> None:
+    with pytest.raises(ValueError, match="did not match any registered object 
type"):
+        gen_abi_cpp("int")
+
+
+def test_unsafe_native_schema_alias_is_rejected(
+    native_layout_probes: tvm_ffi.Module,
+) -> None:
+    del native_layout_probes
+    with pytest.raises(ValueError, match=r"DLTensor\*"):
+        gen_abi_cpp("testing.gen_abi_cpp.native.RawTensor")
+
+
+def test_native_type_without_size_metadata_is_rejected(
+    native_layout_probes: tvm_ffi.Module,
+) -> None:
+    del native_layout_probes
+    with pytest.raises(ValueError, match="does not expose fixed total-size 
metadata"):
+        gen_abi_cpp("testing.gen_abi_cpp.native.NoMetadata")
+
+
+def test_ambiguous_native_optional_is_rejected() -> None:
+    with pytest.raises(ValueError, match="Ambiguous native Optional carrier"):
+        gen_abi_cpp("testing.SchemaAllTypes")
+
+
+def test_generated_header_is_self_contained(tmp_path: Path) -> None:
+    header_path = tmp_path / "abi.h"
+    source_path = tmp_path / "abi_header_test.cc"
+    header_path.write_text(gen_abi_cpp(_GENERATED_TYPE_KEYS))
+    source_path.write_text('#include "abi.h"\n')
+
+    object_path = tvm_ffi.cpp.build(
+        name="test_dataclass_gen_abi_cpp_header",
+        sources=str(source_path),
+        extra_include_paths=[str(tmp_path)],
+        build_directory=str(tmp_path / "build"),
+        output="abi_header_test.o",
+    )
+    assert Path(object_path).is_file()
+
+
+def test_generated_header_compiles_and_reads_live_objects(tmp_path: Path) -> 
None:
+    header = gen_abi_cpp(_GENERATED_TYPE_KEYS)
+    source = (
+        header
+        + r"""
+        static_assert(std::is_base_of_v<
+                      ::tvm::ffi::Object,
+                      ::testing::gen_abi_cpp::DependencyObj>);
+        static_assert(std::is_base_of_v<
+                      ::tvm::ffi::Object,
+                      ::testing::gen_abi_cpp::MixedObj>);
+        static_assert(std::is_base_of_v<
+                      ::testing::gen_abi_cpp::BaseObj,
+                      ::testing::gen_abi_cpp::other::ChildObj>);
+        static_assert(std::is_base_of_v<
+                      ::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<
+                          ::testing::gen_abi_cpp::other::ChildObj>>);
+
+        int64_t gen_abi_cpp_read_sequence(
+            const ::testing::gen_abi_cpp::MixedObj* value) {
+          return value->sequence;
+        }
+
+        int64_t gen_abi_cpp_read_inherited_value(
+            const ::testing::gen_abi_cpp::other::ChildObj* value) {
+          const ::testing::gen_abi_cpp::BaseObj* base = value;
+          return base->base_value;
+        }
+
+        bool gen_abi_cpp_upcast_identity(
+            ::tvm::ffi::ObjectPtr<
+                ::testing::gen_abi_cpp::other::ChildObj> child) {
+          ::tvm::ffi::ObjectPtr<::testing::gen_abi_cpp::BaseObj> base = child;
+          return reinterpret_cast<const void*>(child.get()) ==
+                 reinterpret_cast<const void*>(base.get());
+        }
+
+        int32_t gen_abi_cpp_unicode_type_index() {
+          return 
::testing::gen_abi_cpp::__ffi_escape_ce944e6f6465Obj::RuntimeTypeIndex();
+        }
+        """
+    )
+    module = tvm_ffi.cpp.load_inline(
+        name="test_dataclass_gen_abi_cpp",
+        cpp_sources=source,
+        functions=[
+            "gen_abi_cpp_read_sequence",
+            "gen_abi_cpp_read_inherited_value",
+            "gen_abi_cpp_upcast_identity",
+            "gen_abi_cpp_unicode_type_index",
+        ],
+        build_directory=str(tmp_path / "build"),
+    )
+
+    mixed = _Mixed(
+        ready=True,
+        sequence=123,
+        ratio=1.5,
+        pointer=ctypes.c_void_p(),
+        dtype=DataType("float32"),
+        device=Device("cpu", 0),
+        anything=None,
+        title="title",
+        payload=b"payload",
+        callback=lambda value: str(value),
+        dependency=_Dependency(value=1),
+        array_items=Array[_Dependency]([]),
+        list_items=[],
+        mapping=Map[str, _Dependency]({}),
+        dictionary={},
+        optional=None,
+        choice=2.0,
+    )
+    child = _Child(base_flag=True, base_value=4, child_flag=False, 
dependency=_Dependency(value=2))
+    assert module.gen_abi_cpp_read_sequence(mixed) == 123
+    assert module.gen_abi_cpp_read_inherited_value(child) == 4
+    assert module.gen_abi_cpp_upcast_identity(child) is True
+    unicode_info = 
_lookup_or_register_type_info_from_type_key("testing.gen_abi_cpp.ΔNode")
+    assert module.gen_abi_cpp_unicode_type_index() == unicode_info.type_index
diff --git a/tests/python/test_dataclass_py_class.py 
b/tests/python/test_dataclass_py_class.py
index 99bfcbb2..26bcc487 100644
--- a/tests/python/test_dataclass_py_class.py
+++ b/tests/python/test_dataclass_py_class.py
@@ -55,6 +55,7 @@ from tvm_ffi.testing import (
 from tvm_ffi.testing import (
     TestObjectPtrHolder as _TestObjectPtrHolder,
 )
+from tvm_ffi.testing import _TestCxxClassBase
 from tvm_ffi.testing.testing import requires_py310
 
 # ---------------------------------------------------------------------------
@@ -3597,6 +3598,25 @@ class TestNativeParentInheritance:
         parent_end = max(f.offset + f.size for f in parent_info.fields)
         assert child_info.fields[0].offset >= parent_end
 
+    def test_native_parent_tail_padding_matches_cxx(self) -> None:
+        parent_info = core._type_cls_to_type_info(_TestCxxClassBase)
+        assert parent_info is not None
+        assert max(f.offset + f.size for f in parent_info.fields) == 36
+        expected_offset = 40 if sys.platform == "win32" else 36
+
+        Child = _make_type(
+            "InhNativeTailPadding",
+            [Field(name="extra", _ty_schema=TypeSchema("bool"), 
default=MISSING)],
+            parent=_TestCxxClassBase,
+        )
+        child_info = getattr(Child, "__tvm_ffi_type_info__")
+        assert child_info.fields[0].offset == expected_offset
+
+        obj = Child(v_i64=1, v_i32=2, extra=True)
+        assert obj.v_i64 == 1
+        assert obj.v_i32 == 2
+        assert obj.extra is True
+
     def test_preserves_parent_fields(self) -> None:
         Child = _make_type(
             "InhNativePreserve",
@@ -3910,6 +3930,33 @@ class TestBoolAlignment:
         assert obj.b is False
         assert obj.c is True
 
+    def test_inherited_tail_padding_matches_cxx(self) -> None:
+        @py_class(_unique_key("BoolTailParent"))
+        class Parent(Object):
+            parent_flag: bool = False
+
+        @py_class(_unique_key("BoolTailEmpty"))
+        class Empty(Parent):
+            pass
+
+        @py_class(_unique_key("BoolTailChild"))
+        class Child(Empty):
+            child_flag: bool = False
+
+        parent_info = _get_type_info(Parent)
+        empty_info = _get_type_info(Empty)
+        child_info = _get_type_info(Child)
+        expected_offset = 32 if sys.platform == "win32" else 25
+        assert getattr(parent_info, "total_size") == 32
+        assert getattr(empty_info, "total_size") == 32
+        assert child_info.fields[0].offset == expected_offset
+        assert getattr(child_info, "total_size") == (40 if sys.platform == 
"win32" else 32)
+
+        obj = Child(parent_flag=True, child_flag=False)
+        obj.child_flag = True
+        assert obj.parent_flag is True
+        assert obj.child_flag is True
+
     def test_bool_int_bool_int_alternating(self) -> None:
         Cls = _make_type(
             "BoolIntBoolInt",
diff --git a/tests/python/test_type_converter.py 
b/tests/python/test_type_converter.py
index 64a9bb19..0290a07b 100644
--- a/tests/python/test_type_converter.py
+++ b/tests/python/test_type_converter.py
@@ -1631,6 +1631,20 @@ class TestCustomObjectExactMatch:
 # Category 31: Custom object type hierarchy (subclass passes parent schema)
 # ---------------------------------------------------------------------------
 class TestCustomObjectHierarchy:
+    def test_type_schema_is_subtype_of_uses_type_info_hierarchy(self) -> None:
+        """TypeSchema subtype checks use registered FFI type metadata."""
+        base_schema = A(TestObjectBase)
+        derived_schema = A(TestObjectDerived)
+        int_schema = A(int)
+
+        assert base_schema.is_subtype_of(TestObjectBase)
+        assert derived_schema.is_subtype_of(TestObjectDerived)
+        assert derived_schema.is_subtype_of(TestObjectBase)
+        assert derived_schema.is_subtype_of(tvm_ffi.core.Object)
+        assert not base_schema.is_subtype_of(TestObjectDerived)
+        assert not int_schema.is_subtype_of(TestObjectBase)
+        assert not derived_schema.is_subtype_of(int)
+
     def test_derived_passes_base_schema(self) -> None:
         """TestObjectDerived passes TypeSchema('testing.TestObjectBase')."""
         obj = TestObjectDerived(v_map={"a": 1}, v_array=[1], v_i64=0, 
v_f64=0.0, v_str="")

Reply via email to