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)

GitHub
Install command
npx skhub add claude-dev-suite/uniffi
Markdown
SKILL.md

UniFFI — Rust ↔ Kotlin/Swift Bindings

References: proc-macro.md for inline #[uniffi::export] macro mode (UDL-free). 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

[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

fn main() {
    uniffi::generate_scaffolding("./src/wallet.udl").unwrap();
}

uniffi-bindgen.rs

fn main() {
    uniffi::uniffi_bindgen_main()
}

src/wallet.udl

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

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

# 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):

// 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)

UDLRustKotlinSwift
booleanboolBooleanBool
u8/i8 ... u64/i64u8/i8 ... u64/i64UByte/Byte ... ULong/LongUInt8/Int8 ... UInt64/Int64
f32/f64f32/f64Float/DoubleFloat/Double
stringStringStringString
bytesVec<u8>ByteArrayData
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/ implclass X : Disposableclass X
[Enum] enum X { ... }enum w/ unit variantsenum class Xenum X
enum X { Variant(T) } (with assoc)enum w/ data variantssealed classenum w/ associated values
[Error] enum X { ... }enum impl std::error::Errorsealed Exceptionenum: Error

Async Support

interface Wallet {
    [Async, Throws=WalletError]
    Balance sync();
};
#[uniffi::export(async_runtime = "tokio")]
impl Wallet {
    pub async fn sync(&self) -> Result<Balance, WalletError> {
        // tokio async work
        Ok(self.get_balance()?)
    }
}
// Kotlin — exposed as suspend function
val balance: Balance = wallet.sync()
// 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.

callback interface BlockListener {
    void on_new_block(u64 height, string hash);
};

namespace wallet {
    void watch_blocks(BlockListener listener);
};
class MyListener : BlockListener {
    override fun onNewBlock(height: ULong, hash: String) {
        log("block $height: $hash")
    }
}

watchBlocks(MyListener())
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)

[Trait]
interface Signer {
    bytes sign(bytes message);
};
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

[Error]
enum WalletError {
    "InvalidMnemonic",
    "Network",
    "InsufficientFunds",
};

For richer errors with payload:

#[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.

Custom Types (Newtype Pattern)

[Custom]
typedef string Address;
pub struct Address(pub String);

impl UniffiCustomTypeConverter for Address {
    type Builtin = String;
    fn into_custom(val: String) -> Result<Self, anyhow::Error> {
        if !val.starts_with("bc1") { anyhow::bail!("invalid address"); }
        Ok(Address(val))
    }
    fn from_custom(obj: Self) -> String { obj.0 }
}

Address validates on the FFI boundary. Bindings see String but Rust gets validated Address.

Memory Model

  • Records (dictionaries) → marshaled by value (copied across FFI)
  • Interfaces → reference type, ref-counted (Arc-equivalent on both sides)
  • Kotlin: implements Disposable (AutoCloseable) → use wallet.use { ... } blocks
  • Swift: deinit calls into Rust to drop
Wallet(mnemonic).use { wallet ->
    val addr = wallet.getAddress(0u)
}
// Disposed automatically here
{
    let wallet = try Wallet(mnemonic: mnemonic)
    let addr = wallet.getAddress(index: 0)
    // wallet.deinit at end of scope releases Rust resources
}

CRITICAL: forgetting .use { } (Kotlin) leaks the Rust object until GC eventually finalizes — long-running mobile apps can leak megabytes. Always wrap in use or try-with-resources.

Bundling Bindings

Android (Gradle)

// build.gradle.kts (Android module)
android {
    sourceSets["main"].apply {
        java.srcDirs("../uniffi-output/kotlin")
        jniLibs.srcDirs("../uniffi-output/jniLibs")  // .so files per ABI
    }
}

dependencies {
    implementation("net.java.dev.jna:jna:5.14.0@aar")
}

Build native libs per ABI:

# Use cargo-ndk for cross-compile to Android
cargo install cargo-ndk
cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 -o ./jniLibs build --release

iOS (Swift Package or XCFramework)

Build for iOS targets:

cargo build --release --target aarch64-apple-ios
cargo build --release --target aarch64-apple-ios-sim
cargo build --release --target x86_64-apple-ios

# Package as XCFramework
xcodebuild -create-xcframework \
    -library target/aarch64-apple-ios/release/libwallet_ffi.a \
        -headers ./bindings/swift/include \
    -library target/aarch64-apple-ios-sim/release/libwallet_ffi.a \
        -headers ./bindings/swift/include \
    -output Wallet.xcframework

Then drop Wallet.xcframework + generated wallet.swift into Xcode project (or vendor via SwiftPM).

Anti-Patterns

Anti-patternWhy it's badCorrect approach
Forgetting use { } (Kotlin)Memory leakAlways wrap in use { } or implement Closeable
Returning raw Vec<u8> from hot loopsPer-call allocUse streaming/callbacks or batch
Sync APIs that block IOBlocks UIMark [Async] and use Dispatchers.IO / async
Leaking trait callback registrationsMemory growthAlways unregister listeners
String for type-safe IDsNo FFI safetyUse [Custom] types with validation
Panic in Rust (no Result)Crash on hostConvert panics → Result<_, E>
Large recursive typesSlow marshalingFlatten or paginate
Generic functions in [Trait] interfaceNot supportedSpecialize to concrete types

Anti-Pitfalls Specific to Mobile

  • Android JNA: required runtime dep — bundle correctly, watch for ProGuard rules
  • iOS bitcode: deprecated, but check Xcode build settings for warnings
  • Swift module name conflicts: rename library_name in UDL or generated module
  • Kotlin nullability: T? in UDL maps to nullable in Kotlin — match Rust Option<T>
  • Async cancellation: cancellation does NOT propagate from Kotlin coroutine → Rust future automatically. Implement explicit cancel API if needed

When to Use UniFFI vs Alternatives

NeedPick
Rust → Kotlin/Swift, multi-platformUniFFI
Rust → Kotlin Multiplatform (single common module)uniffi-kotlin-multiplatform-bindings (fork)
Rust → Flutter/Dartflutter_rust_bridge
Rust → React Nativeuniffi-bindgen-react-native
Rust → Web (browser)wasm-bindgen
Rust → C only (or one host language, max perf)Raw extern "C" + cbindgen

When NOT to Use This Skill

ScenarioUse Instead
Pure Rust binding to C librust core skills + bindgen
Manual FFI from Swift to Rustlanguages/swift interop quick-ref
KMP gradle setupmobile/kotlin-multiplatform
Compose-side wallet UIfrontend-frameworks/compose-multiplatform
BDK/Breez SDK API specificsbitcoin/libraries/bdk + bitcoin/lightning/ldk
Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

MIT

Source path

skills/languages/uniffi

Default branch

main

Latest commit

9496306

Tree SHA

fe4e2f1