uniffi
UniFFI by Mozilla — generates idiomatic Kotlin, Swift, Python, and Ruby bindings from a Rust crate. Covers UDL definition, proc-macro mode, async support, callback interfaces, error handling, custom types, and the Kotlin Multiplatform fork (uniffi-kotlin-multiplatform-bindings) used by BDK, Breez SDK, CDK, LWK. USE WHEN: user mentions "UniFFI", "Rust to Kotlin", "Rust to Swift", "FFI bindings", "uniffi-rs", "UDL file", "uniffi-bindgen", "BDK bindings", "Breez SDK bindings", "kotlin-multiplatform-bindings", "Mozilla UniFFI" DO NOT USE FOR: Raw C FFI - use `languages/swift` interop quick-ref + Rust core DO NOT USE FOR: WebAssembly bindings - use wasm-bindgen DO NOT USE FOR: Flutter/Rust bridge - use `flutter_rust_bridge` skill if exists DO NOT USE FOR: React Native - use `uniffi-bindgen-react-native` (out of scope here)
What this skill does
# UniFFI — Rust ↔ Kotlin/Swift Bindings
> **References**: [proc-macro.md](quick-ref/proc-macro.md) for inline `#[uniffi::export]` macro mode (UDL-free). [kmp-bindings.md](quick-ref/kmp-bindings.md) for the Kotlin Multiplatform fork used by BDK/Breez/CDK.
>
> **Deep Knowledge**: Use `mcp__documentation__fetch_docs` with technology: `uniffi`.
## What UniFFI Is
UniFFI generates **safe, idiomatic** language bindings (Kotlin, Swift, Python, Ruby) from a Rust crate. The generated code:
- Marshals types correctly (handles `String`, `Vec<T>`, `Option<T>`, `Result<T,E>`, custom enums/structs, traits)
- Manages memory across the FFI boundary (RAII, reference counting)
- Maps Rust errors to native exceptions
- Supports async functions, callback interfaces, and trait objects
**Two definition modes**:
1. **UDL** (`.udl` file, IDL-like) — explicit, language-neutral
2. **Proc-macro** (`#[uniffi::export]` inline on Rust items) — terser, modern
Most Bitcoin libraries (BDK, LDK Node, Breez SDK Liquid, CDK, LWK) use UDL for stability. New crates increasingly use proc-macro mode.
## Minimal Project Setup
### Cargo.toml
```toml
[package]
name = "wallet-ffi"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "staticlib"] # both for mobile bundling
name = "wallet_ffi"
[dependencies]
uniffi = { version = "0.28", features = ["cli"] }
thiserror = "1.0"
[build-dependencies]
uniffi = { version = "0.28", features = ["build"] }
[[bin]]
name = "uniffi-bindgen"
path = "uniffi-bindgen.rs"
```
### build.rs
```rust
fn main() {
uniffi::generate_scaffolding("./src/wallet.udl").unwrap();
}
```
### uniffi-bindgen.rs
```rust
fn main() {
uniffi::uniffi_bindgen_main()
}
```
### src/wallet.udl
```idl
namespace wallet {
[Throws=WalletError]
string generate_mnemonic(u32 word_count);
string derive_address(string mnemonic, u32 index);
};
[Error]
enum WalletError {
"InvalidMnemonic",
"InvalidIndex",
"Internal",
};
interface Wallet {
[Throws=WalletError]
constructor(string mnemonic);
string get_address(u32 index);
[Throws=WalletError]
Balance get_balance();
};
dictionary Balance {
u64 confirmed;
u64 trusted_pending;
u64 untrusted_pending;
};
```
### src/lib.rs
```rust
use thiserror::Error;
uniffi::include_scaffolding!("wallet");
#[derive(Debug, Error)]
pub enum WalletError {
#[error("invalid mnemonic")]
InvalidMnemonic,
#[error("invalid index")]
InvalidIndex,
#[error("internal error: {0}")]
Internal(String),
}
pub struct Balance {
pub confirmed: u64,
pub trusted_pending: u64,
pub untrusted_pending: u64,
}
pub fn generate_mnemonic(word_count: u32) -> Result<String, WalletError> {
// ...
Ok("abandon abandon ...".to_string())
}
pub fn derive_address(mnemonic: String, index: u32) -> String {
format!("bc1q...{index}")
}
pub struct Wallet { /* internal state */ }
impl Wallet {
pub fn new(mnemonic: String) -> Result<Self, WalletError> {
if mnemonic.split_whitespace().count() < 12 {
return Err(WalletError::InvalidMnemonic);
}
Ok(Wallet { /* ... */ })
}
pub fn get_address(&self, index: u32) -> String {
format!("bc1q...{index}")
}
pub fn get_balance(&self) -> Result<Balance, WalletError> {
Ok(Balance { confirmed: 100_000, trusted_pending: 0, untrusted_pending: 0 })
}
}
```
### Generate Bindings
```bash
# Build native lib first
cargo build --release
# Generate Kotlin bindings
cargo run --bin uniffi-bindgen generate src/wallet.udl \
--language kotlin --out-dir ./bindings/kotlin
# Generate Swift bindings
cargo run --bin uniffi-bindgen generate src/wallet.udl \
--language swift --out-dir ./bindings/swift
```
Output (Kotlin example):
```kotlin
// bindings/kotlin/uniffi/wallet/wallet.kt — generated
@Throws(WalletException::class)
fun generateMnemonic(wordCount: UInt): String { /* ... */ }
class Wallet : Disposable {
@Throws(WalletException::class)
constructor(mnemonic: String) { /* ... */ }
fun getAddress(index: UInt): String { /* ... */ }
@Throws(WalletException::class)
fun getBalance(): Balance { /* ... */ }
}
data class Balance(
val confirmed: ULong,
val trustedPending: ULong,
val untrustedPending: ULong,
)
sealed class WalletException(message: String) : Exception(message) {
object InvalidMnemonic : WalletException("invalid mnemonic")
object InvalidIndex : WalletException("invalid index")
class Internal(message: String) : WalletException("internal: $message")
}
```
## Type Mapping (UDL ↔ Rust ↔ Kotlin ↔ Swift)
| UDL | Rust | Kotlin | Swift |
|---|---|---|---|
| `boolean` | `bool` | `Boolean` | `Bool` |
| `u8/i8 ... u64/i64` | `u8/i8 ... u64/i64` | `UByte/Byte ... ULong/Long` | `UInt8/Int8 ... UInt64/Int64` |
| `f32/f64` | `f32/f64` | `Float/Double` | `Float/Double` |
| `string` | `String` | `String` | `String` |
| `bytes` | `Vec<u8>` | `ByteArray` | `Data` |
| `sequence<T>` | `Vec<T>` | `List<T>` | `[T]` |
| `record<K,V>` | `HashMap<K,V>` | `Map<K,V>` | `[K: V]` |
| `T?` | `Option<T>` | `T?` | `T?` |
| `dictionary X { ... }` | `struct X { ... }` | `data class X(...)` | `struct X` |
| `interface X { ... }` | `pub struct X` w/ `impl` | `class X : Disposable` | `class X` |
| `[Enum] enum X { ... }` | enum w/ unit variants | `enum class X` | `enum X` |
| `enum X { Variant(T) }` (with assoc) | enum w/ data variants | sealed class | enum w/ associated values |
| `[Error] enum X { ... }` | enum impl `std::error::Error` | sealed Exception | enum: Error |
## Async Support
```idl
interface Wallet {
[Async, Throws=WalletError]
Balance sync();
};
```
```rust
#[uniffi::export(async_runtime = "tokio")]
impl Wallet {
pub async fn sync(&self) -> Result<Balance, WalletError> {
// tokio async work
Ok(self.get_balance()?)
}
}
```
```kotlin
// Kotlin — exposed as suspend function
val balance: Balance = wallet.sync()
```
```swift
// Swift — exposed as async throws
let balance = try await wallet.sync()
```
UniFFI bridges Rust futures (Tokio runtime) to Kotlin coroutines and Swift's Task system. Polling is driven by the host runtime — your Rust code can `await` freely.
## Callback Interfaces (Host → Rust)
For event listeners or strategy injection.
```idl
callback interface BlockListener {
void on_new_block(u64 height, string hash);
};
namespace wallet {
void watch_blocks(BlockListener listener);
};
```
```kotlin
class MyListener : BlockListener {
override fun onNewBlock(height: ULong, hash: String) {
log("block $height: $hash")
}
}
watchBlocks(MyListener())
```
```swift
final class MyListener: BlockListener {
func onNewBlock(height: UInt64, hash: String) {
print("block \(height): \(hash)")
}
}
watchBlocks(listener: MyListener())
```
**Lifecycle**: callback objects are reference-counted; Rust holds a strong ref while the listener is registered. Always provide a way to unregister to avoid leaks.
## Trait Interfaces (Rust → Host as polymorphic)
```idl
[Trait]
interface Signer {
bytes sign(bytes message);
};
```
```rust
pub trait Signer: Send + Sync {
fn sign(&self, message: Vec<u8>) -> Vec<u8>;
}
```
The host can implement `Signer` and pass instances back to Rust functions accepting `Arc<dyn Signer>`. Useful for hardware wallet signers, custom key sources.
## Error Handling
```idl
[Error]
enum WalletError {
"InvalidMnemonic",
"Network",
"InsufficientFunds",
};
```
For richer errors with payload:
```rust
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum WalletError {
#[error("invalid mnemonic")]
InvalidMnemonic,
#[error("network: {0}")]
Network(String),
#[error("insufficient funds: need {need}, have {have}")]
InsufficientFunds { need: u64, have: u64 },
}
```
`#[uniffi(flat_error)]` collapses to a single message string in bindings (simpler). Without it, fields are exposed.Related in Web Dev
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.