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.