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.32"(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 --features cli --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 --features cli --bin uniffi-bindgen generate \
--library target/debug/libeqswift.dylib \
--language swift --out-dir eq-swift/swift/Generatedcargo run --features cli --bin uniffi-bindgen generate \
--library target/debug/libeqswift.so \
--language swift --out-dir eq-swift/swift/Generatedcargo run --features cli --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 enable the cli feature: cargo run -p eqswift --features cli --bin uniffi-bindgen.
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.
The cross-family FFI ownership contract is a proposal, not a claim that eqswift conforms or interoperates with other projects. UniFFI behavior for the resolved version (0.32.1 in Cargo.lock) is unverified. Before an adapter opts in:
- Inspect the generated Swift and C bindings for
Person/Greeterand the resolved UniFFI runtime and documentation; document string and object ownership, release, lifetimes, and failure behavior for each supported target. - Extend
eq-swift/tests/integration.rsbeyond binding-generation smoke tests with Swift-to-Rust transfer, release and error-cleanup tests, plus callback shutdown and incompatible contract-version rejection tests where the adapter supports them.
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