# UniFFI Kotlin Multiplatform Bindings (Fork)

The upstream `uniffi-rs` generates **JVM-only Kotlin** (relies on JNA). For Kotlin Multiplatform projects targeting iOS, Android, and Desktop with a **single shared `commonMain` module**, use the **UbiqueInnovation fork**:

> https://github.com/UbiqueInnovation/uniffi-kotlin-multiplatform-bindings

This is the de-facto standard for Bitcoin libs that ship KMP bindings: BDK, LDK Node, Breez SDK Liquid, CDK, LWK.

## Architecture

```
shared/
├── build.gradle.kts
├── src/
│   ├── commonMain/kotlin/        # generated by KMP fork — common interface
│   ├── androidMain/kotlin/       # generated — JNA via Android NDK
│   ├── iosMain/kotlin/           # generated — Kotlin/Native cinterop
│   ├── jvmMain/kotlin/           # generated — JNA via JVM
│   └── (your business logic)
└── nativeInterop/
    └── cinterop/
        └── wallet.def            # Kotlin/Native interop definition
```

## Setup

### 1. Build Rust for all KMP targets

```bash
# Android (via cargo-ndk)
cargo install cargo-ndk
cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 -o ./libs/android build --release

# iOS (device + sim)
cargo build --release --target aarch64-apple-ios
cargo build --release --target aarch64-apple-ios-sim

# Combine sim archs
lipo -create \
    target/aarch64-apple-ios-sim/release/libwallet_ffi.a \
    target/x86_64-apple-ios/release/libwallet_ffi.a \
    -output target/universal/libwallet_ffi.a

# JVM Desktop (host arch)
cargo build --release
```

### 2. Generate KMP bindings

Install the KMP bindgen:

```bash
cargo install --git https://github.com/UbiqueInnovation/uniffi-kotlin-multiplatform-bindings \
    uniffi-bindgen-kotlin-multiplatform
```

Run:

```bash
uniffi-bindgen-kotlin-multiplatform \
    --out-dir ./shared/src \
    ./crates/wallet-ffi/src/wallet.udl
```

Generates:
- `commonMain/kotlin/uniffi/wallet/Wallet.kt` — interface
- `androidMain/.../Wallet.android.kt` — JNA wrapper
- `iosMain/.../Wallet.ios.kt` — Kotlin/Native cinterop wrapper
- `jvmMain/.../Wallet.jvm.kt` — JNA wrapper
- `nativeInterop/cinterop/wallet.def` — Kotlin/Native def

### 3. Wire up Gradle

```kotlin
// shared/build.gradle.kts
plugins {
    kotlin("multiplatform") version "2.2.0"
    id("com.android.library") version "8.7.0"
}

kotlin {
    androidTarget {
        compilations.all {
            kotlinOptions { jvmTarget = "17" }
        }
    }
    listOf(
        iosX64(), iosArm64(), iosSimulatorArm64()
    ).forEach { target ->
        target.binaries.framework {
            baseName = "shared"
            isStatic = true
        }
        target.compilations.getByName("main") {
            cinterops {
                val wallet by creating {
                    defFile(project.file("src/nativeInterop/cinterop/wallet.def"))
                    packageName("uniffi.wallet.cinterop")
                }
            }
        }
    }
    jvm("desktop")

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.0")
            }
        }
        val androidMain by getting {
            dependencies {
                implementation("net.java.dev.jna:jna:5.14.0@aar")
            }
        }
        val desktopMain by getting {
            dependencies {
                implementation("net.java.dev.jna:jna:5.14.0")
            }
        }
    }
}

android {
    namespace = "com.bhodl.shared"
    compileSdk = 35
    defaultConfig { minSdk = 26 }
    sourceSets["main"].jniLibs.srcDirs("src/androidMain/jniLibs")
}
```

### 4. Place native libraries

```
shared/
├── src/
│   ├── androidMain/jniLibs/
│   │   ├── arm64-v8a/libwallet_ffi.so
│   │   ├── armeabi-v7a/libwallet_ffi.so
│   │   └── x86_64/libwallet_ffi.so
│   └── nativeInterop/cinterop/
│       └── wallet.def
```

For iOS, the static lib path is referenced from `wallet.def`:

```
# wallet.def
language = Objective-C
package = uniffi.wallet
headers = wallet.h
staticLibraries = libwallet_ffi.a
libraryPaths.ios_arm64 = ../../target/aarch64-apple-ios/release
libraryPaths.ios_simulator_arm64 = ../../target/aarch64-apple-ios-sim/release
libraryPaths.ios_x64 = ../../target/x86_64-apple-ios/release
linkerOpts = -framework Security -framework Foundation
```

## Using from Common Code

```kotlin
// commonMain
import uniffi.wallet.*

class WalletRepository {
    suspend fun newWallet(mnemonic: String): Wallet =
        Wallet(mnemonic)

    suspend fun balance(wallet: Wallet): Balance =
        wallet.sync()      // async via UniFFI
}
```

Same Kotlin code compiles for Android, iOS, JVM Desktop. UniFFI handles per-platform marshaling under the hood.

## Async Across Platforms

Async functions in UDL generate `suspend` Kotlin functions. The bridge converts:
- **JVM/Android** → JNA → Tokio runtime
- **iOS** → Kotlin/Native → calls into static-linked Rust → Tokio runtime

All work happens on Tokio's thread pool inside the Rust lib. Cancellation does NOT auto-propagate — implement explicit `cancel()` in Rust if needed.

```kotlin
// commonMain
suspend fun syncOrTimeout(wallet: Wallet) = withTimeoutOrNull(30_000) {
    wallet.sync()
}
```

The `withTimeoutOrNull` cancels the Kotlin coroutine, but the Rust task continues until it next yields. For true cancellation, expose `Wallet.cancelSync()` from Rust.

## Memory Management

KMP bindings implement `AutoCloseable`. **Always close** — KN finalizers are not guaranteed to run promptly, and on iOS there's no GC at all.

```kotlin
// Recommended pattern
suspend fun withWallet(mnemonic: String, block: suspend (Wallet) -> Unit) {
    val wallet = Wallet(mnemonic)
    try {
        block(wallet)
    } finally {
        wallet.close()
    }
}
```

For long-lived wallet instances (app lifetime), close on app shutdown:

```kotlin
class WalletManager {
    private var wallet: Wallet? = null

    fun init(mnemonic: String) {
        wallet = Wallet(mnemonic)
    }

    fun shutdown() {
        wallet?.close()
        wallet = null
    }
}
```

## Threading on iOS (Kotlin/Native)

Pre-K/N memory model (1.7-) had freezing rules. **Kotlin 2.x uses the new memory model by default** — works like JVM, no freezing required. Coroutines work normally on iOS.

```kotlin
// commonMain — works on iOS via new memory model
class WalletViewModel {
    private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())

    fun load() {
        scope.launch {
            val balance = wallet.sync()
            // update state
        }
    }
}
```

For UI updates on iOS, marshal to main:

```kotlin
import kotlinx.coroutines.Dispatchers

scope.launch(Dispatchers.Main) {
    state.value = balance
}
```

`Dispatchers.Main` resolves to iOS main runloop on iOS targets (requires `kotlinx-coroutines-core` ≥1.10).

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| `UnsatisfiedLinkError` on Android | `.so` missing for ABI | Build with cargo-ndk for required ABIs, place in `jniLibs/` |
| `library not found for -lwallet_ffi` (iOS link) | `libraryPaths` wrong in `.def` | Verify path relative to `commonNativeInterop` |
| Symbols missing on iOS arm64 sim | Built for device only | Build for `aarch64-apple-ios-sim` separately |
| `IncompatibleClassChangeError` | JNA version mismatch | Pin JNA across all source sets |
| Async hangs forever | No Tokio reactor | Ensure UDL has `[Async]`, Rust impl is `pub async fn` |
| Crash on `close()` from background | iOS thread confinement quirk | Close from main, or use `withContext(Dispatchers.Main)` |

## CI Recipe

```yaml
# GitHub Actions excerpt
- name: Setup Rust targets
  run: |
    rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
    rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
    cargo install cargo-ndk
    cargo install --git https://github.com/UbiqueInnovation/uniffi-kotlin-multiplatform-bindings \
        uniffi-bindgen-kotlin-multiplatform

- name: Build Rust for Android
  run: cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 -o shared/src/androidMain/jniLibs build --release

- name: Build Rust for iOS
  run: |
    cargo build --release --target aarch64-apple-ios
    cargo build --release --target aarch64-apple-ios-sim
    cargo build --release --target x86_64-apple-ios

- name: Generate KMP bindings
  run: |
    uniffi-bindgen-kotlin-multiplatform \
        --out-dir shared/src \
        crates/wallet-ffi/src/wallet.udl

- name: Build KMP project
  run: ./gradlew assemble
```

## Bitcoin Library Examples Using KMP UniFFI

| Library | Repo | Notes |
|---|---|---|
| **BDK** | bitcoindevkit/bdk-ffi | Full BDK 1.x KMP bindings |
| **LDK Node** | lightningdevkit/ldk-node | Full LDK Node KMP bindings |
| **Breez SDK Liquid** | breez/breez-sdk-liquid | Path A nodeless Lightning |
| **CDK** | cashubtc/cdk (cdk-ffi) | Cashu wallet primitives |
| **LWK** | Blockstream/lwk | Liquid Wallet Kit |

All follow the pattern: Rust crate (`*-ffi`) with UDL → KMP bindings published to Maven Central + Swift Package.

## When the Fork Hits Limits

If you need cutting-edge UniFFI features (latest async, advanced trait objects), the fork lags upstream by 1-2 minor versions. Workarounds:
- Pin to fork-supported UniFFI version in your Rust crate
- Use **proc-macro mode in upstream UniFFI** for JVM-only Android, generate Swift separately for iOS, hand-write KMP `expect/actual` glue
- Contribute upstream/fork to bring features in

For most production wallet code, the fork is fully sufficient.
