Callbacks & Traits
Target language code can flow into Rust through closures and traits. Pass a closure and the target language provides a lambda. Define a trait and the target language implements it as a protocol or interface. Both enable patterns like event handling, progress reporting, data providers, and transformation functions. BoltFFI generates the appropriate bindings for each form, handling the FFI boundary so implementations on either side can call each other safely.
Closures
Use closures when you need a single callback function. Accept impl Fn, impl FnMut, or impl FnOnce as a parameter, and BoltFFI generates a function type in the target language. The caller passes a lambda or closure, and Rust invokes it during execution. This works for iteration, mapping, filtering, and simple event handlers. Closures work in both standalone functions and class methods.
Return Values
Closures can return values. The return type becomes part of the generated function signature.
Traits
Use traits when you need multiple related callback methods. Define a trait with #[export] and BoltFFI generates a protocol or interface in the target language. The target language implements this protocol, and Rust receives an object that can call any of its methods. This pattern works well for progress handlers, event listeners, and delegate protocols. Like closures, trait callbacks work in both standalone functions and class methods.
Ownership
Trait callbacks can be passed as Box<dyn Trait> or Arc<dyn Trait>. Use Box when the callback is used once or owned by a single call. Use Arc when the callback needs to be shared across multiple calls or stored for later use. The choice affects how the target language object is retained.
Async Methods
Trait methods can be async. Mark the trait with #[async_trait] and use async fn for methods that need to perform async work on the target language side. When Rust calls an async method, it awaits the result before continuing.
Storing Traits
Trait objects can be stored in class fields for later use. The target language object remains alive as long as the Rust class holds the reference.
Thread Safety
For traits that will be called from multiple threads, add Send + Sync bounds. This ensures the target language implementation can be safely shared across threads.
#[export]
pub trait ThreadSafeLogger: Send + Sync {
fn log(&self, message: &str);
}
Limitations
Trait callbacks have some restrictions:
- Generic traits are not supported
- Associated types are not supported
- Default method implementations are ignored
How It Works
Closures are passed as function pointers with an associated context. When the target language calls a function that accepts a closure, it packages the lambda and any captured state into a handle. Rust receives this handle and invokes the closure through a generated wrapper that unpacks arguments, calls the target language function, and returns the result.
Trait callbacks use a vtable mechanism. When the target language passes a callback object, BoltFFI creates a handle that pairs the object reference with a vtable of function pointers. Each method in the trait has a corresponding entry in the vtable. When Rust calls a method, it invokes the function pointer with the handle, which dispatches to the target language implementation. The handle tracks ownership so the object stays alive as long as Rust holds a reference.