Use this page with an LLM
Functions
C examples use the package name demo and include demo.h. See C memory management for borrowed inputs and returned owners.
This page covers free functions - standalone functions that aren’t attached to a struct or class. For methods on objects, see Classes. For standalone values, see Constants.
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 and GetUser in C#.
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. C string inputs use borrowed views; returned strings own a separate allocation that the caller releases.
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.
Default arguments
Put #[boltffi::default(...)] on a parameter to give it a default. Kotlin renders it as a default argument, so callers may leave the parameter out or pass the ones they need by name. It takes the same values as record field defaults, and applies to free functions, methods, constructors and factories alike. None is accepted on Option<T> parameters only.
#[export]
pub fn connect(
host: String,
#[boltffi::default(8080)] port: u16,
#[boltffi::default(None)] on_event: Option<Box<dyn EventListener>>,
) -> Connection { /* ... */ }
fun connect(host: String, port: UShort = 8080.toUShort(), onEvent: EventListener? = null): Connection
connect("localhost")
connect("localhost", onEvent = listener)
The other targets still take every argument, so pass the default value explicitly there. What crosses the FFI boundary is the same either way.
Classes
Functions can accept class instances as parameters. The class reference passes across the boundary without copying the object.
Callback traits
Functions can accept callback traits. A callback trait is a Rust trait marked with #[export] that the target language implements. Unlike closures (single anonymous function), callback traits can have multiple methods and carry state. C supports synchronous callback traits with the C vtable contract; inline closures are not generated for C.
See Callbacks for more.
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
C does not generate async functions yet. The following examples apply to the other targets.
Mark a function async and BoltFFI generates an async function in the target language:
- Swift:
async - Kotlin:
suspend - Java:
CompletableFutureon Java 8+, with virtual-thread blocking calls on Java 21+ - C#:
Task - TypeScript:
Promise
BoltFFI has no built-in executor. You choose your Rust async runtime (Tokio, async-std, smol, etc.), and BoltFFI bridges the async boundary between Rust and the target language. See Async for more.
Closures
C does not generate inline closure parameters yet. Synchronous callback traits have a C vtable API.
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 }
}