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="")