Zero-config Rust-to-Swift FFI. Annotate your Rust code, build, generate Swift — no UDL files, no build scripts.
[dependencies]
eqswift = "0.1"
uniffi = "0.31"(UniFFI must be a direct dependency because its proc-macros emit ::uniffi::... paths.)
// src/lib.rs
eqswift::setup!();
#[eqswift::export]
pub fn add(a: u32, b: u32) -> u32 {
a + b
}
#[derive(eqswift::Record)]
pub struct Person {
pub name: String,
pub age: u32,
}
#[derive(eqswift::Object)]
pub struct Greeter;
#[eqswift::export]
impl Greeter {
#[uniffi::constructor]
pub fn new() -> Self {
Self
}
pub fn greet(&self, name: String) -> String {
format!("Hello, {name}!")
}
pub fn greet_person(&self, person: Person) -> String {
format!("Hello, {}! You are {} years old.", person.name, person.age)
}
}cargo build# Using cargo-eqswift (recommended)
cargo eqswift swift
# Or the long way with uniffi-bindgen directly
cargo run --bin uniffi-bindgen generate \
--library target/debug/libeqswift.dylib \
--language swift --out-dir swift/Generated(On Linux use .so, on Windows use .dll)
import eqswift
let sum = add(a: 1, b: 2)
let greeter = Greeter()
let msg = greeter.greet(name: "World")
let person = Person(name: "Alice", age: 30)
let msg2 = greeter.greetPerson(person: person)eqswift/
├── Cargo.toml # Workspace root
├── README.md # This file
├── AGENTS.md # Agent / contributor notes
├── eqswift-macros/ # Proc-macro crate
│ ├── Cargo.toml
│ └── src/lib.rs # #[eqswift::export], eqswift::setup!()
├── eq-swift/ # Your Rust library (crate name = "eqswift")
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs # Your Rust code
│ └── bin/
│ └── uniffi-bindgen.rs # Binding generator binary
├── cargo-eqswift/ # cargo subcommand
│ ├── Cargo.toml
│ └── src/main.rs # cargo eqswift swift / build / kotlin / python
├── examples/
│ └── otto-ffi/ # Real-world example: AI autocomplete backend
│ ├── Cargo.toml
│ └── src/lib.rs
└── swift/ # Swift package
├── Package.swift
└── Sources/
└── EqSwift/
└── EqSwift.swift
cargo install cargo-eqswiftThen use it from any eqswift project:
cargo eqswift swift # generate Swift bindings
cargo eqswift swift --release # use release build
cargo eqswift build # cargo build + generate Swift
cargo eqswift kotlin --out-dir ./out # generate Kotlin bindingsCall once at the top of your lib.rs. It expands to uniffi::setup_scaffolding!() and configures the crate for proc-macro-based FFI.
eqswift::setup!();Annotate free functions or impl blocks to expose them to Swift.
Free functions:
#[eqswift::export]
pub fn calculate(x: f64) -> f64 {
x * 2.0
}Object methods:
#[derive(eqswift::Object)]
pub struct Calculator;
#[eqswift::export]
impl Calculator {
#[uniffi::constructor]
pub fn new() -> Self { Self }
pub fn double(&self, x: f64) -> f64 {
x * 2.0
}
}| Macro | Rust type | Swift type |
|---|---|---|
#[derive(eqswift::Record)] |
struct with public fields |
struct (value type) |
#[derive(eqswift::Object)] |
struct + impl block |
class (reference type) |
#[derive(eqswift::Enum)] |
enum |
enum |
#[derive(eqswift::Error)] |
enum implementing Error |
Error |
Most Rust primitives and common types map directly:
| Rust | Swift |
|---|---|
u32 |
UInt32 |
i32 |
Int32 |
u64 |
UInt64 |
f64 |
Double |
bool |
Bool |
String |
String |
Vec<T> |
[T] |
Option<T> |
T? |
Result<T, E> |
throws |
See UniFFI type docs for the full list.
# Generate Swift bindings (debug build)
cargo eqswift swift
# Generate Swift bindings (release build)
cargo eqswift swift --release
# Build + generate in one step
cargo eqswift build
# Generate Kotlin bindings
cargo eqswift kotlin --out-dir ./android/src/main/java
# Generate Python bindings
cargo eqswift python --out-dir ./python/eqswiftcargo run --bin uniffi-bindgen generate \
--library target/debug/libeqswift.dylib \
--language swift --out-dir eq-swift/swift/Generatedcargo run --bin uniffi-bindgen generate \
--library target/debug/libeqswift.so \
--language swift --out-dir eq-swift/swift/Generatedcargo run --bin uniffi-bindgen generate ^
--library target/debug/eqswift.dll ^
--language swift --out-dir eq-swift/swift/GeneratedReplace target/debug/ with target/release/ and add --release to cargo build.
For shipping to iOS/macOS, wrap the generated Swift code and the Rust library in an XCFramework:
# Build for multiple targets
cargo build --release --target aarch64-apple-darwin
cargo build --release --target aarch64-apple-ios
# Generate bindings once (metadata is arch-agnostic)
cargo eqswift swift --release \
--library target/aarch64-apple-darwin/release/libeqswift.dylib \
--out-dir swift/GeneratedThe generated files are:
eqswift.swift— Swift types and APIeqswiftFFI.h— C FFI headereqswiftFFI.modulemap— Clang module map
Add them to your Xcode project or Swift package. A minimal Package.swift:
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "EqSwift",
platforms: [.macOS(.v14), .iOS(.v17)],
products: [.library(name: "EqSwift", targets: ["EqSwift"])],
targets: [
.target(
name: "EqSwift",
dependencies: [],
path: "Sources/EqSwift",
publicHeadersPath: "include"
)
]
)Then symlink or copy the generated files into Sources/EqSwift/.
Make sure eq-swift/Cargo.toml has the [[bin]] section and uniffi is declared with features = ["cli"].
Delete eq-swift/swift/Generated/ and re-run the bindgen command.
When running Swift tests that load the Rust library, the dynamic linker needs to find the .dylib. Set:
export DYLD_LIBRARY_PATH="$(pwd)/target/debug:$DYLD_LIBRARY_PATH"Only types used in #[eqswift::export] functions or marked with #[derive(...)] are exported. Make sure the type is public and appears in a signature or derive.
Install it first:
cargo install cargo-eqswifteqswift::setup!()callsuniffi::setup_scaffolding!(), which sets up the proc-macro metadata system.#[eqswift::export]wraps#[uniffi::export], registering functions and methods in the metadata.#[derive(eqswift::Record)]re-exportsuniffi::Record, which implements the FFI conversion traits.cargo buildembeds all metadata into the compiled library.cargo eqswift(oruniffi-bindgen) reads the metadata from the library and generates Swift/Kotlin/Python bindings.
No UDL file is ever written or parsed. The entire interface is defined by your Rust code.
Run the integration smoke tests:
cargo test -p eqswift --test integrationThis verifies:
- Library compiles
- Swift bindings are generated
- Exported items appear in generated Swift
MPL-2.0