L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Développement · SKILL.md

uniffi

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 Ko…

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: uniffi description: | 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) allowed-tools: Read, Grep, Glob, Write, Edit

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
Skills enregistrées