blob: d964421c071afb933d9f3906aaa686e52baed87f [file] [view]
<!--- Licensed to the Apache Software Foundation (ASF) under one -->
<!--- or more contributor license agreements. See the NOTICE file -->
<!--- distributed with this work for additional information -->
<!--- regarding copyright ownership. The ASF licenses this file -->
<!--- to you under the Apache License, Version 2.0 (the -->
<!--- "License"); you may not use this file except in compliance -->
<!--- with the License. You may obtain a copy of the License at -->
<!--- http://www.apache.org/licenses/LICENSE-2.0 -->
<!--- Unless required by applicable law or agreed to in writing, -->
<!--- software distributed under the License is distributed on an -->
<!--- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -->
<!--- KIND, either express or implied. See the License for the -->
<!--- specific language governing permissions and limitations -->
<!--- under the License. -->
# C++ Guide
{#cpp-guide}
This guide introduces the tvm-ffi C++ API.
We provide C++ API on top of the stable C ABI to provide a type-safe and efficient way to work with the tvm-ffi.
The C++ API is designed to abstract away the complexity of the C ABI while maintaining full compatibility.
The C++ API builds around the following key concepts:
- **Any and AnyView**: Type-erased containers that can hold values of any supported type in tvm-ffi.
- **Function**: A type-erased "packed" function that can be invoked like normal functions.
- **Objects and ObjectRefs**: Reference-counted objects to manage on-heap data types.
Code examples in this guide use `EXPECT_EQ` for demonstration purposes, which is a testing framework macro. In actual applications, you would use standard C++ assertions or error handling.
You can find runnable code of the examples under tests/cpp/test_example.cc.
## Any and AnyView
```{seealso}
For a deep dive into Any including memory layout, ownership semantics, and the type conversion
machinery, see {doc}`../concepts/any`.
```
`Any` and `AnyView` are the foundation of tvm-ffi, providing
ways to store values that are compatible with the ffi system.
The following example shows how we can interact with Any and AnyView.
```cpp
#include <tvm/ffi/any.h>
void ExampleAny() {
namespace ffi = tvm::ffi;
// Create an Any from various types
// EXPECT_EQ is used here for demonstration purposes (testing framework)
ffi::Any int_value = 42;
ffi::Any float_value = 3.14;
ffi::Any string_value = "hello world";
// AnyView provides a lightweight view without ownership
ffi::AnyView view = int_value;
// we can cast Any/AnyView to a specific type
int extracted = view.cast<int>();
EXPECT_EQ(extracted, 42);
// If we are not sure about the type
// we can use as to get an optional value
std::optional<int> maybe_int = view.as<int>();
if (maybe_int.has_value()) {
EXPECT_EQ(maybe_int.value(), 42);
}
// Try cast is another version that will try to run the type
// conversion even if the type does not exactly match
std::optional<int> maybe_int_try = view.try_cast<int>();
if (maybe_int_try.has_value()) {
EXPECT_EQ(maybe_int_try.value(), 42);
}
}
```
At a high level, we can perform the following operations:
- We can store a value into Any, under the hood, Any will record the type of the value by its type_index.
- We can fetch a value from Any or AnyView using the `cast` function.
- If we are unsure about the type in Any, we can use `as` or `try_cast` function to get an optional value.
Under the hood, Any and AnyView store the value via the ABI convention and also manage the reference
counting correctly when the stored value is an on-heap object.
## Object and ObjectRef
The tvm-ffi object system provides the foundation for all managed, reference-counted objects
in the system. It enables type safety, cross-language compatibility, and efficient memory management.
The object system is built around four key classes: `Object`, `ObjectPtr`, `Arc`, and `ObjectRef`.
The `Object` class is the base class of all heap-allocated objects. It contains a common header
that includes the `type_index`, reference counter and deleter for the object.
Users do not need to explicitly manage these fields as part of the C++ API. Instead,
they are automatically managed through a smart pointer `ObjectPtr` which points
to a heap-allocated object instance.
The following code shows an example object and the creation of an `ObjectPtr`:
```cpp
#include <tvm/ffi/object.h>
#include <tvm/ffi/memory.h>
class MyIntPairObj : public tvm::ffi::Object {
public:
int64_t a;
int64_t b;
MyIntPairObj() = default;
MyIntPairObj(int64_t a, int64_t b) : a(a), b(b) {}
// Required: declare type information
// to register a dynamic type index through the system
TVM_FFI_DECLARE_OBJECT_INFO_FINAL("example.MyIntPair", MyIntPairObj, tvm::ffi::Object);
};
void ExampleObjectPtr() {
namespace ffi = tvm::ffi;
// make_arc allocates, initializes, and sets up reference counting and the deleter.
ffi::Arc<MyIntPairObj> required = ffi::make_arc<MyIntPairObj>(100, 200);
// ObjectPtr is the nullable one-pointer carrier.
ffi::ObjectPtr<MyIntPairObj> optional = required;
// EXPECT_EQ is used here for demonstration purposes (testing framework)
EXPECT_EQ(required->a, 100);
EXPECT_EQ(required->b, 200);
optional = nullptr;
}
```
For an unqualified `Object` subclass `T`, use `Arc<T>` when the object is required and
`ObjectPtr<T>` when it may be absent. Both are owning pointers with the same one-pointer layout and
reference-counting behavior. Copying through `Any` retains the object, while moving transfers its
reference. They can be used in typed containers such as `Array`, `List`, `Map`, `Dict`, `Tuple`,
`Optional`, `Variant`, and `Expected`, as well as reflected fields and function signatures.
`Arc<T>` has no default or `nullptr` constructor, and `make_arc<T>(args...)` is its normal creation
API. Its type schema is the bare object schema `{"type":"T"}`, so ABI `None` is rejected. By
contrast, `ObjectPtr<T>` remains mechanically nullable, maps a null pointer to ABI `None`, and has
schema `Optional[T]`. `Optional<Arc<T>>` is also supported and produces exactly `Optional[T]`; use
it when the native field needs the 16-byte `Optional` carrier rather than the 8-byte
`ObjectPtr<T>` carrier.
Normal C++ move semantics leave a moved-from `Arc` empty. `Arc(UnsafeInit{})` also creates an empty
value for reflection's controlled construct-then-populate path. These states are exceptions to the
safe API's non-null guarantee and must be populated before use; Arc operations rely on the invariant
without runtime validation. A reflected class with an `Arc` field should initialize that field
explicitly in its unsafe constructor, for example
`HolderObj(UnsafeInit) : required(UnsafeInit{}) {}`. Because `Arc` publicly inherits `ObjectPtr`,
deliberately mutating it through the base class can likewise bypass the guarantee and is unsafe.
Qualified pointee types such as `Arc<const T>`, `ObjectPtr<const T>`, volatile pointees, and
reference pointee types are not supported.
We typically provide a reference class that wraps the ObjectPtr.
The `ObjectRef` base class provides the interface and reference counting
functionality for these wrapper classes.
```cpp
#include <tvm/ffi/object.h>
#include <tvm/ffi/memory.h>
class MyIntPair : public tvm::ffi::ObjectRef {
public:
// Constructor
explicit MyIntPair(int64_t a, int64_t b) {
data_ = tvm::ffi::make_object<MyIntPairObj>(a, b);
}
// Required: define object reference methods
// This macro provides the necessary methods for ObjectRef functionality
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(MyIntPair, tvm::ffi::ObjectRef, MyIntPairObj);
};
void ExampleObjectRef() {
namespace ffi = tvm::ffi;
MyIntPair pair(100, 200);
// EXPECT_EQ is used here for demonstration purposes (testing framework)
EXPECT_EQ(pair->a, 100);
EXPECT_EQ(pair->b, 200);
}
```
**Note:** The ObjectRef provides a user-friendly interface while ObjectPtr handles the low-level memory management.
The ObjectRef acts as a smart pointer wrapper that automatically manages the ObjectPtr lifecycle.
The overall implementation pattern is as follows:
- **Object Class**: Inherits from `ffi::Object`, stores data and implements the core functionality.
- **Arc**: Non-null owning pointer used for required object values.
- **ObjectPtr**: Nullable owning pointer used when the object may be absent.
- **Ref Class**: Inherits from `ffi::ObjectRef`, provides a user-friendly interface and automatic memory management.
This design ensures efficient memory management while providing a clean API for users. Once we define an ObjectRef class,
we can integrate it with the Any, AnyView and Functions.
```cpp
#include <tvm/ffi/object.h>
#include <tvm/ffi/any.h>
void ExampleObjectRefAny() {
namespace ffi = tvm::ffi;
MyIntPair pair(100, 200);
ffi::Any any = pair;
MyIntPair pair2 = any.cast<MyIntPair>();
// Note: EXPECT_EQ is used here for demonstration purposes (testing framework)
EXPECT_EQ(pair2->a, 100);
EXPECT_EQ(pair2->b, 200);
}
```
Under the hood, ObjectPtr manages the lifecycle of the object through the same mechanism as shared pointers. We designed
the object to be intrusive, which means the reference counter and type index metadata are embedded at the header of each object.
This design allows us to allocate the control block and object memory together. As we will see in future sections,
all of our heap-allocated classes such as Function, on-heap String, Array and Map are managed using subclasses of Object,
and the user-facing classes such as Function are ObjectRefs.
We provide a collection of built-in object and reference types, which are sufficient for common cases.
Developers can also bring new object types as shown in the example of this section. We provide mechanisms
to expose these objects to other language bindings such as Python.
## Function
The `Function` class provides a type-safe way to create and invoke callable objects
through tvm-ffi ABI convention. We can create a `ffi::Function` from an existing typed lambda function.
```cpp
#include <tvm/ffi/function.h>
void ExampleFunctionFromTyped() {
namespace ffi = tvm::ffi;
// Create a function from a typed lambda
ffi::Function fadd1 = ffi::Function::FromTyped(
[](const int a) -> int { return a + 1; }
);
int b = fadd1(1).cast<int>();
// EXPECT_EQ is used here for demonstration purposes (testing framework)
EXPECT_EQ(b, 2);
}
```
Under the hood, tvm-ffi leverages Any and AnyView to create a unified ABI for
all functions. The following example demonstrates the low-level way of defining
a "packed" function for the same `fadd1`.
```cpp
void ExampleFunctionFromPacked() {
namespace ffi = tvm::ffi;
// Create a function from a typed lambda
ffi::Function fadd1 = ffi::Function::FromPacked(
[](const ffi::AnyView* args, int32_t num_args, ffi::Any* rv) {
// Check that we have exactly one argument
TVM_FFI_ICHECK_EQ(num_args, 1);
int a = args[0].cast<int>();
*rv = a + 1;
}
);
int b = fadd1(1).cast<int>();
// EXPECT_EQ is used here for demonstration purposes (testing framework)
EXPECT_EQ(b, 2);
}
```
At a high level, `ffi::Function` implements function calling by the following convention:
- The arguments are passed through an on-stack array of `ffi::AnyView`
- Return values are passed through `ffi::Any`
Because the return value is `ffi::Any`, we need to explicitly call `cast` to convert the return
value to the desirable type. Importantly, `ffi::Function` itself is a value type that is compatible
with tvm-ffi, which means we can pass it as an argument and return values. The following code shows
an example of passing a function as an argument and applying it inside.
```cpp
void ExampleFunctionPassFunction() {
namespace ffi = tvm::ffi;
// Create a function from a typed lambda
ffi::Function fapply = ffi::Function::FromTyped(
[](const ffi::Function f, ffi::Any param) { return f(param.cast<int>()); });
ffi::Function fadd1 = ffi::Function::FromTyped( //
[](const int a) -> int { return a + 1; });
int b = fapply(fadd1, 2).cast<int>();
// EXPECT_EQ is used here for demonstration purposes (testing framework)
EXPECT_EQ(b, 3);
}
```
This pattern is very powerful because we can construct `ffi::Function` not only from C++,
but from any languages that expose to the tvm-ffi ABI. For example, this means we can easily call functions
passed in or registered from Python for quick debugging or other purposes.
### Global Function Registry
Besides creating functions locally, tvm-ffi provides a global function registry that allows
functions to be registered and called across different modules and languages.
The following code shows an example
```cpp
#include <tvm/ffi/function.h>
#include <tvm/ffi/reflection/registry.h>
void ExampleGlobalFunctionRegistry() {
namespace ffi = tvm::ffi;
ffi::reflection::GlobalDef().def("xyz.add1", [](const int a) -> int { return a + 1; });
ffi::Function fadd1 = ffi::Function::GetGlobalRequired("xyz.add1");
int b = fadd1(1).cast<int>();
// EXPECT_EQ is used here for demonstration purposes (testing framework)
EXPECT_EQ(b, 2);
}
```
You can also access and register global functions from the Python API.
### Exporting as Library Symbol
Besides the API that allows registration of functions into the global table,
we also provide a macro to export static functions as `TVMFFISafeCallType` symbols in a dynamic library.
```c++
void AddOne(DLTensor* x, DLTensor* y) {
// ... implementation omitted ...
}
TVM_FFI_DLL_EXPORT_TYPED_FUNC(add_one, my_ffi_extension::AddOne);
```
The new `add_one` takes the signature of `TVMFFISafeCallType`, and can loaded and queried through the C++ `ffi::Module` API.
When flag `TVM_FFI_DLL_EXPORT_TYPED_FUNC_METADATA` is on, the macro exports both the function and its type metadata, enabling
signature validation without calling the function.
The metadata contains:
- **type_schema**: JSON string describing function signature (return type and argument types)
```cpp
ffi::Module mod = ffi::Module::LoadFromFile("path/to/export_lib.so");
// Get the function
ffi::Function func = mod->GetFunction("add_one").value();
// Query metadata (type schema information)
ffi::Optional<ffi::String> metadata = mod->GetFunctionMetadata("add_one");
if (metadata.has_value()) {
// Parse JSON metadata for validation
// Contains: {"type_schema": "..."}
}
```
For functions that need documentation, use the `TVM_FFI_DLL_EXPORT_TYPED_FUNC_DOC` macro separately:
```cpp
#define TVM_FFI_DLL_EXPORT_INCLUDE_METADATA 1
void ProcessBatch(ffi::TensorView input, ffi::TensorView output) {
// ... implementation
}
// Export the function
TVM_FFI_DLL_EXPORT_TYPED_FUNC(process_batch, ProcessBatch);
// Export documentation separately (make sure TVM_FFI_DLL_EXPORT_INCLUDE_METADATA is set to 1)
TVM_FFI_DLL_EXPORT_TYPED_FUNC_DOC(
process_batch,
R"(Process a batch of inputs and write results to output tensor.
Parameters
----------
input : TensorView
Input tensor to process
output : TensorView
Output tensor for results)");
// Query documentation
ffi::Optional<ffi::String> doc = mod->GetFunctionDoc("process_batch");
```
## Error Handling
We provide a specific `ffi::Error` type that is also made compatible with the ffi ABI.
We also provide a macro `TVM_FFI_THROW` to simplify the error throwing step.
```cpp
// file: cpp/test_example.cc
#include <tvm/ffi/error.h>
void FuncThrowError() {
namespace ffi = tvm::ffi;
TVM_FFI_THROW(TypeError) << "test0";
}
void ExampleErrorHandling() {
namespace ffi = tvm::ffi;
try {
FuncThrowError();
} catch (const ffi::Error& e) {
EXPECT_EQ(e.kind(), "TypeError");
EXPECT_EQ(e.message(), "test0");
std::cout << e.TracebackMostRecentCallLast() << std::endl;
}
}
```
The structured error class records kind, message and backtrace that can be mapped to
Pythonic style error types and traces. The `TracebackMostRecentCallLast()` call reverses
the backtrace and print out follows the Python style,
tvm-ffi will try to preserve the backtrace when possible. In the above example,
you can see the output as
```text
... more lines omitted
File "cpp/test_example.cc", line 106, in ExampleErrorHandling
File "cpp/test_example.cc", line 100, in void FuncThrowError()
```
The ffi ABI provides minimal but sufficient mechanisms to propagate these errors across
language boundaries.
So when we call the function from Python, the Error will be translated into a corresponding
Error type. Similarly, when we call a Python callback from C++, the error will be translated
into the right error kind and message.
## Tensor
For many use cases, we do not need to manage the nd-array/Tensor memory.
In such cases, `DLTensor*` can be used as the function arguments.
There can be cases for a managed container for multi-dimensional arrays.
`ffi::Tensor` is a minimal container to provide such support.
Notably, specific logic of device allocations and array operations are non-goals
of the FFI. Instead, we provide minimal generic API `ffi::Tensor::FromNDAlloc`
to enable flexible customization of Tensor allocation.
```cpp
#include <tvm/ffi/container/tensor.h>
#include <tvm/ffi/container/shape.h>
struct CPUNDAlloc {
void AllocData(DLTensor* tensor) {
tensor->data = malloc(tvm::ffi::GetDataSize(*tensor));
}
void FreeData(DLTensor* tensor) { free(tensor->data); }
};
void ExampleTensor() {
namespace ffi = tvm::ffi;
ffi::Shape shape = {1, 2, 3};
DLDataType dtype = {kDLFloat, 32, 1};
DLDevice device = {kDLCPU, 0};
ffi::Tensor tensor = ffi::Tensor::FromNDAlloc(CPUNDAlloc(), shape, dtype, device);
// now tensor is a managed tensor
}
```
The above example shows how we define `CPUNDAlloc` that customizes `AllocData`
and `FreeData` behavior. The CPUNDAlloc struct will be kept alive with the Tensor object.
This pattern allows us to implement various Tensor allocations using the same API:
- For CUDA allocation, we can change malloc to cudaMalloc
- For memory-pool based allocation, we can update `CPUNDAlloc` to keep a strong reference to the pool,
so we can keep memory-pool alive when the array is alive.
**Working with Shapes** As you may have noticed in the example, we have a `ffi::Shape` container that is used
to represent the shapes in nd-array. This container allows us to have compact and efficient representation
of managed shapes and we provide quick conversions from standard vector types.
### DLPack Conversion
We provide first-class DLPack support to the `ffi::Tensor` that enables efficient exchange
through the DLPack Protocol.
```cpp
#include <tvm/ffi/container/tensor.h>
void ExampleTensorDLPack() {
namespace ffi = tvm::ffi;
ffi::Shape shape = {1, 2, 3};
DLDataType dtype = {kDLFloat, 32, 1};
DLDevice device = {kDLCPU, 0};
ffi::Tensor tensor = ffi::Tensor::FromNDAlloc(CPUNDAlloc(), shape, dtype, device);
// convert to DLManagedTensorVersioned
DLManagedTensorVersioned* dlpack = nd.ToDLPackVersioned();
// load back from DLManagedTensorVersioned
ffi::Tensor tensor2 = ffi::Tensor::FromDLPackVersioned(dlpack);
}
```
These APIs are also available through the C APIs
`TVMFFITensorFromDLPackVersioned` and `TVMFFITensorToDLPackVersioned`.
## String and Bytes
The tvm-ffi provides first-class support for `String` and `Bytes` types that are efficient,
FFI-compatible, and interoperable with standard C++ string types.
```cpp
#include <tvm/ffi/string.h>
void ExampleString() {
namespace ffi = tvm::ffi;
ffi::String str = "hello world";
// EXPECT_EQ is used here for demonstration purposes (testing framework)
EXPECT_EQ(str.size(), 11);
std::string std_str = str;
EXPECT_EQ(std_str, "hello world");
}
```
Alternatively, users can always directly use `std::string` in function arguments, conversion
will happen automatically.
**Rationale:** We need to have separate Bytes and String so they map well to corresponding Python types.
`ffi::String` is backed by a possibly managed object that makes it more compatible with the Object system.
## Container Types
To enable effective passing and storing of collections of values that are compatible with tvm-ffi,
we provide several built-in container types. See [Containers](../concepts/containers.rst) for a
conceptual overview.
| Type | Header | Mutability | Semantics |
| ------ | -------- | ------------ | ----------- |
| `Array<T>` | `container/array.h` | Immutable (copy-on-write) | Homogeneous sequence |
| `List<T>` | `container/list.h` | Mutable (shared reference) | Homogeneous sequence |
| `Tuple<Ts...>` | `container/tuple.h` | Immutable (copy-on-write) | Heterogeneous fixed-size sequence |
| `Map<K,V>` | `container/map.h` | Immutable (copy-on-write) | Homogeneous key-value mapping |
| `Dict<K,V>` | `container/dict.h` | Mutable (shared reference) | Homogeneous key-value mapping |
### Array
`Array<T>` provides an array data type that can be used as function arguments.
When we use `Array<T>` as an argument of a Function, it will
perform runtime checks of the elements to ensure the values match the expected type.
```cpp
#include <tvm/ffi/container/array.h>
void ExampleArray() {
namespace ffi = tvm::ffi;
ffi::Array<int> numbers = {1, 2, 3};
// EXPECT_EQ is used here for demonstration purposes (testing framework)
EXPECT_EQ(numbers.size(), 3);
EXPECT_EQ(numbers[0], 1);
ffi::Function head = ffi::Function::FromTyped([](const ffi::Array<int> a) {
return a[0];
});
EXPECT_EQ(head(numbers).cast<int>(), 1);
try {
// throw an error because 2.2 is not int
head(ffi::Array<ffi::Any>({1, 2.2}));
} catch (const ffi::Error& e) {
EXPECT_EQ(e.kind(), "TypeError");
}
}
```
Under the hood, Array is backed by a reference-counted Object `ArrayObj` that stores
a collection of Any values. Note that conversion from Any to `Array<T>` will result in
runtime checks of elements because the type index only indicates `ArrayObj` as the backing storage.
If you want to defer such checks at the FFI function boundary, consider using `Array<Any>` instead.
When passing lists and tuples from Python, the values will be converted to `Array<Any>` before
being passed into the Function.
**Performance note:** Repeatedly converting Any to `Array<T>` can incur repeated
checking overhead at each element. Consider using `Array<Any>` to defer checking or only run conversion once.
### List
`List<T>` provides a mutable sequence container with shared reference semantics.
Unlike `Array`, mutations happen directly on the underlying shared `ListObj` --
there is no copy-on-write. All handles sharing the same `ListObj` see mutations
immediately.
```cpp
#include <tvm/ffi/container/list.h>
void ExampleList() {
namespace ffi = tvm::ffi;
ffi::List<int> numbers = {1, 2, 3};
// EXPECT_EQ is used here for demonstration purposes (testing framework)
EXPECT_EQ(numbers.size(), 3);
EXPECT_EQ(numbers[0], 1);
// Mutate in-place
numbers.push_back(4);
EXPECT_EQ(numbers.size(), 4);
numbers.Set(0, 10);
EXPECT_EQ(numbers[0], 10);
// Shared reference semantics: both handles see the mutation
ffi::List<int> alias = numbers;
alias.push_back(5);
EXPECT_EQ(numbers.size(), 5);
}
```
Under the hood, `List` is backed by a reference-counted `ListObj` that stores
a collection of Any values. Like `Array`, conversion from Any to `List<T>` will result in
runtime checks of elements.
**When to use List vs Array:** Use `Array<T>` when you need an immutable snapshot
(e.g., passing a collection through FFI boundaries where the receiver should not
mutate). Use `List<T>` when you need to build up or modify a collection in place.
### Tuple
`Tuple<Types...>` provides type-safe fixed-size collections.
```cpp
#include <tvm/ffi/container/tuple.h>
void ExampleTuple() {
namespace ffi = tvm::ffi;
ffi::Tuple<int, ffi::String, bool> tup(42, "hello", true);
// EXPECT_EQ is used here for demonstration purposes (testing framework)
EXPECT_EQ(tup.get<0>(), 42);
EXPECT_EQ(tup.get<1>(), "hello");
EXPECT_EQ(tup.get<2>(), true);
}
```
Under the hood, Tuple is backed by the same `ArrayObj` as the Array container.
This enables zero-cost exchange with input arguments.
**Rationale:** This design unifies the conversion rules from Python list/tuple to
Array/Tuple. We always need a container representation for tuples
to be stored in Any.
### Map
`Map<K, V>` provides a key-value based hashmap container that can accept dict-style parameters.
```cpp
#include <tvm/ffi/container/map.h>
void ExampleMap() {
namespace ffi = tvm::ffi;
ffi::Map<ffi::String, int> map0 = {{"Alice", 100}, {"Bob", 95}};
// EXPECT_EQ is used here for demonstration purposes (testing framework)
EXPECT_EQ(map0.size(), 2);
EXPECT_EQ(map0.at("Alice"), 100);
EXPECT_EQ(map0.count("Alice"), 1);
}
```
Under the hood, Map is backed by a reference-counted Object `MapObj` that stores
a collection of Any values. The implementation provides a SmallMap variant that stores
values as an array and another variant that is based on a hashmap. The Map preserves insertion
order like Python dictionaries. Conversion from Any to `Map<K, V>` will result in
runtime checks of its elements because the type index only indicates `MapObj` as the backing storage.
If you want to defer such checks at the FFI function boundary, consider using `Map<Any, Any>` instead.
When passing dictionaries from Python, the values will be converted to `Map<Any, Any>` before
being passed into the Function.
**Performance note:** Repeatedly converting Any to `Map<K, V>` can incur repeated
checking overhead at each element. Consider using `Map<Any, Any>` to defer checking or only run conversion once.
### Dict
`Dict<K, V>` provides a mutable key-value mapping with shared reference semantics.
Unlike `Map`, mutations happen directly on the underlying shared `DictObj`.
All handles sharing the same `DictObj` see mutations immediately.
```cpp
#include <tvm/ffi/container/dict.h>
void ExampleDict() {
namespace ffi = tvm::ffi;
ffi::Dict<ffi::String, int> scores = {{"Alice", 100}, {"Bob", 95}};
// EXPECT_EQ is used here for demonstration purposes (testing framework)
EXPECT_EQ(scores.size(), 2);
EXPECT_EQ(scores.at("Alice"), 100);
// Mutate in-place
scores.Set("Charlie", 88);
EXPECT_EQ(scores.size(), 3);
// Shared reference semantics: both handles see the mutation
ffi::Dict<ffi::String, int> alias = scores;
alias.Set("Dave", 92);
EXPECT_EQ(scores.size(), 4);
// Erase
scores.erase("Bob");
EXPECT_EQ(scores.size(), 3);
}
```
Under the hood, `Dict` is backed by a reference-counted `DictObj` that stores
a collection of Any key-value pairs. Like `Map`, the `Dict` preserves insertion order.
**When to use Dict vs Map:** Use `Map<K, V>` when you need an immutable snapshot
with copy-on-write semantics. Use `Dict<K, V>` when you need mutable in-place operations.
### Optional
`Optional<T>` provides a safe way to handle values that may or may not exist.
We specialize Optional for `ffi::String` and Object types to be more compact,
using nullptr to indicate non-existence.
```cpp
#include <tvm/ffi/container/optional.h>
void ExampleOptional() {
namespace ffi = tvm::ffi;
ffi::Optional<int> opt0 = 100;
// EXPECT_EQ is used here for demonstration purposes (testing framework)
EXPECT_EQ(opt0.has_value(), true);
EXPECT_EQ(opt0.value(), 100);
ffi::Optional<ffi::String> opt1;
EXPECT_EQ(opt1.has_value(), false);
EXPECT_EQ(opt1.value_or("default"), "default");
}
```
### Variant
`Variant<Types...>` provides a type-safe union of different types.
```cpp
#include <tvm/ffi/container/variant.h>
void ExampleVariant() {
namespace ffi = tvm::ffi;
ffi::Variant<int, ffi::String> var0 = 100;
// EXPECT_EQ is used here for demonstration purposes (testing framework)
EXPECT_EQ(var0.get<int>(), 100);
var0 = ffi::String("hello");
std::optional<ffi::String> maybe_str = var0.as<ffi::String>();
EXPECT_EQ(maybe_str.value(), "hello");
std::optional<int> maybe_int2 = var0.as<int>();
EXPECT_EQ(maybe_int2.has_value(), false);
}
```
Under the hood, Variant is a wrapper around Any that restricts the type to the specific types in the list.