Configuration
BoltFFI reads its configuration from a file called boltffi.toml in your project’s root directory. This file tells BoltFFI how to name your modules, where to put generated files, and how to structure the output packages. Run boltffi init to create one with sensible defaults, then customize it for your needs.
Package Identity
Every boltffi.toml starts with a [package] section that identifies your library:
[package]
name = "mylib"
The name field is used to derive default names throughout the pipeline. For example, if your package name is mylib, BoltFFI generates a Swift module called MyLib and a Kotlin class called MyLib. All output paths also use this name as a base.
If your Rust crate has a different name than what you want to expose (perhaps your crate is my_lib with underscores but you want mylib), specify both:
[package]
name = "mylib"
crate = "my_lib"
BoltFFI scans and builds the crate specified by crate, but uses name for all generated module names and paths.
Apple Configuration
The [apple] section controls iOS, iOS Simulator, and optionally macOS builds.
Output Directory
All Apple artifacts go into a single root directory. By default this is dist/apple, but you can change it:
[apple]
output = "build/ios"
After running boltffi pack apple, this directory contains your xcframework, Package.swift, and generated Swift sources.
Deployment Target
The deployment target sets the minimum iOS version your library supports. This affects which APIs are available and which devices can run your code:
[apple]
deployment_target = "15.0"
The default is 16.0. Lower this if you need to support older devices, but be aware that some Swift concurrency features require iOS 13+ and full async/await requires iOS 15+.
Including macOS
By default, BoltFFI only builds for iOS and iOS Simulator. If your library also needs to run on Mac, enable macOS builds:
[apple]
include_macos = true
This adds macOS slices to your xcframework, making it usable in Mac Catalyst apps and native macOS applications.
Swift Module Name
The generated Swift code is organized into a module. By default, BoltFFI converts your package name to PascalCase (mylib becomes MyLib). Override this if you want a different name:
[apple.swift]
module_name = "MyLibrary"
This name appears in your Swift import statements: import MyLibrary.
SwiftPM Layouts
The layout determines how BoltFFI structures the SwiftPM package. This is the most important configuration choice for Apple because it affects how you integrate the library into your Xcode project.
ffi-only Layout
This is the default and simplest option. BoltFFI creates a self-contained SwiftPM package with everything inside:
[apple.spm]
layout = "ffi-only"
When to use: You want a drop-in package that works immediately. No additional setup required.
Output structure:
dist/apple/
├── MyLib.xcframework/
│ ├── ios-arm64/
│ │ ├── Headers/mylib.h
│ │ └── libmylib.a
│ └── ios-arm64_x86_64-simulator/
│ ├── Headers/mylib.h
│ └── libmylib.a
├── Package.swift
└── Sources/
└── BoltFFI/
└── MyLibBoltFFI.swift
Generated Package.swift:
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "MyLib",
platforms: [.iOS(.v16)],
products: [
.library(name: "MyLib", targets: ["MyLib"])
],
targets: [
.binaryTarget(
name: "MyLibFFI",
path: "MyLib.xcframework"
),
.target(
name: "MyLib",
dependencies: ["MyLibFFI"],
path: "Sources/BoltFFI"
)
]
)
To use this package in Xcode, add it as a local package dependency pointing to dist/apple, then import MyLib in your Swift code.
bundled Layout
Use this when you have an existing Swift package and want to add BoltFFI-generated bindings to it. BoltFFI places the generated Swift inside your existing source directory:
[apple.spm]
layout = "bundled"
wrapper_sources = "Sources/MyWrapper"
When to use: You’re adding Rust functionality to an existing Swift package, or you want to write additional Swift wrapper code alongside the generated bindings.
Output structure:
dist/apple/
├── MyLib.xcframework/
├── Package.swift
└── Sources/
└── MyWrapper/
├── YourExistingCode.swift
└── BoltFFI/
└── MyLibBoltFFI.swift
The wrapper_sources path tells BoltFFI where your existing Swift target lives. Generated bindings go into a Riff subdirectory inside that path. Your Package.swift should already have a target pointing at Sources/MyWrapper.
split Layout
Use this when you want maximum control. BoltFFI creates a binary-only package containing just the xcframework, and writes the generated Swift to a separate location:
[apple.spm]
layout = "split"
[apple.swift]
output = "Sources/Generated"
When to use: You maintain your own SwiftPM package structure and just need the xcframework and generated code as raw ingredients.
Output structure:
dist/apple/
├── MyLib.xcframework/
└── Package.swift # binary target only
Sources/
└── Generated/
└── BoltFFI/
└── MyLibBoltFFI.swift
Generated Package.swift:
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "MyLibFFI",
platforms: [.iOS(.v16)],
products: [
.library(name: "MyLibFFI", targets: ["MyLibFFI"])
],
targets: [
.binaryTarget(
name: "MyLibFFI",
path: "MyLib.xcframework"
)
]
)
You then create your own package that depends on MyLibFFI and includes the generated Swift from Sources/Generated.
Remote Distribution
By default, Package.swift references the xcframework via a local file path. This works for development and when you bundle the package directly in your app repository. For distributing your library to others, switch to remote distribution:
[apple.spm]
distribution = "remote"
repo_url = "https://github.com/yourname/mylib/releases/download"
Then package with a version:
boltffi pack apple --release --version 1.0.0
Generated Package.swift:
.binaryTarget(
name: "MyLibFFI",
url: "https://github.com/yourname/mylib/releases/download/1.0.0/MyLib.xcframework.zip",
checksum: "abc123..."
)
Upload MyLib.xcframework.zip to your GitHub release. Consumers can then add your package by URL without needing the source.
Type Mappings
If you use custom types that should map to native Swift types, configure type mappings. For example, if your Rust code uses a Uuid type that should become Swift’s UUID:
[apple.swift.type_mappings]
Uuid = { type = "UUID", conversion = "uuid_string" }
The generated Swift uses UUID directly instead of a wrapper type. BoltFFI handles the string conversion automatically at the FFI boundary.
Available conversions:
uuid_string: Converts between String and UUIDurl_string: Converts between String and URL
Android Configuration
The [android] section controls builds for all Android ABIs.
Output Directory
All Android artifacts go into a single root directory:
[android]
output = "build/android"
The default is dist/android. After running boltffi pack android, this directory contains your jniLibs and Kotlin sources.
Minimum SDK
Set the minimum Android API level:
[android]
min_sdk = 21
The default is 24 (Android 7.0). Lower values support more devices but may limit available APIs.
Kotlin Package
Generated Kotlin code needs a package name. By default, BoltFFI uses com.example.{name}:
[android.kotlin]
package = "com.mycompany.mylib"
This determines the directory structure of the generated files and the package declaration in the Kotlin source.
API Style
Choose how exported functions appear in Kotlin:
[android.kotlin]
api_style = "top_level"
top_level (default): Functions are top-level Kotlin functions. Import the package and call directly:
import com.mycompany.mylib.*
val result = processData(input)
module_object: Functions are methods on an object. Useful if you want to namespace everything:
[android.kotlin]
api_style = "module_object"
module_name = "MyLib"
import com.mycompany.mylib.MyLib
val result = MyLib.processData(input)
Factory Style
When your Rust code has factory functions (functions that create class instances), choose how they appear in Kotlin:
[android.kotlin]
factory_style = "constructors"
constructors (default): Factory functions become Kotlin constructors:
val client = HttpClient(baseUrl)
companion_methods: Factory functions become companion object methods:
val client = HttpClient.create(baseUrl)
Output Structure
After running boltffi pack android, you get:
dist/android/
├── jniLibs/
│ ├── arm64-v8a/
│ │ └── libmylib.so
│ ├── armeabi-v7a/
│ │ └── libmylib.so
│ ├── x86/
│ │ └── libmylib.so
│ └── x86_64/
│ └── libmylib.so
└── kotlin/
├── com/mycompany/mylib/
│ └── MyLib.kt
└── jni/
└── jni_glue.c
Copy jniLibs to your Android project’s src/main/ directory. Copy the Kotlin sources to your source set. The native libraries are loaded automatically when you first use the Kotlin bindings.
Complete Example
Here’s a full configuration for a library that targets both platforms:
[package]
name = "mylib"
[apple]
output = "dist/apple"
deployment_target = "15.0"
include_macos = true
[apple.swift]
module_name = "MyLib"
[apple.swift.type_mappings]
Uuid = { type = "UUID", conversion = "uuid_string" }
[apple.spm]
layout = "ffi-only"
distribution = "local"
[android]
output = "dist/android"
min_sdk = 24
[android.kotlin]
package = "com.mycompany.mylib"
api_style = "top_level"
factory_style = "constructors"
This configuration:
- Names the library
mylib, generatingMyLibmodules - Builds for iOS 15+, iOS Simulator, and macOS
- Uses the simple ffi-only SwiftPM layout
- Maps
Uuidto native SwiftUUID - Builds for Android API 24+
- Generates top-level Kotlin functions with constructor-style factories