Dataclass-Style Reflection#
TVM-FFI’s reflection system provides Python-dataclass-style features for C++
classes: auto-generated constructors, default values, keyword-only parameters,
repr, hashing, comparison, and deep copy. These features are enabled by
per-field and per-class traits registered via
ObjectDef.
This guide assumes familiarity with Export Functions and Classes and Object and Class.
Quick Start#
Define a C++ object with fields, register it with traits, and use it from Python with full dataclass semantics:
#include <tvm/ffi/tvm_ffi.h>
namespace ffi = tvm::ffi;
class PointObj : public ffi::Object {
public:
int64_t x;
int64_t y;
ffi::String label;
static constexpr bool _type_mutable = true;
TVM_FFI_DECLARE_OBJECT_INFO_FINAL("my_ext.Point", PointObj, ffi::Object);
};
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = ffi::reflection;
refl::ObjectDef<PointObj>()
.def_rw("x", &PointObj::x)
.def_rw("y", &PointObj::y)
.def_rw("label", &PointObj::label, refl::default_(""));
}
No refl::init<>() call is needed — the reflection system auto-generates a
packed __ffi_init__ from the reflected fields:
import tvm_ffi
@tvm_ffi.register_object("my_ext.Point")
class Point(tvm_ffi.Object):
x: int
y: int
label: str
p1 = Point(1, 2) # positional args
p2 = Point(1, 2, label="origin") # keyword arg with default
Auto-Generated Constructors#
When no explicit refl::init<Args...>() is registered, ObjectDef
auto-generates a packed constructor (__ffi_init__) from the reflected
fields. The generated signature follows Python conventions:
Required positional parameters come first.
Optional positional parameters (those with defaults) come next.
Keyword-only parameters follow after a
*separator.
Field Traits for Init#
Trait |
Effect on auto-init |
|---|---|
|
Makes the parameter optional with a literal default value. |
|
Makes the parameter optional; calls |
|
Moves the parameter after the |
|
Excludes the field from the constructor entirely. The field must have a default value or be initialized by a base-class constructor. |
refl::ObjectDef<ConfigObj>()
.def_rw("batch_size", &ConfigObj::batch_size)
.def_rw("lr", &ConfigObj::lr, refl::default_(0.001))
.def_rw("device", &ConfigObj::device, refl::kw_only(true),
refl::default_("cpu"))
.def_rw("_cache", &ConfigObj::_cache, refl::init(false),
refl::default_factory([] { return ffi::Dict(); }));
The generated Python signature is:
def __init__(self, batch_size, lr=0.001, *, device="cpu"):
...
# _cache is excluded from __init__ but initialized to Dict()
Suppressing Auto-Init#
Pass refl::init(false) at the class level to suppress auto-init entirely:
refl::ObjectDef<InternalObj>(refl::init(false))
.def_rw("x", &InternalObj::x)
.def_rw("y", &InternalObj::y);
The object will have no __ffi_init__ method. Construction must happen
through a custom factory or from C++.
Explicit Constructors#
Use refl::init<Args...>() to register an explicit typed constructor
instead of auto-init:
refl::ObjectDef<IntPairObj>()
.def(refl::init<int64_t, int64_t>())
.def_ro("a", &IntPairObj::a)
.def_ro("b", &IntPairObj::b);
This calls IntPairObj(int64_t, int64_t) directly. Auto-init is
automatically suppressed when an explicit constructor is registered.
Default Values#
Literal Defaults#
refl::default_(value) stores a literal default. The value is captured once
at registration time:
.def_rw("threshold", &Obj::threshold, refl::default_(0.5))
Factory Defaults#
refl::default_factory(fn) calls fn() each time a default is needed.
Use this for mutable containers to avoid aliasing:
.def_rw("items", &Obj::items,
refl::default_factory([] { return ffi::List<ffi::String>(); }))
Note
refl::default_ and refl::default_factory are the preferred names for
new code. The original names refl::DefaultValue and
refl::DefaultFactory are retained for backward compatibility.
Field Traits Reference#
All traits are passed as extra arguments to def_ro or def_rw.
Multiple traits can be combined on a single field:
.def_rw("name", &Obj::name,
refl::default_("unnamed"),
refl::kw_only(true),
refl::repr(false))
Trait |
Description |
|---|---|
|
Include/exclude field from auto-generated |
|
Mark field as keyword-only in auto-init. |
|
Literal default value for the field. |
|
Factory function |
|
Include/exclude field from repr output. |
|
Include/exclude field from recursive hashing. |
|
Include/exclude field from recursive comparison. |
|
Attach custom key-value metadata to the field. |
Dataclass Operations#
Once a class is registered with ObjectDef, several dataclass operations are
available automatically. These are defined in
include/tvm/ffi/extra/dataclass.h:
#include <tvm/ffi/extra/dataclass.h>
ffi::Any value = ...;
ffi::Any copy = ffi::DeepCopy(value);
ffi::String repr = ffi::ReprPrint(value);
int64_t h = ffi::RecursiveHash(value);
bool eq = ffi::RecursiveEq(a, b);
All operations use iterative DFS with an explicit stack (no recursion), so they are safe for deep object graphs.
Deep Copy#
DeepCopy(value) recursively copies an object and all reachable objects in
its graph. Objects that register a copy constructor via ObjectDef
automatically support deep copy (through the __ffi_shallow_copy__ type
attribute).
Immutable leaves (returned as-is): primitives,
String,Bytes,ShapeRecursively copied:
Array,List,Map,Dict, and reflected objects
Repr#
ReprPrint(value) produces a human-readable string representation using
field names and values from reflection metadata. It handles cycles (prints
...) and DAG structures (caches repr strings).
Exclude a field from repr output with refl::repr(false):
.def_ro("internal_state", &Obj::internal_state, refl::repr(false))
Hashing#
RecursiveHash(value) computes a deterministic recursive hash. The hash is
consistent with equality: if RecursiveEq(a, b) then
RecursiveHash(a) == RecursiveHash(b).
Exclude a field from hashing with refl::hash(false):
.def_ro("cache_key", &Obj::cache_key, refl::hash(false))
Comparison#
RecursiveEq(a, b) tests structural equality. Ordering comparisons
(RecursiveLt, RecursiveLe, RecursiveGt, RecursiveGe) provide
lexicographic field-by-field ordering.
Exclude a field from comparison with refl::compare(false):
.def_ro("timestamp", &Obj::timestamp, refl::compare(false))
In Python, these are wired up as __eq__, __lt__, __le__, __gt__,
__ge__, and __hash__ on classes created with c_class.
Custom Hooks#
Override the default behavior for repr, hash, or comparison by registering
type-level attributes via TypeAttrDef:
namespace refl = ffi::reflection;
// Custom hash: only hash the "key" field
refl::TypeAttrDef<MyObj>().def(
refl::type_attr::kHash,
[](const Object* self, const Function& fn_hash) -> int64_t {
auto* obj = static_cast<const MyObj*>(self);
return fn_hash(AnyView(obj->key)).cast<int64_t>();
});
// Custom equality: only compare "key" fields
refl::TypeAttrDef<MyObj>().def(
refl::type_attr::kEq,
[](const Object* lhs, const Object* rhs, const Function& fn_eq) -> bool {
auto* a = static_cast<const MyObj*>(lhs);
auto* b = static_cast<const MyObj*>(rhs);
return fn_eq(AnyView(a->key), AnyView(b->key)).cast<bool>();
});
// Custom three-way comparison
refl::TypeAttrDef<MyObj>().def(
refl::type_attr::kCompare,
[](const Object* lhs, const Object* rhs, const Function& fn_cmp) -> int32_t {
auto* a = static_cast<const MyObj*>(lhs);
auto* b = static_cast<const MyObj*>(rhs);
return fn_cmp(AnyView(a->key), AnyView(b->key)).cast<int32_t>();
});
// Custom repr
refl::TypeAttrDef<MyObj>().def(
refl::type_attr::kRepr,
[](const Object* self, const Function& fn_repr) -> String {
auto* obj = static_cast<const MyObj*>(self);
return "MyObj(key=" + fn_repr(AnyView(obj->key)).cast<String>() + ")";
});
Attribute |
Constant |
Signature |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
The fn_hash, fn_eq, fn_cmp, and fn_repr callbacks are provided
by the framework for recursing into child values.
Python c_class Decorator#
On the Python side, tvm_ffi.dataclasses.c_class provides equivalent
functionality to Python’s @dataclass for C++-backed objects:
from tvm_ffi.dataclasses import c_class
@c_class("my_ext.Point")
class Point(tvm_ffi.Object):
x: int
y: int
label: str
The decorator:
Looks up the C++ type info registered by
ObjectDef<PointObj>.Matches Python annotations to C++ fields.
Generates
__init__from__ffi_init__, respectingkw_only, defaults, andinit(false)settings from C++.Installs
__copy__,__deepcopy__,__eq__,__hash__,__repr__, and comparison operators.
Note
@tvm_ffi.register_object can also be used, which delegates to
c_class internally for objects with reflected fields.
Inheritance#
Dataclass traits compose across inheritance. A child class inherits the parent’s fields and adds its own:
class ParentObj : public ffi::Object {
public:
int64_t parent_required;
int64_t parent_default;
static constexpr bool _type_mutable = true;
static constexpr uint32_t _type_child_slots = 1;
TVM_FFI_DECLARE_OBJECT_INFO("my_ext.Parent", ParentObj, ffi::Object);
};
class ChildObj : public ParentObj {
public:
int64_t child_required;
int64_t child_kw_only;
TVM_FFI_DECLARE_OBJECT_INFO_FINAL("my_ext.Child", ChildObj, ParentObj);
};
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = ffi::reflection;
refl::ObjectDef<ParentObj>()
.def_rw("parent_required", &ParentObj::parent_required)
.def_rw("parent_default", &ParentObj::parent_default,
refl::default_(int64_t{5}));
refl::ObjectDef<ChildObj>()
.def_rw("child_required", &ChildObj::child_required)
.def_rw("child_kw_only", &ChildObj::child_kw_only, refl::kw_only(true));
}
In Python, the child’s auto-init includes all fields:
# Generated signature:
# def __init__(self, parent_required, child_required, parent_default=5, *, child_kw_only):
child = Child(1, 2, child_kw_only=3)
assert child.parent_required == 1
assert child.child_required == 2
assert child.parent_default == 5 # uses default
assert child.child_kw_only == 3
Further Reading#
Export Functions and Classes: Basic function and class export guide
Object and Class: Object system fundamentals, type registry, and ABI
Stub Generation: Auto-generating Python type stubs from reflection metadata
C++ Guide: Full C++ API guide covering
Any,Function, containers