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,Shape, andEnuminstances (includingIntEnumandStrEnumsubclasses)Recursively 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.
Use frozen=True when the Python class should expose reflected fields as
read-only after construction:
@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
c_class internally for objects with reflected fields.
Enum Payload Fields and Structural Identity#
tvm_ffi.dataclasses.Enum and all of its subclasses use "singleton"
structural equality by default. This includes IntEnum, StrEnum, and
their subclasses: two enum values are structurally equal only when they are
the same registered variant.
tvm_ffi.dataclasses.IntEnum and StrEnum are reflected subclasses of
ffi.IntEnum and ffi.StrEnum. Their user-visible value payload is
stored by the native base class, so subclasses only declare entries:
from tvm_ffi.dataclasses import IntEnum, StrEnum, entry
class Level(IntEnum, type_key="my_ext.Level"):
low = entry(value=1)
high = entry(value=2)
class Opcode(StrEnum, type_key="my_ext.Opcode"):
add = entry(value="+")
mul = entry(value="*")
Enum-specific __data_to_json__ and __data_from_json__ hooks preserve
this singleton identity during JSON graph serialization. Following the scalar
TypeAttrDef hook pattern, plain Enum variants serialize as their
registered name, while IntEnum and StrEnum variants serialize as their
public value payload (an integer or string, respectively). Deserialization
uses that scalar to resolve the registered variant instead of allocating a
replacement object. Singleton-preserving round-trips therefore require payload
values to be unique within an enum type. This applies both to an enum serialized
directly and to enum-valued fields nested in another reflected object:
from tvm_ffi.serialization import from_json_graph_str, to_json_graph_str
data = to_json_graph_str(Opcode.add)
restored = from_json_graph_str(data)
assert restored.same_as(Opcode.add)
C++ enum owners register the value-based hook pair on their concrete
ObjectDef or TypeAttrDef. Python-defined IntEnum and StrEnum
classes obtain missing hooks from C++ reflection with
core._lookup_type_attr: the serializer is reused from the native payload
base, while a native factory binds the deserializer to the concrete Python
type index. This keeps payload lookup in C++ while leaving explicit Python
hooks unchanged. A plain Enum owner defines its name-based hook pair
explicitly; EnumDef itself only registers variants.
Payload enum values can be used in reflected object fields, including optional
and container fields. Payload normalization recurses through Array, List,
Map, and Dict values.
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