Tutorial
This tutorial walks through building a counter library in Rust and using it from Swift and Kotlin.
Create the Rust library
cargo new --lib counter
cd counter
Add BoltFFI to Cargo.toml:
[package]
name = "counter"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["staticlib", "cdylib"]
[dependencies]
boltffi = "0.1"
Write the Rust code
Create src/lib.rs:
use boltffi::*;
use std::sync::atomic::{AtomicI64, Ordering};
pub struct Counter {
value: AtomicI64,
}
#[export]
impl Counter {
pub fn new(initial: i64) -> Self {
Counter {
value: AtomicI64::new(initial),
}
}
pub fn increment(&self) -> i64 {
self.value.fetch_add(1, Ordering::SeqCst) + 1
}
pub fn decrement(&self) -> i64 {
self.value.fetch_sub(1, Ordering::SeqCst) - 1
}
pub fn get(&self) -> i64 {
self.value.load(Ordering::SeqCst)
}
pub fn reset(&self) {
self.value.store(0, Ordering::SeqCst);
}
}
The #[export] attribute tells BoltFFI to generate bindings for Counter. The struct uses AtomicI64 for thread-safe access without locks.
Generate bindings
For Apple (Swift):
boltffi pack apple
For Android (Kotlin):
boltffi pack android
This produces a framework you can import directly.
Use from Swift
import Counter
let counter = Counter(initial: 0)
print(counter.increment()) // 1
print(counter.increment()) // 2
print(counter.get()) // 2
print(counter.decrement()) // 1
counter.reset()
print(counter.get()) // 0
Use from Kotlin
import com.example.counter.Counter
val counter = Counter(initial = 0)
println(counter.increment()) // 1
println(counter.increment()) // 2
println(counter.get()) // 2
println(counter.decrement()) // 1
counter.reset()
println(counter.get()) // 0
Adding error handling
Extend the counter to reject negative values:
use boltffi::*;
use std::sync::atomic::{AtomicI64, Ordering};
#[error]
pub struct CounterError {
pub message: String,
}
pub struct Counter {
value: AtomicI64,
min: i64,
}
#[export]
impl Counter {
pub fn new(initial: i64, min: i64) -> Result<Self, CounterError> {
if initial < min {
return Err(CounterError {
message: format!("initial {} below minimum {}", initial, min),
});
}
Ok(Counter {
value: AtomicI64::new(initial),
min,
})
}
pub fn decrement(&self) -> Result<i64, CounterError> {
let current = self.value.load(Ordering::SeqCst);
if current <= self.min {
return Err(CounterError {
message: format!("cannot go below {}", self.min),
});
}
Ok(self.value.fetch_sub(1, Ordering::SeqCst) - 1)
}
}
In Swift, the error becomes a thrown exception:
do {
let counter = try Counter(initial: 0, min: 0)
try counter.decrement() // throws CounterError
} catch let error as CounterError {
print(error.message)
}
In Kotlin:
try {
val counter = Counter(initial = 0, min = 0)
counter.decrement() // throws CounterError
} catch (e: CounterError) {
println(e.message)
}
Adding async
Make the counter persist to a file asynchronously:
use boltffi::*;
use std::sync::atomic::{AtomicI64, Ordering};
use std::path::PathBuf;
pub struct PersistentCounter {
value: AtomicI64,
path: PathBuf,
}
#[export]
impl PersistentCounter {
pub fn new(path: String) -> Self {
PersistentCounter {
value: AtomicI64::new(0),
path: PathBuf::from(path),
}
}
pub fn increment(&self) -> i64 {
self.value.fetch_add(1, Ordering::SeqCst) + 1
}
pub async fn save(&self) -> Result<(), String> {
let value = self.value.load(Ordering::SeqCst);
tokio::fs::write(&self.path, value.to_string().as_bytes())
.await
.map_err(|e| e.to_string())
}
pub async fn load(&self) -> Result<i64, String> {
let contents = tokio::fs::read_to_string(&self.path)
.await
.map_err(|e| e.to_string())?;
let value: i64 = contents.trim().parse().map_err(|e: std::num::ParseIntError| e.to_string())?;
self.value.store(value, Ordering::SeqCst);
Ok(value)
}
}
In Swift, async methods use structured concurrency:
let counter = PersistentCounter(path: "/tmp/counter.txt")
counter.increment()
counter.increment()
try await counter.save()
let loaded = try await counter.load()
print(loaded) // 2
In Kotlin, they become suspend functions:
val counter = PersistentCounter(path = "/tmp/counter.txt")
counter.increment()
counter.increment()
counter.save()
val loaded = counter.load()
println(loaded) // 2
Next steps
See Types for the full list of supported types, Async for async patterns, and Streaming for real-time data.