Classes
BoltFFI has two ways to expose a struct: as data or as a class.
Data (#[data]) is for plain values. A Point { x, y } or a User { id, name, email }. Data is copied when it crosses the boundary. The target language gets a struct or record with public fields. No behavior, no methods, just data.
Classes (#[export] impl) are for objects with behavior. A DatabaseConnection or a HttpClient. The object lives in Rust, the target language holds a reference to it. Methods operate on that reference. The object is not copied - there’s one instance, and both sides point to it.
Use data when you’re passing values around. Use classes when you’re managing state or resources.
Defining a class
Put #[export] on the impl block, not the struct. The struct stays private; only the methods you define in the impl block are exposed.
Constructors
Methods that return Self become constructors. How they appear in the target language depends on the method name and parameters.
The new() method
A method named new() becomes the primary constructor.
Named constructors with parameters
Methods with parameters that return Self become additional constructors. In Swift, they become convenience init. In Kotlin, they go in the companion object.
Factory methods (no parameters)
Methods with no parameters and a name other than new become factory methods.
Fallible constructors
Constructors can return Result<Self, E>. The error type must be marked with #[error]. The constructor becomes throwing in the target language.
Methods
Methods take &self (read-only access) or &mut self (mutable access).
&self vs &mut self
&self takes a shared reference. &mut self takes an exclusive reference.
BoltFFI does not provide automatic locking. If your class has &mut self methods and you call them from multiple threads, you must handle synchronization yourself. The simplest approach: use &self for all methods and handle mutability internally with atomics, Mutex, or other synchronization primitives. See Thread safety for more.
Static methods
Methods without self become static methods on the class.
Async methods
Mark a method async and it becomes an async method in the target language. BoltFFI has no built-in executor. You choose your Rust async runtime (Tokio, async-std, etc.), and the target language’s async system coordinates with it automatically. See Async for more.
Methods that take or return classes
Methods can accept or return other class instances.
Skipping methods
Use #[skip] to exclude a method from FFI export. The method stays in Rust but isn’t exposed to the target language.
#[export]
impl MyClass {
pub fn exported(&self) -> i32 {
self.helper() * 2
}
#[skip]
pub fn helper(&self) -> i32 {
42
}
}
Thread safety
BoltFFI requires exported classes to be Send + Sync by default. This is a compile-time check. If your struct isn’t thread-safe, compilation fails.
If your struct contains types that aren’t thread-safe (like RefCell, Rc, or raw mutable state), you have two options:
-
Make it thread-safe using
Mutex,RwLock, atomics, or other synchronization primitives. -
Opt out with
#[boltffi::export(thread_unsafe)]. This disables the check, but you’re responsible for ensuring the class is only used from a single thread.
Opting out
If you guarantee single-threaded access, you can disable the thread safety check:
pub struct NotThreadSafe {
data: RefCell<Vec<String>>,
}
#[boltffi::export(thread_unsafe)]
impl NotThreadSafe {
// ...
}
This compiles, but calling methods from multiple threads is undefined behavior.
The key insight: &self methods don’t lock at the FFI boundary. The Rust code runs concurrently. You’re responsible for making the Rust implementation thread-safe.
Memory management
The Rust struct lives in Rust’s heap. The target language holds a reference to it. When the target language’s object is deallocated (garbage collected, reference count hits zero, etc.), BoltFFI drops the Rust struct.
You don’t need to manually free anything. But be aware: the Rust object stays alive as long as the target language holds a reference. If you store a class instance in a long-lived collection, the Rust memory stays allocated.
When to use classes vs data structs
Use #[data] structs when:
- The data is small and copied frequently
- There’s no behavior, just fields
- You’re passing data in and out of functions
Use #[export] classes when:
- The object has internal state that changes over time
- The object manages resources (files, connections, memory)
- You want to hide implementation details behind methods
- The object is expensive to copy
A Point { x, y } is data. A DatabaseConnection is a class.