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

cast<T>()

Returns T or throws tvm::ffi::Error

When you know the expected type and want an exception on mismatch

try_cast<T>()

Returns std::optional<T>

When you want graceful failure and allow type conversions (e.g., int to double)

as<T>()

Returns std::optional<T> or const T* (for TVM-FFI object types)

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

AnyView

Any

Ownership

Non-owning (like std::string_view)

Owning (like std::string)

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.

Layout of the 128-bit Any tagged union

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.

Figure 2. Common atomic types stored directly in TVMFFIAny#

Type

type_index

Payload Field

None / nullptr

kTVMFFINone = 0

v_int64 (must be 0)

int64_t

kTVMFFIInt = 1

v_int64

bool

kTVMFFIBool = 2

v_int64 (0 or 1)

float64_t

kTVMFFIFloat = 3

v_float64

void* (opaque pointer)

kTVMFFIOpaquePtr = 4

v_ptr

DLDataType

kTVMFFIDataType = 5

v_dtype

DLDevice

kTVMFFIDevice = 6

v_device

DLTensor*

kTVMFFIDLTensorPtr = 7

v_ptr

const char* (raw string)

kTVMFFIRawStr = 8

v_c_str

TVMFFIByteArray*

kTVMFFIByteArrayPtr = 9

v_ptr

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#

Figure 3. Common TVM-FFI object types stored as pointers in TVMFFIAny::v_obj.#

Type

type_index

Payload Field

ErrorObj*

kTVMFFIError = 67

v_obj

FunctionObj*

kTVMFFIFunction = 68

v_obj

TensorObj*

kTVMFFITensor = 70

v_obj

ArrayObj*

kTVMFFIArray = 71

v_obj

MapObj*

kTVMFFIMap = 72

v_obj

ModuleObj*

kTVMFFIModule = 73

v_obj

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#