Object and Class#

TVM-FFI provides a unified object system that enables cross-language interoperability between C++, Python, and Rust. The object system is built around tvm::ffi::Object and tvm::ffi::ObjectRef, which together form the foundation for:

  • Type-safe runtime type identification without relying on C++ RTTI

  • Intrusive reference counting for smart memory management

  • Reflection-based class exposure across programming languages

  • Serialization and deserialization via reflection metadata

This tutorial covers defining, using, and extending TVM-FFI objects across languages.

Glossary#

tvm::ffi::Object

A heap-allocated, reference-counted container. All TVM-FFI objects inherit from this base class and share a common 24-byte header that stores reference counts, type index, and a deleter callback.

tvm::ffi::ObjectRef

An intrusive pointer that manages an Object’s lifetime through reference counting. Its subclasses provide type-safe access to specific object types. In its low-level implementation, it is equivalent to a normal C++ pointer to a heap-allocated Object.

Type index and type key

Type index is an integer that uniquely identifies each object type. Built-in types have statically assigned indices defined in TVMFFITypeIndex (see Atomic Types and Heap-Allocated Objects for the complete list), while user-defined types receive indices at startup when first accessed. Type key is a unique string identifier (e.g., "my_ext.MyClass") that names an object type. It is used for registration, serialization, and cross-language mapping.

Common Usage#

Define a Class in C++#

To define a custom object class in normal C++, inherit it from tvm::ffi::Object or its subclasses, and then add one of the following macros that declares its metadata:

TVM_FFI_DECLARE_OBJECT_INFO(TypeKey, TypeName, ParentType)

Declare an object type that can be subclassed. Type index is assigned dynamically.

TVM_FFI_DECLARE_OBJECT_INFO_FINAL(TypeKey, TypeName, ParentType)

Declare a final object type (no subclasses). Enables faster type checking.

Example. The code below shows a minimal example of defining a TVM-FFI object class. It declares a class MyObjectObj that inherits from Object.

#include <tvm/ffi/tvm_ffi.h>

namespace ffi = tvm::ffi;

class MyObjectObj : public ffi::Object {
 public:
  // Normal C++ code: Declare fields, methods, constructor, destructor, etc.
  int64_t value;
  ffi::String name;

  MyObjectObj(int64_t value, ffi::String name) : value(value), name(std::move(name)) {}

  int64_t GetValue() const { return value; }

  void AddToValue(int64_t other) { value += other; }

  // Declare object type info
  TVM_FFI_DECLARE_OBJECT_INFO(
    /*type_key=*/"my_ext.MyObject",
    /*type_name=*/MyObjectObj,
    /*parent_type=*/ffi::Object);
};

Named managed reference. ObjectRef is the base class for named, managed references to TVM-FFI objects. Each reference holds an ObjectPtr<Object>, which manages the underlying object’s lifetime through reference counting. Use operator-> to access data members and member functions of the underlying object.

A reference class can define its own constructors, member functions, and operators, and inherit from other reference classes to share and specialize its interface. It can also impose additional constraints on the underlying object. For example, Array<T> uses ArrayObj as its underlying object type while constraining the array’s element type to T.

Optionally, define a named managed reference class by inheriting from ObjectRef and using one of the following macros. A constructor that creates the underlying object can wrap tvm::ffi::make_object().

For example, a non-nullable reference class MyObject can be defined as follows:

class MyObject : public ffi::ObjectRef {
 public:
  MyObject(int64_t value, ffi::String name)
      : ObjectRef(ffi::make_object<MyObjectObj>(value, std::move(name))) {}
  TVM_FFI_DEFINE_OBJECT_REF_METHODS_NOTNULLABLE(
     /*type_name=*/MyObject,
     /*parent_type=*/ffi::ObjectRef,
     /*object_name=*/MyObjectObj);
};

// Create a managed object
MyObject obj = MyObject(42, "hello");

// Access fields via operator->
std::cout << obj->value << std::endl;  // -> 42

Expose a Class in Python#

Reflection. The object’s metadata is used for reflection. Use tvm::ffi::reflection::ObjectDef to register an object’s constructor, fields, and methods.

TVM_FFI_STATIC_INIT_BLOCK() {
  namespace refl = tvm::ffi::reflection;
  refl::ObjectDef<MyObjectObj>()
      // Register constructor with signature
      .def(refl::init<int64_t, ffi::String>())
      // Register read-write fields
      .def_rw("value", &MyObjectObj::value, "The integer value")
      .def_rw("name", &MyObjectObj::name, "The name string")
      // Register methods
      .def("get_value", &MyObjectObj::GetValue, "Returns the value");
}

Python binding. After registration, the object is automatically available in Python. Use tvm_ffi.register_object() to bind a Python class to a registered C++ type:

import tvm_ffi
from typing import TYPE_CHECKING

@tvm_ffi.register_object("my_ext.MyObject")
class MyObject(tvm_ffi.Object):
    # tvm-ffi-stubgen(begin): object/my_ext.MyObject
    value: int
    name: str

    if TYPE_CHECKING:
      def __init__(self, value: int, name: str) -> None: ...
      def get_value(self) -> int: ...
    # tvm-ffi-stubgen(end)

# Create and use objects
obj = MyObject(42, "hello")
print(obj.value)        # -> 42
print(obj.get_value())  # -> 42
obj.value = 100         # Mutable field access

The decorator looks up the type key "my_ext.MyObject" in the C++ type registry and binds the Python class to it. Fields and methods registered via ObjectDef are automatically available on the Python class.

The tool tvm-ffi-stubgen automatically generates the Python type stubs (the code between the markers) from reflection metadata. See Stub Generation Tool for details.

Type Checking and Casting#

Type checking. Use Object::IsInstance<T>() for runtime type checking:

bool CheckType(const ffi::ObjectRef& obj) {
  if (obj->IsInstance<MyObjectObj>()) {
    // obj is a MyObjectObj or subclass
    return true;
  }
  return false;
}

Type casting. Use ObjectRef::as<T>() for safe downcasting. For strict conversion that throws on mismatch, use cast() (see Exception Handling for error handling details):

ffi::ObjectRef obj = ...;

// as<ObjectType>() returns a pointer (nullptr if type doesn't match)
if (const MyObjectObj* ptr = obj.as<MyObjectObj>()) {
  std::cout << ptr->value << std::endl;
}

// as<ObjectRefType>() returns std::optional
if (auto opt = obj.as<MyObject>()) {
  std::cout << opt->get()->value << std::endl;
}

Type info. Type index is available via ObjectRef::type_index() and type key is available via ObjectRef::GetTypeKey(). These methods can be used to safely identify object types without relying on C++ RTTI.

Note

C++ RTTI (e.g. typeid, dynamic_cast) is strictly not useful in TVM-FFI-based approaches.

Miscellaneous APIs#

C++ Serialization. Use tvm::ffi::ToJSONGraph() to serialize an object to a JSON value, and tvm::ffi::FromJSONGraph() to deserialize a JSON value to an object.

#include <tvm/ffi/extra/serialization.h>

// Serialize to JSON
ffi::Any obj = ...;
ffi::json::Value json = ffi::ToJSONGraph(obj);

// Deserialize from JSON
ffi::Any restored = ffi::FromJSONGraph(json);

Python Serialization. Pickle is overloaded in Python to support TVM-FFI object serialization. Or explicitly use the tvm_ffi.serialization.to_json_graph_str() and tvm_ffi.serialization.from_json_graph_str() to serialize and deserialize an object to a JSON string.

import pickle

obj = MyObject(42, "test")
data = pickle.dumps(obj)
restored = pickle.loads(data)

Convert between raw and managed references. Use tvm::ffi::GetRef() to convert a raw object pointer to a managed reference, and tvm::ffi::ObjectRef::get() to convert a managed reference to a raw object pointer.

ABI and Layout#

Stable C Layout. All subclasses of tvm::ffi::Object share a common 24-byte header (TVMFFIObject) containing reference counts, type index, and a deleter callback. See Object for the C struct definition. tvm::ffi::ObjectRef and tvm::ffi::ObjectPtr are smart pointers equivalent to a single void* pointer.

Reference Counting#

See also

Ownership Management for C code examples.

Intrusive reference counting. The reference count is stored directly in the object header, not in a separate control block. This design reduces memory overhead and improves cache locality. The TVMFFIObject::combined_ref_count field packs both strong (lower 32 bits) and weak (upper 32 bits) reference counts in a single 64-bit integer.

C APIs are provided to manipulate the reference count:

Deleter. When an object is managed by ObjectRef, the deleter callback is invoked:

  • When strong reference count reaches zero: the object’s destructor is called.

  • When weak reference count reaches zero: the memory is freed.

The flags in TVMFFIObjectDeleterFlagBitMask indicate which action to perform.

Conversion with Any. At the stable C ABI boundary, TVM-FFI passes values using Any (owning) or AnyView (non-owning). Object handles are stored in the TVMFFIAny::v_obj field with a type index >= kTVMFFIStaticObjectBegin.

See Ownership Management for C code examples demonstrating:

  • Extracting an object handle from TVMFFIAny

  • Storing an object handle into Any or AnyView

  • Managing ownership via reference counting

Object Type Registry#

TVM-FFI maintains a global type registry that keeps track of all registered object types, their inheritance relationships, and their reflection metadata.

Inheritance and Type Casting#

Warning

Only single inheritance is supported in TVM-FFI Object system.

TVM-FFI implements its own runtime type system that enables type-safe operations without relying on C++ RTTI. Every object carries a runtime type index in its header.

Example. Code below shows a minimal example of defining a base class and a derived class.

class MyBaseObj : public ffi::Object {
 public:
  TVM_FFI_DECLARE_OBJECT_INFO("my_ext.MyBase", MyBaseObj, ffi::Object);
};

class MyDerivedObj : public MyBaseObj {
 public:
  // Final class: no subclasses allowed
  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("my_ext.MyDerived", MyDerivedObj, MyBaseObj);
};

Registration happens automatically on first access. The TVM_FFI_DECLARE_OBJECT_INFO and TVM_FFI_DECLARE_OBJECT_INFO_FINAL macros use TVMFFITypeGetOrAllocIndex() internally to allocate a type index.

See Type Checking and Casting for how to use the type system.

Reflect Fields and Methods#

The reflection system enables cross-language exposure of C++ classes, their fields, and methods. Use ObjectDef<T> to register reflection metadata for object type T:

Method

Description

.def(init<Args...>())

Register a constructor with the given argument types

.def_ro("name", &T::field)

Register a read-only field

.def_rw("name", &T::field)

Register a read-write field

.def("name", &T::method)

Register a member method

.def_static("name", &func)

Register a static method

Example. Code below shows a minimal example of registering a class with reflection metadata.

class IntPairObj : public ffi::Object {
 public:
  int64_t a;
  int64_t b;

  IntPairObj(int64_t a, int64_t b) : a(a), b(b) {}

  int64_t Sum() const { return a + b; }

  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("my_ext.IntPair", IntPairObj, ffi::Object);
};

TVM_FFI_STATIC_INIT_BLOCK() {
  namespace refl = tvm::ffi::reflection;
  refl::ObjectDef<IntPairObj>()
      .def(refl::init<int64_t, int64_t>())
      .def_rw("a", &IntPairObj::a, "the first field")
      .def_rw("b", &IntPairObj::b, "the second field")
      .def("sum", &IntPairObj::Sum, "compute a + b");
}

Metadata and Documentation. Add documentation strings and custom metadata to fields and methods:

// The following example uses MyObjectObj defined earlier to show
// how to add documentation and metadata.
refl::ObjectDef<MyObjectObj>()
    .def_rw("value", &MyObjectObj::value,
            "The numeric value",                    // docstring
            refl::default_(0),                       // default value
            refl::Metadata{{"min", 0}, {"max", 100}})  // custom metadata
    .def("add_to_value", &MyObjectObj::AddToValue,
         "Add a value to the object's value field");

Python Interoperability#

Cross-language lifetime. Each Python tvm_ffi.Object instance holds a C handle (void*) that references the underlying C++ object. The Python wrapper increments the reference count when constructed and decrements when garbage collected.

obj = MyObject(42, "test")    # C++ object created, C++ refcount = 1
obj2 = obj                    # Python alias created, C++ refcount unchanged
del obj                       # Python alias removed, C++ refcount unchanged
del obj2                      # Last Python reference gone, C++ refcount -> 0, object destroyed

Further Reading#