Function and Module#
TVM-FFI provides a unified and ABI-stable calling convention that enables cross-language function calls between C++, Python, Rust, and other languages. Functions are first-class TVM-FFI objects.
This tutorial covers defining, registering, and calling TVM-FFI functions, exception handling, and working with modules.
Glossary#
- TVM-FFI ABI, or “Packed Function”.
TVMFFISafeCallType A stable C calling convention where every function is represented by a single signature, which enables type-erased, cross-language function calls. This calling convention is used across all TVM-FFI function calls at the ABI boundary. See Stable C ABI for a quick introduction.
- TVM-FFI Function.
tvm_ffi.Function,tvm::ffi::FunctionObj,tvm::ffi::Function A reference-counted function object and its managed reference, which wraps any callable, including language-agnostic functions and lambdas (C++, Python, Rust, etc.), member functions, external C symbols, and other callable objects, all sharing the same calling convention.
- TVM-FFI Module.
tvm_ffi.Module,tvm::ffi::ModuleObj,tvm::ffi::Module A namespace for a collection of functions, loaded from a shared library via
dlopen(Linux, macOS) orLoadLibraryW(Windows), or statically linked to the current executable.- Global Functions and Registry.
tvm_ffi.get_global_func()andtvm_ffi.register_global_func() A registry is a table that maps string names to
Functionobjects and their metadata (name, docs, signatures, etc.) for cross-language access. Functions in the registry are called global functions.
Common Usage#
TVM-FFI C Symbols#
Shared library. Use TVM_FFI_DLL_EXPORT_TYPED_FUNC to export
a function as a C symbol that follows the TVM-FFI ABI:
static int AddTwo(int x) { return x + 2; }
TVM_FFI_DLL_EXPORT_TYPED_FUNC(/*ExportName=*/add_two, /*Function=*/AddTwo)
This creates a C symbol __tvm_ffi_<ExportName> in the shared library,
which can then be loaded and called via tvm_ffi.load_module():
import tvm_ffi
mod = tvm_ffi.load_module("path/to/library.so")
result = mod.add_two(40) # -> 42
System library. For symbols bundled in the same executable, use TVMFFIEnvModRegisterSystemLibSymbol()
to register each symbol during static initialization within a TVM_FFI_STATIC_INIT_BLOCK.
See tvm_ffi.system_lib() for a complete workflow.
Global Functions#
Register a global function. In C++, use tvm::ffi::reflection::GlobalDef to
register a function:
#include <tvm/ffi/tvm_ffi.h>
static int AddOne(int x) { return x + 1; }
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef()
.def("my_ext.add_one", AddOne, "Add one to the input");
}
The TVM_FFI_STATIC_INIT_BLOCK macro ensures that registration occurs
during library initialization. The registered function is then accessible from
Python by the name my_ext.add_one.
In Python, use the decorator tvm_ffi.register_global_func() to register a global function:
import tvm_ffi
@tvm_ffi.register_global_func("my_ext.add_one")
def add_one(x: int) -> int:
return x + 1
Retrieve a global function. After registration, functions are accessible by name.
In Python, use tvm_ffi.get_global_func() to retrieve a global function:
import tvm_ffi
# Get a function from the global registry
add_one = tvm_ffi.get_global_func("my_ext.add_one")
result = add_one(41) # -> 42
In C++, use tvm::ffi::Function::GetGlobal() or tvm::ffi::Function::GetGlobalRequired()
to retrieve a global function:
ffi::Function func = ffi::Function::GetGlobalRequired("my_ext.add_one");
int result = func(41); // -> 42
Create Functions#
From C++. An tvm::ffi::Function can be created via tvm::ffi::Function::FromTyped()
or tvm::ffi::TypedFunction’s constructor.
// Create type-erased function: add_type_erased
ffi::Function add_type_erased = ffi::Function::FromTyped([](int x, int y) {
return x + y;
});
// Create a typed function: add_typed
ffi::TypedFunction<int(int, int)> add_typed = [](int x, int y) {
return x + y;
};
// Convert a typed function to a type-erased function
ffi::Function generic = add_typed;
From Python. Any Python Callable is automatically converted
to a tvm_ffi.Function at the ABI boundary. The example below demonstrates that in my_ext.bind:
The input
funcis automatically converted to atvm_ffi.Function.The returned lambda is also automatically converted to a
tvm_ffi.Function.
import tvm_ffi
@tvm_ffi.register_global_func("my_ext.bind")
def bind(func, x):
assert isinstance(func, tvm_ffi.Function)
return lambda *args: func(x, *args) # converted to `tvm_ffi.Function`
def add_x_y(x, y):
return x + y
func_bind = tvm_ffi.get_global_func("my_ext.bind")
add_y = func_bind(add_x_y, 1) # bind x = 1
assert isinstance(add_y, tvm_ffi.Function)
print(add_y(2)) # -> 3
tvm_ffi.convert() explicitly converts a Python callable to tvm_ffi.Function:
import tvm_ffi
def add(x, y):
return x + y
func_add = tvm_ffi.convert(add)
print(func_add(1, 2))
Function#
Calling Convention#
All TVM-FFI functions ultimately conform to the TVMFFISafeCallType signature,
which provides a stable C ABI for cross-language calls. The C calling convention is defined as:
int tvm_ffi_c_abi(
void* handle, // Resource handle
const TVMFFIAny* args, // Input arguments (non-owning)
int32_t num_args, // Number of input arguments
TVMFFIAny* result // Output argument (owning, zero-initialized)
);
Input arguments. The input arguments are passed as an array of tvm::ffi::AnyView values,
specified by args and num_args.
Output argument. The output argument result is an owning tvm::ffi::Any
that the caller must zero-initialize before the call.
Important
The caller must zero-initialize the output argument result before the call.
Return value. The ABI returns an error code that indicates:
Error code 0: Success
Error code -1: Error occurred, retrievable with
TVMFFIErrorMoveFromRaised()Error code -2: Very rare frontend error
Hint
See Any for more details on the semantics of tvm::ffi::AnyView and tvm::ffi::Any.
This design is called a packed function, because it “packs” all arguments into a single array of type-erased tvm::ffi::AnyView,
and further unifies calling convention across all languages without resorting to JIT compilation.
More specifically, this mechanism enables the following scenarios:
Dynamic languages. Well-optimized bindings are provided for, e.g. Python, to translate arguments into packed function format, and translate return value back to the host language.
Static languages. Metaprogramming techniques, such as C++ templates, are usually available to directly instantiate packed format on stack, saving the need for dynamic examination.
Cross-language callbacks. Language-agnostic
tvm::ffi::Functionmakes it easy to call between languages without depending on language-specific features such as GIL.
Performance Implications. This approach is in practice highly efficient in machine learning workloads.
In Python/C++ calls, we can get to microsecond level overhead, which is generally similar to overhead for eager mode;
When both sides of calls are static languages, the overhead will go down to tens of nanoseconds.
Note
Although we found it less necessary in practice, further link time optimization (LTO) is still theoretically possible in scenarios where both sides are static languages with a known symbol and linked into a single binary. In this case, the callee can be inlined into caller side and the stack argument memory can be passed into register passing.
Layout and ABI#
tvm::ffi::FunctionObj stores two call pointers in TVMFFIFunctionCell:
safe_call: Used for cross-ABI function calls; intercepts exceptions and stores them in TLS.cpp_call: Used within the same DSO; exceptions are thrown directly for better performance.
See Function for the C struct definition.
Important
TVMFFIFunctionCall() is the idiomatic way to call a tvm::ffi::FunctionObj in C,
while safe_call or cpp_call remain low-level ABIs for fast access.
Conversion with Any. Since tvm_ffi.Function is a TVM-FFI object, it follows the same
conversion rules as any other TVM-FFI object. See Object Conversion with Any for details.
Throw and Catch Errors#
TVM-FFI gracefully handles exceptions across language boundaries without requiring manual error code management.
Important
Stack traces from all languages are properly preserved and concatenated in the TVM-FFI Stable C ABI.
Python. In Python, raise native Exception instances or derived classes.
TVM-FFI catches these at the ABI boundary and converts them to tvm::ffi::Error objects.
When C++ code calls into Python and a Python exception occurs, it propagates back to C++ as a
tvm::ffi::Error, which C++ code can handle appropriately.
C++. In C++, use tvm::ffi::Error or the TVM_FFI_THROW macro:
#include <tvm/ffi/error.h>
void ThrowError(int x) {
if (x < 0) {
TVM_FFI_THROW(ValueError) << "x must be non-negative, got " << x;
}
}
The TVM_FFI_THROW macro captures the current file name, line number, stack trace,
and error message, then constructs a tvm::ffi::Error object. At the ABI boundary,
this error is stored in TLS and the function returns -1 per the TVMFFISafeCallType
calling convention.
Hint
A detailed implementation of such graceful handling behavior can be found
in TVM_FFI_SAFE_CALL_BEGIN / TVM_FFI_SAFE_CALL_END macros.
Compiler developers commonly need to look up global functions in generated code.
Use TVMFFIFunctionGetGlobal() to retrieve a function by name, then call it with TVMFFIFunctionCall().
See Function for C code examples.
Exception#
This section describes the exception handling contract in the TVM-FFI Stable C ABI. Exceptions are first-class citizens in TVM-FFI, and this section specifies:
How to properly throw exceptions from a TVM-FFI ABI function
How to check for and propagate exceptions from a TVM-FFI ABI function
When a TVM-FFI function returns a non-zero code, an error occurred.
An ErrorObj is stored in thread-local storage (TLS) and can be retrieved
with TVMFFIErrorMoveFromRaised().
Error code -1: Retrieve the error from TLS, print it, and release via
TVMFFIObjectDecRef().Error code -2: A rare frontend error; consult the frontend’s error mechanism instead of TLS.
To raise an error, use TVMFFIErrorSetRaisedFromCStr() to set the TLS error and return -1.
For chains of calls, simply propagate return codes - TLS carries the error details.
See Exception for C code examples.
Modules#
A tvm_ffi.Module is a namespace for a collection of functions that can be loaded
from a shared library or bundled with the current executable. Modules provide namespace isolation
and dynamic loading capabilities for TVM-FFI functions.
System Library#
System library modules contain symbols that are statically linked to the current executable.
This technique is useful when you want to simulate dynamic module loading behavior but cannot
or prefer not to use dlopen or LoadLibraryW (e.g., on iOS). Functions are statically
linked to the executable as a system library module. Symbols can be registered via
TVMFFIEnvModRegisterSystemLibSymbol() and looked up via tvm_ffi.system_lib().
Register symbols in C/C++. Use TVMFFIEnvModRegisterSystemLibSymbol() to register
a symbol during static initialization:
#include <tvm/ffi/c_api.h>
#include <tvm/ffi/extra/c_env_api.h>
// A function following the TVM-FFI ABI
static int add_one_impl(void*, const TVMFFIAny* args, int32_t num_args, TVMFFIAny* result) {
TVM_FFI_SAFE_CALL_BEGIN();
int64_t x = reinterpret_cast<const tvm::ffi::AnyView*>(args)[0].cast<int64_t>();
reinterpret_cast<tvm::ffi::Any*>(result)[0] = x + 1;
TVM_FFI_SAFE_CALL_END();
}
// Register during static initialization
// The symbol name follows the convention `__tvm_ffi_<prefix>.<name>`
TVM_FFI_STATIC_INIT_BLOCK() {
TVMFFIEnvModRegisterSystemLibSymbol(
"__tvm_ffi_my_prefix.add_one",
reinterpret_cast<void*>(add_one_impl)
);
}
Access from Python. Use tvm_ffi.system_lib() to get the system library module:
import tvm_ffi
# Get system library with symbol prefix "my_prefix."
# This looks up symbols prefixed with `__tvm_ffi_my_prefix.`
mod = tvm_ffi.system_lib("my_prefix.")
# Call the registered function
func = mod.add_one # looks up `__tvm_ffi_my_prefix.add_one`
result = func(10) # -> 11
Note
The system library is intended for statically linked symbols that exist for the entire program lifetime. For dynamic loading with the ability to unload, use shared library modules instead.
Further Reading#
Any and AnyView: How functions are stored in
AnycontainersObject and Class: The object system that backs
FunctionObjABI Overview: Low-level C ABI details for functions and exceptions
Python Packaging: Packaging functions for Python wheels