Claude
Skills
Sign in
Back

uniffi

Included with Lifetime
$97 forever

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)

Web Dev

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