Functions
This page covers free functions - standalone functions that aren’t attached to a struct or class. For methods on objects, see Classes.
Mark a Rust function with #[export] and BoltFFI generates a corresponding function in each target language. The function signature, parameter types, and return type all map according to the rules in Types.
Function names may be renamed to match target language conventions. For example, get_user becomes getUser in languages that use camelCase.
Basic export
The simplest case: a function that takes primitives and returns a primitive.
Parameters
Primitives and strings
Primitive parameters pass directly with no overhead. Strings require copying since each language manages its own memory.
Use &str for string parameters (you’re borrowing) and String for return values (you’re transferring ownership).
Structs and enums
Functions can accept structs and enums marked with #[data]. The data and all its fields move across the boundary.
Slices
Use &[T] to accept a collection without taking ownership. The caller’s array or list is accessible as a slice inside Rust.
Optional
Use Option<T> when a parameter might not be provided. The caller passes nil/null or a value.
Return types
Option
Return Option<T> when a value might not exist. The caller gets a nullable type they can check before using.
Result
Return Result<T, E> when an operation can fail. The error type must be marked with #[error]. The generated function throws in the target language.
Use Option when absence is expected. Use Result when absence is an error.
Vec
Return Vec<T> when you’re producing a collection. Each element moves across the boundary.
Async functions
Mark a function async and BoltFFI generates an async function in the target language. Swift gets async, Kotlin gets suspend.
The async runtime is handled by BoltFFI. Your Rust async code runs on a Tokio runtime that BoltFFI manages.
Closures
Functions can accept closures as parameters. The closure is called synchronously within the Rust function.
Each closure call crosses the FFI boundary. If you’re calling the closure many times in a tight loop, consider restructuring to reduce crossings.
Limitations
-
Generic functions like
fn max<T: Ord>(a: T, b: T) -> Tare not supported. Create concrete versions for each type you need. -
Functions cannot return references. Return owned data instead.
-
Closures that outlive the function call (stored for later use) are not supported. The closure must be called within the function body.
// Not supported - generic
#[export]
pub fn max<T: Ord>(a: T, b: T) -> T { ... }
// Supported - concrete versions
#[export]
pub fn max_i32(a: i32, b: i32) -> i32 {
if a > b { a } else { b }
}
#[export]
pub fn max_f64(a: f64, b: f64) -> f64 {
if a > b { a } else { b }
}