Any and AnyView#
TVM-FFI has tvm::ffi::Any and tvm::ffi::AnyView,
type-erased containers that hold any supported value and transport it across
C, C++, Python, and Rust boundaries through a stable ABI.
Similar to std::any, Any is a tagged union that stores
values of a wide variety of types, including primitives, objects, and strings.
Unlike std::any, it is designed for zero-copy inter-language exchange without RTTI,
featuring a fixed 16-byte layout with built-in reference counting and ownership semantics.
This tutorial covers everything you need to know about Any and AnyView:
common usage patterns, ownership semantics, and memory layout.
Common Usage#
Function Signatures#
Use AnyView for function parameters
to avoid reference count overhead and unnecessary copies.
Use Any for return values to transfer ownership to the caller.
ffi::Any func_cpp_signature(ffi::AnyView arg0, ffi::AnyView arg1) {
ffi::Any result = arg0.cast<int>() + arg1.cast<int>();
return result;
}
// Variant: variadic function
void func_cpp_variadic(PackedArgs args, Any* ret) {
int32_t num_args = args.size();
int x0 = args[0].cast<int>();
int x1 = args[1].cast<int>();
int y = x0 + x1;
*ret = y;
}
// Variant: variadic function with C ABI signature
int func_c_abi_variadic(void*, const TVMFFIAny* args, int32_t num_args, TVMFFIAny* ret) {
TVM_FFI_SAFE_CALL_BEGIN();
int x0 = reinterpret_cast<const AnyView*>(args)[0].cast<int>();
int x1 = reinterpret_cast<const AnyView*>(args)[1].cast<int>();
int y = x0 + x1;
reinterpret_cast<Any*>(ret)[0] = y;
TVM_FFI_SAFE_CALL_END();
}
Container Storage#
Any can be stored in containers like Map and Array:
ffi::Map<ffi::String, ffi::Any> config;
config.Set("learning_rate", 0.001);
config.Set("batch_size", 32);
config.Set("device", DLDevice{kDLCUDA, 0});
Extracting Values#
Three methods extract values from Any
and AnyView, each with different levels of strictness:
Method |
Behavior |
Use When |
|---|---|---|
Returns |
When you know the expected type and want an exception on mismatch |
|
Returns |
When you want graceful failure and allow type conversions (e.g., int to double) |
|
Returns |
When you need an exact type match with no conversions |
Example of cast<T>()
cast<T>() is the workhorse. It returns the value or throws:
ffi::Any value = 42;
int x = value.cast<int>(); // OK: 42
double y = value.cast<double>(); // OK: 42.0 (int → double)
try {
ffi::String s = value.cast<ffi::String>(); // Throws TypeError
} catch (const ffi::Error& e) {
// "Cannot convert from type `int` to `ffi.Str`"
}
Example of try_cast<T>()
try_cast<T>() allows type coercion:
ffi::Any value = 42;
std::optional<double> opt_float = value.try_cast<double>();
// opt_float.has_value() == true, *opt_float == 42.0
std::optional<bool> opt_bool = value.try_cast<bool>();
// opt_bool.has_value() == true, *opt_bool == true
Example of as<T>()
as<T>() is strict - it succeeds only if the stored type matches exactly:
ffi::Any value = 42;
std::optional<int64_t> opt_int = value.as<int64_t>();
// opt_int.has_value() == true
std::optional<double> opt_float = value.as<double>();
// opt_float.has_value() == false (int stored, not float)
ffi::Any str_value = ffi::String("hello, world!");
if (const ffi::Object* obj = str_value.as<ffi::Object>()) {
// Use obj without copying
}
Nullability Checks#
Compare with nullptr to check for None:
ffi::Any value = std::nullopt;
if (value == nullptr) {
// Handle None case
} else {
// Process value
}
Ownership#
The core distinction between tvm::ffi::Any and
tvm::ffi::AnyView is ownership:
Aspect |
||
|---|---|---|
Ownership |
Non-owning (like |
Owning (like |
Reference counting |
No reference count changes on copy |
Increments reference count on copy; decrements on destroy |
Lifetime |
Valid only while source lives |
Extends object lifetime |
Primary use |
Function inputs |
Return values, storage |
Code Examples#
AnyView is a lightweight, non-owning view. Copying it simply
copies 16 bytes with no reference count updates, making it ideal for passing arguments without overhead:
void process(ffi::AnyView value) {}
Any is an owning container. Copying an Any that holds an object
increments the reference count; destroying it decrements the count:
ffi::Any create_value() {
ffi::Any result;
{
ffi::String str = "hello"; // refcount = 1 (str created)
result = str; // refcount 1 -> 2 (result owns str)
} // refcount 2 -> 1 (str is destroyed)
return result; // refcount = 1 (result returns to caller)
}
ABI Boundary#
TVM-FFI’s function calling convention follows two simple rules:
Inputs are non-owning: Arguments are passed as
AnyView. The caller retains ownership, and the callee borrows them for the duration of the call.Outputs are owning: Return values are passed as
Any. Ownership transfers to the caller, who becomes responsible for managing the value’s lifetime.
// TVM-FFI C ABI
int32_t tvm_ffi_c_abi(
void* handle,
const AnyView* args, // (Non-owning) args: AnyView[num_args]
int32_t num_args,
Any* result, // (Owning) result: Any (caller takes ownership)
);
Destruction Semantics in C#
In C, which lacks RAII, you must manually destroy Any objects
by calling TVMFFIObjectDecRef() for heap-allocated objects.
void destroy_any(TVMFFIAny* any) {
if (any->type_index >= kTVMFFIStaticObjectBegin) {
// Decrement the reference count of the heap-allocated object
TVMFFIObjectDecRef(any->v_obj);
}
*any = (TVMFFIAny){0};
}
In contrast, destroying an AnyView is effectively a no-op - just clear its contents.
void destroy_any_view(TVMFFIAny* any_view) {
*any_view = (TVMFFIAny){0};
}
Layout#
Tagged Union#
At C ABI level, every value lives in a TVMFFIAny:
typedef struct TVMFFIAny {
int32_t type_index; // Bytes 0-3: identifies the stored type
union {
uint32_t zero_padding; // Bytes 4-7: must be zero (or small_str_len)
uint32_t small_str_len;
};
union { // Bytes 8-15: the actual value
int64_t v_int64;
double v_float64;
void* v_ptr;
DLDataType v_dtype;
DLDevice v_device;
TVMFFIObject* v_obj;
// ... other union members
};
} TVMFFIAny;
Tip
Think of TVMFFIAny as the “layout format”, and Any/AnyView
as a thin “application layer” that adds type safety, RAII, and ergonomic APIs, which has no change to the layout.
Figure 1. Layout of the TVMFFIAny tagged union in C ABI. Any/AnyView
shares the same layout as TVMFFIAny, but adds extra C++ APIs on top of it for type safety, RAII, and ergonomics.#
It is effectively a layout-stable 16-byte tagged union.
The first 4 bytes (
TVMFFIAny::type_index) serve as a tag identifying the stored type.The last 8 bytes hold the actual value - either stored inline for atomic types (e.g.,
int64_t,float64,void*) or as a pointer to a heap-allocated object.
Atomic Types#
Primitive values - integers, floats, booleans, devices, and raw pointers - are stored directly in the 8-byte payload with no heap allocation and no reference counting.
Type |
type_index |
Payload Field |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||
|
||
|
|
|
Figure 2 shows common atomic types stored in-place inside the
TVMFFIAny payload.
AnyView int_val = 42; // v_int64 = 42
AnyView float_val = 3.14; // v_float64 = 3.14
AnyView bool_val = true; // v_int64 = 1
AnyView device = DLDevice{kDLCUDA, 0}; // v_device
DLTensor tensor;
AnyView view = &tensor; // v_ptr = &tensor
Note that raw pointers like DLTensor* and char* also fit here.
These pointers carry no ownership, so the caller must ensure the pointed-to data outlives
the AnyView or Any.
Heap-Allocated Objects#
Type |
type_index |
Payload Field |
|---|---|---|
|
||
|
||
|
||
|
||
|
||
|
Heap-allocated objects - String, Function, Tensor, Array, Map, and custom types - are
stored as pointers to reference-counted TVMFFIObject headers:
ffi::String str = "hello world";
ffi::Any any_str = str; // v_obj points to StringObj
// Object layout in memory:
// [TVMFFIObject header (24 bytes)][object-specific data]
Caveats#
Small String Optimization#
Strings and byte arrays receive special treatment: values of 7 bytes or fewer are stored inline using small string optimization, avoiding heap allocation entirely:
ffi::Any small = "hello"; // kTVMFFISmallStr, in v_bytes
ffi::Any large = "this is a longer string"; // kTVMFFIStr, heap allocated
Further Reading#
Object system: Object and Class covers how TVM-FFI objects work, including reference counting and type checking
C examples: Stable C ABI demonstrates working with
TVMFFIAnydirectly in CTensor conversions: Tensor and DLPack covers how tensors flow through
AnyandAnyViewFunction calling: C++ Guide explains how functions use
Anyfor arguments and returns