用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/tomevault-io/skills-registry --skill uniffi命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
基于 SOC 职业分类
正在显示 SKILL.md
| name | uniffi |
| description | | Use when this capability is needed. |
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_docswith technology:uniffi.
UniFFI generates safe, idiomatic language bindings (Kotlin, Swift, Python, Ruby) from a Rust crate. The generated code:
String, Vec<T>, Option<T>, Result<T,E>, custom enums/structs, traits)Two definition modes:
.udl file, IDL-like) — explicit, language-neutral#[uniffi::export] inline on Rust items) — terser, modernMost Bitcoin libraries (BDK, LDK Node, Breez SDK Liquid, CDK, LWK) use UDL for stability. New crates increasingly use proc-macro mode.
[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"
fn main() {
uniffi::generate_scaffolding("./src/wallet.udl").unwrap();
}
fn main() {
uniffi::uniffi_bindgen_main()
}
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;
};
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 (mnemonic: ) <, WalletError> {
mnemonic.().() < {
(WalletError::InvalidMnemonic);
}
(Wallet { })
}
(&, index: ) {
()
}
(&) <Balance, WalletError> {
(Balance { confirmed: , trusted_pending: , untrusted_pending: })
}
}
# 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")
}
| 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 |
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.
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]
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]
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]
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.
Arc-equivalent on both sides)Disposable (AutoCloseable) → use wallet.use { ... } blocksWallet(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.
// 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
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-pattern | Why it's bad | Correct approach |
|---|---|---|
Forgetting use { } (Kotlin) | Memory leak | Always wrap in use { } or implement Closeable |
Returning raw Vec<u8> from hot loops | Per-call alloc | Use streaming/callbacks or batch |
| Sync APIs that block IO | Blocks UI | Mark [Async] and use Dispatchers.IO / async |
| Leaking trait callback registrations | Memory growth | Always unregister listeners |
String for type-safe IDs | No FFI safety | Use [Custom] types with validation |
Panic in Rust (no Result) | Crash on host | Convert panics → Result<_, E> |
| Large recursive types | Slow marshaling | Flatten or paginate |
Generic functions in [Trait] interface | Not supported | Specialize to concrete types |
library_name in UDL or generated moduleT? in UDL maps to nullable in Kotlin — match Rust Option<T>| Need | Pick |
|---|---|
| Rust → Kotlin/Swift, multi-platform | UniFFI |
| Rust → Kotlin Multiplatform (single common module) | uniffi-kotlin-multiplatform-bindings (fork) |
| Rust → Flutter/Dart | flutter_rust_bridge |
| Rust → React Native | uniffi-bindgen-react-native |
| Rust → Web (browser) | wasm-bindgen |
| Rust → C only (or one host language, max perf) | Raw extern "C" + cbindgen |
| Scenario | Use Instead |
|---|---|
| Pure Rust binding to C lib | rust core skills + bindgen |
| Manual FFI from Swift to Rust | languages/swift interop quick-ref |
| KMP gradle setup | mobile/kotlin-multiplatform |
| Compose-side wallet UI | frontend-frameworks/compose-multiplatform |
| BDK/Breez SDK API specifics | bitcoin/libraries/bdk + bitcoin/lightning/ldk |
Source: claude-dev-suite/claude-dev-suite — distributed by TomeVault.
| sealed class |
| enum w/ associated values |
[Error] enum X { ... } | enum impl std::error::Error | sealed Exception | enum: Error |