FFI framework only: UniFFI proc-macro wrappers + cargo eqswift for Swift bindings. No transports or app logic in the library crate — annotate Rust, build, generate Swift (no UDL, no build.rs).
[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
├── examples/
│ └── otto-ffi/ # Real-world example: AI autocomplete backend
│ ├── Cargo.toml
│ └── src/lib.rs
└── swift/ # Swift package
├── Package.swift
└── Generated/ # cargo eqswift swift (not committed)
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 swift --static # static lib link hints (iOS ship)Call 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
# Static linking (uses libeqswift.a when built)
cargo eqswift swift --static --release
EQSWIFT_STATIC=1 EQSWIFT_PROFILE=release swift buildcargo 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 build -p eqswift --release --target aarch64-apple-darwin
cargo eqswift swift --release --target aarch64-apple-darwin --out-dir swift/GeneratedThe generated files are:
eqswift.swift— Swift types and APIeqswiftFFI.h— C FFI headereqswiftFFI.modulemap— Clang module map
Run cargo eqswift swift --out-dir swift/Generated from the repo root. This repo's swift/Package.swift reads bindings from swift/Generated/ and links ../../target/<profile>/libeqswift (dynamic) or -force_load libeqswift.a when EQSWIFT_STATIC=1. Set EQSWIFT_PROFILE=release for release Rust artifacts.
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 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
ISC