Rust Guide#
Note
The Rust support is currently in an experimental stage.
This guide demonstrates how to use TVM FFI from Rust applications.
Installation#
Prerequisites#
The Rust support depends on libtvm_ffi. First, install the tvm-ffi Python package:
pip install -v -e .
Confirm that tvm-ffi-config is available:
tvm-ffi-config --libdir
Adding to Your Project#
Add to your Cargo.toml:
[dependencies]
tvm-ffi = { path = "path/to/tvm-ffi/rust/tvm-ffi" }
For published versions (when available):
[dependencies]
tvm-ffi = "0.1.0-alpha.0"
Environment Setup#
Set the library path so libtvm_ffi can be found at runtime:
export LD_LIBRARY_PATH=$(tvm-ffi-config --libdir):$LD_LIBRARY_PATH
Basic Usage#
Named managed reference. A named managed reference follows the newtype pattern around
ObjectArc<T>, which manages the underlying object’s lifetime through reference counting.
Define methods and associated functions on the named type, and implement traits to provide
shared interfaces. The named type can also impose additional constraints on the underlying object.
For example, Array<T> holds an ObjectArc<ArrayObj> while constraining the array’s
element type to T. Named reference types implement ObjectRefCore for shared reference operations.
Loading a Module#
Load a compiled TVM FFI module and call its functions:
use tvm_ffi::{Module, Result};
fn main() -> Result<()> {
// Load compiled module
let module = Module::load_from_file("build/add_one_cpu.so")?;
// Get function by name
let add_fn = module.get_function("add_one_cpu")?;
Ok(())
}
Working with Tensors#
Create and manipulate tensors:
use tvm_ffi::Tensor;
// Create a tensor from a slice
let data = vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
let tensor = Tensor::from_slice(&data, &[2, 3])?;
Calling Functions#
Call functions with tensors:
use tvm_ffi::{Module, Tensor, Result};
fn run_example() -> Result<()> {
let module = Module::load_from_file("build/add_one_cpu.so")?;
let func = module.get_function("add_one_cpu")?;
// Create input and output tensors
let input = Tensor::from_slice(&[1.0f32, 2.0, 3.0, 4.0], &[4])?;
let output = Tensor::from_slice(&[0.0f32; 4], &[4])?;
// Call function
func.call_tuple((&input, &output))?;
Ok(())
}
Advanced Topics#
Global Functions#
Register and access global functions:
use tvm_ffi::Function;
// Get global function
let func = Function::get_global("my_function")?;
// Register a new global function
let my_func = Function::from_packed(|args: &[AnyView]| -> Result<Any> {
// Function implementation
Ok(Any::default())
});
Function::register_global("my_custom_func", my_func)?;
Reflected Type Methods#
Libraries that register their API through the C++ reflection registry
(refl::ObjectDef<T>().def(...)) store methods in a per-type method table
rather than the global function table. Resolve them by type key (or type
index) and method name; constructors registered via refl::init are
reachable under the reserved name __ffi_init__:
use tvm_ffi::{AnyView, Function};
// Resolve the reflected constructor and construct an instance
let ctor = Function::from_type_key_method("testing.TestIntPair", "__ffi_init__")?;
let pair = ctor.call_tuple((1i64, 2i64))?;
// Resolve an instance method; the first packed argument is the object itself
let sum = Function::from_type_key_method("testing.TestIntPair", "sum")?;
let result = sum.call_packed(&[AnyView::from(&pair)])?;
assert_eq!(i64::try_from(result)?, 3);
Function::from_type_method(type_index, name) performs the same lookup when
the type index is already known (e.g. from Any::type_index).
Converting Borrowed Values into Any#
Any::from(value) takes ownership of value. Use it when you own the value.
When you only have a shared reference—for example, a field accessed through
&AddObj—you cannot move the field into an Any. Call
AnyCompatible::to_any() instead:
use tvm_ffi::{Any, AnyCompatible};
fn first_operand(node: &AddObj) -> Any {
node.a.to_any()
}
to_any() creates an owned Any while leaving the borrowed value usable. For
object-backed values, it retains the object by incrementing its reference
count. It is equivalent to Any::from(node.a.clone()), without requiring an
explicit clone at the call site.
Type-Erased Functions#
Create functions from Rust closures:
use tvm_ffi::{Function, Any, AnyView, Result};
// From packed closure
let func = Function::from_packed(|args: &[AnyView]| -> Result<Any> {
// Process args and return result
Ok(Any::default())
});
// From typed closure
let typed_func = Function::from_typed(|x: i64, y: i64| -> Result<i64> {
Ok(x + y)
});
Error Handling#
TVM FFI uses standard Rust Result types:
use tvm_ffi::{Error, Module, Result, VALUE_ERROR};
fn may_fail(value: i32) -> Result<()> {
// Operations that may fail
let module = Module::load_from_file("path.so")?;
// Custom errors
if value < 0 {
return Err(Error::new(
VALUE_ERROR,
"Value must be non-negative",
""
));
}
Ok(())
}
Structural Walk and Visit#
Rust provides equivalents of the C++ StructuralWalk/StructuralVisitor
APIs. Put #[dispatch(walk)] on an impl to turn its walk_* methods into
typed handlers, then pass it to structural_walk; each handler returns a
WalkResult (Advance, Skip, or Interrupt) to steer the traversal.
Handlers dispatch on their argument type and may take an optional trailing
DefRegionKind argument:
use tvm_ffi::{dispatch, structural_walk, Array, DefRegionKind, WalkOrder, WalkResult};
#[derive(Default)]
struct Probe {
total: i64,
floats: usize,
}
#[dispatch(walk)]
impl Probe {
fn walk_integer(&mut self, value: i64) -> WalkResult {
self.total += value;
WalkResult::Advance
}
fn walk_float(&mut self, _value: f64, _kind: DefRegionKind) -> WalkResult {
self.floats += 1;
WalkResult::Advance
}
}
let values = Array::new(vec![1_i64, 2, 3]);
let mut probe = Probe::default();
structural_walk(&values, &mut probe, WalkOrder::PreOrder)?;
assert_eq!(probe.total, 6);
For Map and Dict, structural walk treats keys as structural anchors: it
visits container values but does not pass keys to handlers. The map or dict
object itself is still visited normally.
Lambdas also work — pass a single typed lambda, or a tuple of them tried in
order with the first matching argument type winning, like the variadic C++
StructuralWalk(root, callbacks...) chain. A flat tuple holds up to 12
lambdas; a tuple is itself a link, so nest (a, b, (c, d, ...)) to chain
more — order stays the flattened left-to-right order. Unmatched values
simply advance; a &StructuralView lambda acts as a catch-all and must come
last, since links after an always-matching one never run. Each lambda may
take a trailing DefRegionKind argument:
use tvm_ffi::{structural_walk, Array, DefRegionKind, Object, WalkOrder, WalkResult};
let values = Array::new(vec![1_i64, 2, 3]);
let mut total = 0;
structural_walk(
&values,
|value: i64| {
total += value;
WalkResult::Advance
},
WalkOrder::PreOrder,
)?;
assert_eq!(total, 6);
let mut evens = 0;
let mut objects = 0;
structural_walk(
&values,
(
|value: i64| {
if value % 2 == 0 {
evens += 1;
}
WalkResult::Advance
},
|_object: &Object, _kind: DefRegionKind| {
objects += 1;
WalkResult::Advance
},
),
WalkOrder::PreOrder,
)?;
assert_eq!((evens, objects), (1, 1));
Both entry points return Result<Option<VisitInterrupt>>: Ok(None) means
the whole graph was visited, and a handler stops the walk early by returning
WalkResult::interrupt_with(payload), which comes back to the caller as
Ok(Some(interrupt)). Handlers may also return Result<WalkResult> and
propagate errors with ?:
use tvm_ffi::{structural_walk, Array, WalkOrder, WalkResult};
let values = Array::new(vec![1_i64, 2, 3]);
let found = structural_walk(
&values,
|value: i64| {
if value == 2 {
return WalkResult::interrupt_with(value);
}
WalkResult::Advance
},
WalkOrder::PreOrder,
)?;
assert_eq!(found.map(|i| i64::try_from(i.value).unwrap()), Some(2));
structural_visit accepts typed callbacks in addition to a StructuralVisitor.
Callbacks receive VisitContext, use first-match dispatch, and own traversal
of matched values. VisitCallbacks adds state shared by the callback chain:
use tvm_ffi::{structural_visit, Array, VisitCallbacks, VisitContext, StructuralView};
#[derive(Default)]
struct Stats {
total: i64,
}
let values = Array::new(vec![1_i64, 2]);
let mut visitor = VisitCallbacks::new(
Stats::default(),
(
|value: i64, visitor: &mut VisitContext<'_, Stats>| {
visitor.state_mut().total += value;
},
|_value: &StructuralView, visitor: &mut VisitContext<'_, Stats>| {
visitor.visit_children()
},
),
);
structural_visit(&values, &mut visitor)?;
assert_eq!(visitor.state().total, 3);
Callbacks are Fn; mutable data belongs in the visitor state. A catch-all
callback must call visit_children() explicitly, and interrupt values must be
returned explicitly because ? only propagates errors.
VisitCallbacks::with_policy and WalkWithContextPolicy use ContextPolicy<State>
to manage context around default recursion. Pass WalkWithContextPolicy to
structural_walk; pass &mut walker to reuse it and inspect its state afterward.
See the ContextPolicy API documentation for composition and shared state access.
For a named implementation, #[dispatch(visit)] generates
StructuralVisitor from visit_* methods. Matching handlers own recursion;
unmatched values use default child traversal:
use tvm_ffi::{
dispatch, structural_visit, Array, DefRegionKind, Result, StructuralVisitor, VisitInterrupt,
StructuralView,
};
#[derive(Default)]
struct Depth {
max: usize,
current: usize,
}
#[dispatch(visit)]
impl Depth {
fn visit_any(
&mut self,
value: &StructuralView,
def_region_kind: DefRegionKind,
) -> Result<Option<VisitInterrupt>> {
self.current += 1;
self.max = self.max.max(self.current);
let interrupt = self.default_visit_children(value, def_region_kind)?;
self.current -= 1;
Ok(interrupt)
}
}
let values = Array::new(vec![1_i64, 2]);
let mut depth = Depth::default();
structural_visit(&values, &mut depth)?;
assert_eq!(depth.max, 2);
Implement StructuralVisitor directly to override its low-level visit
method.
Use #[dispatch(visit, policy = MyPolicy)] to apply a ContextPolicy<Self>
to default recursion.
Two safety notes: mutable List/Dict contents are snapshotted before
callbacks run, so mutation during traversal cannot invalidate the walk; and
a non-container type with a foreign __s_visit__ hook is rejected rather
than silently walked through reflection — visit such a type’s children
explicitly from a StructuralVisitor, or skip it with a pre-order
WalkResult::Skip.
StructuralView is the shared borrowed callback value for visit, walk, map,
and mutate. Mapping callbacks return replacement values; their input view is
borrowed and does not grant in-place mutation permission.
Structural Mapping and Mutation#
structural_map is the transforming counterpart to structural_walk. Put
#[dispatch(map)] on an impl whose map_* methods return any value convertible
into Any, directly or in Result. Methods are tested in source order, the
first matching argument type wins, and an unmatched value is preserved. A
method may take an optional trailing DefRegionKind; a &StructuralView method is a
catch-all and should therefore come last:
use tvm_ffi::{
dispatch, structural_map, Any, Array, DefRegionKind, StructuralView, Result, WalkOrder,
};
#[derive(Default)]
struct Increment {
integers: usize,
}
#[dispatch(map)]
impl Increment {
fn map_integer(&mut self, value: i64, _kind: DefRegionKind) -> Result<i64> {
self.integers += 1;
Ok(value + 1)
}
fn map_other(&mut self, value: &StructuralView) -> Any {
value.to_owned()
}
}
let mut increment = Increment::default();
let mapped = structural_map(
Array::new(vec![1_i64, 2]),
&mut increment,
WalkOrder::PostOrder,
)?;
let mapped = Array::<i64>::try_from(mapped)?;
assert_eq!(mapped.iter().collect::<Vec<_>>(), vec![2, 3]);
A single typed closure and an ordered tuple of up to twelve closures are also
accepted; a tuple is itself a link, so (a, b, (c, d, ...)) nests beyond that
without changing the flattened order. Tuple dispatch is first-match, not broadcast: later closures do not
run after an earlier argument type matches. As with generated dispatch, a
&StructuralView catch-all belongs last. Numeric handlers claim the complete FFI
Int or Float tag and then use Rust as conversion semantics; prefer i64
or f64 unless narrowing is deliberate.
WalkOrder::PreOrder runs the callback before default child mapping. If it
returns a different root, that replacement root is not passed to the callback
again, but its children are still mapped. WalkOrder::PostOrder maps children
first and passes the resulting value to the callback; a replacement returned
there is final. Array and List elements are mapped in order. Map and
Dict keys are identity anchors and are never mapped; only their values are.
The root is consumed. A uniquely owned built-in container may reuse its
storage in place; passing root.clone() keeps the source shared and selects
copy-on-write behavior. The engine rechecks uniqueness after a pre-order
callback, so retaining an owning StructuralView::to_owned() alias before the
container’s children are mapped forces the non-in-place path. Reflected
objects must provide __ffi_shallow_copy__; the copy is validated before
fields are mapped and discarded if no structural field changes.
Within one structural_map call, callbacks run at every occurrence; their
results are not cached. Default recursion manages identity remapping with
the same semantics as C++.
MutateCallbacks::with_policy and MapWithContextPolicy use MutContextPolicy<State>
to manage context around default recursion. Policies consume MutateValue
and return UnchangedOr<Any>; see the API documentation for composition and
scoped definition regions. Pass MapWithContextPolicy directly to structural_map,
or pass &mut mapper to reuse it and inspect its state afterward;
it cannot be a callback tuple member or another wrapper’s dispatcher. Compose
policies as (outer, inner) within one wrapper.
Default recursion uses the same type attributes as C++: it calls
__s_maybe_inplace_mutate__ for a uniquely owned object when available, or
falls back to __s_mutate__. Each hook receives the active Rust-backed
mutator and can recurse through its language-independent vtable. This lets the
implementation that registered the type own its storage and mutation rules.
When a type has no hook, object-backed values use reflected fields.
Callbacks may return Result<T> for any T convertible into Any; for
example, an integer handler can return Result<i64> to report failures and use
?. Errors propagate with object or reflected-field context. In-place changes
completed before a later error are not rolled back, and the consumed root is
not returned on error.
Callbacks can also return Unchanged or UnchangedOr<T>, optionally wrapped
in Result. A pre-order map still maps the original value’s children when
a callback returns Unchanged; a post-order map keeps the result of child
mapping. Use mutate_result and
default_mutate_result to preserve unchanged during recursion; existing
owning-value helpers and top-level functions resolve the marker to the
original value.
structural_mutate accepts typed callback chains in addition to a
StructuralMutator. Closure callbacks receive a MutateContext;
MutateCallbacks adds state shared by that callback chain:
use tvm_ffi::{
structural_mutate, Array, MutateCallbacks, MutateContext, MutateValue,
};
#[derive(Default)]
struct Stats {
integers: usize,
}
let mut mutator = MutateCallbacks::new(
Stats::default(),
(
|value: i64, mutator: &mut MutateContext<'_, Stats>| {
mutator.state_mut().integers += 1;
value + 1
},
|value: MutateValue<'_>, mutator: &mut MutateContext<'_, Stats>| {
mutator.default_maybe_inplace_mutate(value)
},
),
);
let mutated = structural_mutate(Array::new(vec![1_i64, 2]), &mut mutator)?;
let mutated = Array::<i64>::try_from(mutated)?;
assert_eq!(mutated.iter().collect::<Vec<_>>(), vec![2, 3]);
assert_eq!(mutator.state().integers, 2);
MutateContext::mutate uses the copy path for a borrowed value, while
maybe_inplace_mutate preserves the reuse opportunity of an owned value.
Closure callbacks are Fn; mutable data belongs in the callback state.
Use MutateValue<'_, T> with default_maybe_inplace_mutate to forward its
permission, or default_mutate_with_mode to restrict it. Borrow through value; mutation
contexts do not expose current(), and copy-only default descent takes an
explicit borrow (default_mutate(value)). Generated handlers can read the mode
through Mutator::inplace_mode(); see MutateValue for ownership requirements.
#[dispatch(mutate)] groups typed mutate_* callbacks. The dispatch object
owns its mutable pass state, while Mutator supplies recursion and the current
definition region. mutator.mutate(self, child) safely reborrows that dispatch
object and inherits the current region; mutate_with is available for an
explicit override. An unmatched value follows default mutation with its
current in-place permit. A handler that does not need these controls can omit
the &mut Mutator parameter:
use tvm_ffi::{dispatch, structural_mutate, Array};
#[derive(Default)]
struct Increment {
integers: usize,
}
#[dispatch(mutate)]
impl Increment {
fn mutate_integer(&mut self, value: i64) -> i64 {
self.integers += 1;
value + 1
}
}
let mut increment = Increment::default();
let mutated = structural_mutate(
Array::new(vec![1_i64, 2]),
&mut increment,
)?;
let mutated = Array::<i64>::try_from(mutated)?;
assert_eq!(mutated.iter().collect::<Vec<_>>(), vec![2, 3]);
assert_eq!(increment.integers, 2);
Use #[dispatch(mutate, policy = MyPolicy)] to apply a MutContextPolicy<Self>
to default recursion.
For low-level custom recursion, implement StructuralMutator and pass
&mut it to structural_mutate. InplaceValue is an engine-issued
capability: callers cannot construct it from a read-only StructuralView. Override
dispatch_maybe_inplace_mutate to opt into default container reuse;
default_maybe_inplace_mutate rechecks uniqueness before writing. Borrowed
values can be re-entered with mutate, while owned values can use
maybe_inplace_mutate:
use tvm_ffi::{
structural_mutate, Any, Array, DefRegionKind, InplaceValue, StructuralView, Result,
StructuralMutator,
};
#[derive(Default)]
struct Increment;
impl StructuralMutator for Increment {
fn dispatch_mutate(&mut self, value: &StructuralView, kind: DefRegionKind) -> Result<Any> {
match value.cast::<i64>() {
Some(value) => Ok(Any::from(value + 1)),
None => self.default_mutate(value, kind),
}
}
fn dispatch_maybe_inplace_mutate(
&mut self,
value: InplaceValue<'_>,
kind: DefRegionKind,
) -> Result<Any> {
self.default_maybe_inplace_mutate(value, kind)
}
}
let mutated = structural_mutate(
Array::new(vec![1_i64, 2]),
&mut Increment::default(),
)?;
let mutated = Array::<i64>::try_from(mutated)?;
assert_eq!(mutated.iter().collect::<Vec<_>>(), vec![2, 3]);
The default var_remap_get and var_remap_set methods use state local to one
structural_mutate invocation, preserving completed FreeVar and DAGNode
substitutions without adding a field to the mutator. Override them and use
StructuralVarRemap when a custom identity policy is required.
Examples#
The repository includes a complete example in rust/tvm-ffi/examples/load_library.rs.
Run it with:
cd rust
cargo run --example load_library --features example
Building the Workspace#
Build the entire Rust workspace:
cd rust
cargo build
Run tests:
cargo test
API Reference#
For detailed API documentation, see the Rust API Reference.