Rust UniFFI Cross-Platform Development: 6 Production Patterns from Core Logic to Multi-Language Bindings

编程语言

Writing the Same Logic in 3 Languages — Haven't You Lost Your Mind Yet

Your encryption module is written in Rust, rewritten in Kotlin for Android, rewritten again in Swift for iOS, and then again in Python for the server. Four codebases, four sets of bugs, four security audits. Even worse: the Kotlin AES and Swift AES behave differently — you spent three days debugging only to discover a padding mode mismatch.

In 2026, Rust UniFFI delivers the ultimate answer: write core logic once in Rust, auto-generate Kotlin/Swift/Python bindings. Built by Mozilla, Firefox and Application Services have been running it in production for three years. This isn't a toy project — it's the industrial-grade solution for cross-platform Rust.

This article starts from UDL interface definition and walks you through UDL type system → Kotlin Android integration → Swift iOS integration → Python desktop bindings → async callbacks → error handling — 6 production patterns to take Rust UniFFI cross-platform development from "it compiles" to "it's production-ready".


Key Takeaways

  • UniFFI defines interfaces via UDL files and auto-generates multi-language binding code
  • Supports Kotlin/Swift/Python/Ruby and 7+ language bindings
  • Async operations implemented via callback mechanisms, never blocking the host language main thread
  • Error handling auto-maps: Rust enums → Kotlin sealed classes / Swift enums / Python Exceptions
  • Type conversion with zero-copy: primitive types map directly, complex types bridge through FFI
  • Mobile integration: Android via JNA, iOS via static library linking

Table of Contents

  1. UniFFI Core Concepts
  2. Pattern 1: UDL Interface Definition and Type System
  3. Pattern 2: Kotlin Bindings and Android Integration
  4. Pattern 3: Swift Bindings and iOS Integration
  5. Pattern 4: Python Bindings and Desktop Applications
  6. Pattern 5: Async Operations and Callbacks
  7. Pattern 6: Error Handling and Type Conversion
  8. 5 Common Pitfalls and Solutions
  9. 10 Common Error Troubleshooting
  10. Advanced Optimization Tips
  11. Comparison: UniFFI vs JNI vs FFI vs CXX
  12. Recommended Online Tools

UniFFI Core Concepts

Concept Description
UDL UniFFI Definition Language — interface definition language describing cross-language APIs
Scaffold Rust skeleton code generated from UDL
Binding Target language binding code generated from UDL
FFI Foreign Function Interface — interop layer between Rust and host languages
Lift/Lower Type conversion: Lower (Rust→FFI), Lift (FFI→host language)
Object Cross-language shared Rust object, managed via Arc reference counting
Callback Interface implemented by host language, called back from Rust side
Record Data structure passed across languages, similar to DTO
Enum Enumeration type passed across languages
Error Error type passed across languages

UniFFI Architecture Flow

┌─────────────────────────────────────────────────────────┐
│                    UniFFI Architecture                    │
├─────────────────────────────────────────────────────────┤
│                                                           │
│  ┌─────────┐    ┌─────────┐    ┌─────────┐              │
│  │ Kotlin  │    │  Swift  │    │ Python  │              │
│  │ Binding │    │ Binding │    │ Binding │              │
│  └────┬────┘    └────┬────┘    └────┬────┘              │
│       │              │              │                     │
│       ▼              ▼              ▼                     │
│  ┌────────────────────────────────────────┐              │
│  │           FFI Bridge (C ABI)           │              │
│  │     Lower(Rust→C) / Lift(C→Host)      │              │
│  └──────────────────┬─────────────────────┘              │
│                     │                                     │
│                     ▼                                     │
│  ┌────────────────────────────────────────┐              │
│  │         Rust Core Library              │              │
│  │   ┌──────┐ ┌──────┐ ┌──────┐         │              │
│  │   │Crypto│ │ Auth │ │ Data │         │              │
│  │   └──────┘ └──────┘ └──────┘         │              │
│  └────────────────────────────────────────┘              │
│                                                           │
│  UDL ──→ uniffi-bindgen ──→ Scaffold + Bindings          │
│                                                           │
└─────────────────────────────────────────────────────────┘

Pattern 1: UDL Interface Definition and Type System

UDL is the heart of UniFFI — it defines the contract between Rust and all host languages.

Project Structure

crypto-core/
├── Cargo.toml
├── src/
│   ├── lib.rs
│   ├── crypto.rs
│   ├── auth.rs
│   └── error.rs
└── src/crypto_core.udl

Cargo.toml Configuration

[package]
name = "crypto-core"
version = "0.1.0"
edition = "2021"

[lib]
name = "crypto_core"
crate-type = ["cdylib", "staticlib", "lib"]

[dependencies]
uniffi = "0.28"
thiserror = "2.0"
sha2 = "0.10"
hmac = "0.12"
aes-gcm = "0.10"
rand = "0.8"

[build-dependencies]
uniffi = { version = "0.28", features = ["build"] }

[dev-dependencies]
uniffi = { version = "0.28", features = ["bindgen-tests"] }

UDL Interface Definition

namespace crypto_core {
    string hash_password(string password, string salt);
    string generate_token(string user_id, i64 expiry_seconds);
};

dictionary PasswordHashResult {
    string hash;
    string salt;
    i32 iterations;
};

dictionary TokenPayload {
    string user_id;
    i64 issued_at;
    i64 expires_at;
    boolean is_valid;
};

enum EncryptionAlgorithm {
    "aes-256-gcm",
    "chacha20-poly1305",
    "xchacha20-poly1305",
};

[Error]
enum CryptoError {
    InvalidKey,
    EncryptionFailed(string reason),
    DecryptionFailed(string reason),
    TokenExpired,
    InvalidToken(string reason),
};

[Throws=CryptoError]
dictionary EncryptedData {
    bytes ciphertext;
    bytes nonce;
    bytes tag;
    EncryptionAlgorithm algorithm;
};

interface CryptoService {
    [Throws=CryptoError]
    EncryptedData encrypt(bytes plaintext, bytes key, EncryptionAlgorithm algorithm);

    [Throws=CryptoError]
    bytes decrypt(EncryptedData data, bytes key);

    [Throws=CryptoError]
    PasswordHashResult hash_password_secure(string password, i32 iterations);

    [Throws=CryptoError]
    boolean verify_password(string password, PasswordHashResult stored_hash);

    [Throws=CryptoError]
    string generate_token(string user_id, i64 expiry_seconds);

    [Throws=CryptoError]
    TokenPayload validate_token(string token);
};

interface AuthService {
    constructor(string secret_key, i64 default_token_expiry);

    [Throws=CryptoError]
    string login(string user_id, string password);

    [Throws=CryptoError]
    TokenPayload verify(string token);

    void revoke_token(string token);
};

callback interface ProgressCallback {
    void on_progress(i32 current, i32 total);
    void on_complete(string result);
};

Rust Implementation

uniffi::include_scaffolding!("crypto_core");

use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
use aes_gcm::aead::Aead;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use rand::RngCore;

type HmacSha256 = Hmac<Sha256>;

#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum CryptoError {
    #[error("Invalid key")]
    InvalidKey,

    #[error("Encryption failed: {reason}")]
    EncryptionFailed { reason: String },

    #[error("Decryption failed: {reason}")]
    DecryptionFailed { reason: String },

    #[error("Token expired")]
    TokenExpired,

    #[error("Invalid token: {reason}")]
    InvalidToken { reason: String },
}

#[derive(Debug, uniffi::Record)]
pub struct PasswordHashResult {
    pub hash: String,
    pub salt: String,
    pub iterations: i32,
}

#[derive(Debug, uniffi::Record)]
pub struct TokenPayload {
    pub user_id: String,
    pub issued_at: i64,
    pub expires_at: i64,
    pub is_valid: bool,
}

#[derive(Debug, uniffi::Enum)]
pub enum EncryptionAlgorithm {
    Aes256Gcm,
    ChaCha20Poly1305,
    XChaCha20Poly1305,
}

#[derive(Debug, uniffi::Record)]
pub struct EncryptedData {
    pub ciphertext: Vec<u8>,
    pub nonce: Vec<u8>,
    pub tag: Vec<u8>,
    pub algorithm: EncryptionAlgorithm,
}

#[derive(uniffi::Object)]
pub struct CryptoService;

#[uniffi::export]
impl CryptoService {
    #[uniffi::constructor]
    pub fn new() -> Self {
        Self
    }

    pub fn encrypt(
        &self,
        plaintext: Vec<u8>,
        key: Vec<u8>,
        algorithm: EncryptionAlgorithm,
    ) -> Result<EncryptedData, CryptoError> {
        if key.len() != 32 {
            return Err(CryptoError::InvalidKey);
        }

        let mut nonce_bytes = [0u8; 12];
        rand::thread_rng().fill_bytes(&mut nonce_bytes);
        let nonce = Nonce::from_slice(&nonce_bytes);

        let cipher = Aes256Gcm::new_from_slice(&key)
            .map_err(|_| CryptoError::InvalidKey)?;

        let ciphertext = cipher
            .encrypt(nonce, plaintext.as_ref())
            .map_err(|e| CryptoError::EncryptionFailed {
                reason: e.to_string(),
            })?;

        let (encrypted, tag) = ciphertext.split_at(ciphertext.len() - 16);

        Ok(EncryptedData {
            ciphertext: encrypted.to_vec(),
            nonce: nonce_bytes.to_vec(),
            tag: tag.to_vec(),
            algorithm,
        })
    }

    pub fn decrypt(&self, data: EncryptedData, key: Vec<u8>) -> Result<Vec<u8>, CryptoError> {
        let nonce = Nonce::from_slice(&data.nonce);
        let cipher = Aes256Gcm::new_from_slice(&key)
            .map_err(|_| CryptoError::InvalidKey)?;

        let mut combined = data.ciphertext.clone();
        combined.extend_from_slice(&data.tag);

        cipher
            .decrypt(nonce, combined.as_ref())
            .map_err(|e| CryptoError::DecryptionFailed {
                reason: e.to_string(),
            })
    }

    pub fn hash_password_secure(
        &self,
        password: String,
        iterations: i32,
    ) -> Result<PasswordHashResult, CryptoError> {
        let mut salt = [0u8; 32];
        rand::thread_rng().fill_bytes(&mut salt);
        let salt_hex = hex::encode(salt);

        let mut mac = HmacSha256::new_from_slice(salt.as_ref())
            .map_err(|e| CryptoError::EncryptionFailed {
                reason: e.to_string(),
            })?;

        let effective_iterations = if iterations < 10000 { 10000 } else { iterations };

        for _ in 0..effective_iterations {
            mac.update(password.as_bytes());
        }

        let result = mac.finalize();
        let hash = hex::encode(result.into_bytes());

        Ok(PasswordHashResult {
            hash,
            salt: salt_hex,
            iterations: effective_iterations,
        })
    }

    pub fn verify_password(
        &self,
        password: String,
        stored_hash: PasswordHashResult,
    ) -> Result<bool, CryptoError> {
        let salt = hex::decode(&stored_hash.salt)
            .map_err(|e| CryptoError::InvalidToken {
                reason: format!("Invalid salt: {}", e),
            })?;

        let mut mac = HmacSha256::new_from_slice(salt.as_ref())
            .map_err(|e| CryptoError::EncryptionFailed {
                reason: e.to_string(),
            })?;

        for _ in 0..stored_hash.iterations {
            mac.update(password.as_bytes());
        }

        let result = mac.finalize();
        let hash = hex::encode(result.into_bytes());

        Ok(hash == stored_hash.hash)
    }

    pub fn generate_token(
        &self,
        user_id: String,
        expiry_seconds: i64,
    ) -> Result<String, CryptoError> {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map_err(|_| CryptoError::EncryptionFailed {
                reason: "System time error".to_string(),
            })?
            .as_secs();

        let payload = format!(
            "{}:{}:{}",
            user_id,
            now,
            now + expiry_seconds as u64
        );

        let mut mac = HmacSha256::new_from_slice(b"uniffi-token-secret")
            .map_err(|e| CryptoError::EncryptionFailed {
                reason: e.to_string(),
            })?;
        mac.update(payload.as_bytes());
        let signature = hex::encode(mac.finalize().into_bytes());

        Ok(format!("{}.{}", hex::encode(payload), signature))
    }

    pub fn validate_token(&self, token: String) -> Result<TokenPayload, CryptoError> {
        let parts: Vec<&str> = token.split('.').collect();
        if parts.len() != 2 {
            return Err(CryptoError::InvalidToken {
                reason: "Malformed token".to_string(),
            });
        }

        let payload = hex::decode(parts[0])
            .map_err(|e| CryptoError::InvalidToken {
                reason: format!("Invalid payload: {}", e),
            })?;
        let payload_str = String::from_utf8(payload)
            .map_err(|e| CryptoError::InvalidToken {
                reason: format!("Invalid UTF-8: {}", e),
            })?;

        let fields: Vec<&str> = payload_str.split(':').collect();
        if fields.len() != 3 {
            return Err(CryptoError::InvalidToken {
                reason: "Invalid payload structure".to_string(),
            });
        }

        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map_err(|_| CryptoError::TokenExpired)?
            .as_secs();

        let expires_at: u64 = fields[2]
            .parse()
            .map_err(|_| CryptoError::InvalidToken {
                reason: "Invalid expiry".to_string(),
            })?;

        if now > expires_at {
            return Err(CryptoError::TokenExpired);
        }

        Ok(TokenPayload {
            user_id: fields[0].to_string(),
            issued_at: fields[1].parse().unwrap_or(0),
            expires_at: expires_at as i64,
            is_valid: true,
        })
    }
}

#[derive(uniffi::Object)]
pub struct AuthService {
    secret_key: String,
    default_token_expiry: i64,
}

#[uniffi::export]
impl AuthService {
    #[uniffi::constructor]
    pub fn new(secret_key: String, default_token_expiry: i64) -> Self {
        Self {
            secret_key,
            default_token_expiry,
        }
    }

    pub fn login(&self, user_id: String, password: String) -> Result<String, CryptoError> {
        let crypto = CryptoService::new();
        let token = crypto.generate_token(user_id, self.default_token_expiry)?;
        Ok(token)
    }

    pub fn verify(&self, token: String) -> Result<TokenPayload, CryptoError> {
        let crypto = CryptoService::new();
        crypto.validate_token(token)
    }

    pub fn revoke_token(&self, _token: String) {
        // In production: add to revocation list / Redis cache
    }
}

#[uniffi::export(callback_interface)]
pub trait ProgressCallback: Send + Sync {
    fn on_progress(&self, current: i32, total: i32);
    fn on_complete(&self, result: String);
}

Build Script

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

Pattern 2: Kotlin Bindings and Android Integration

Generate Kotlin Bindings

cargo build --release
cargo run --features=uniffi/cli -- generate \
    --library target/release/libcrypto_core.so \
    --language kotlin \
    --out-dir generated/kotlin

Android Project Integration

android-app/
├── app/
│   ├── src/main/java/com/example/crypto/
│   │   ├── CryptoViewModel.kt
│   │   └── MainActivity.kt
│   └── jniLibs/
│       ├── arm64-v8a/libcrypto_core.so
│       ├── armeabi-v7a/libcrypto_core.so
│       └── x86_64/libcrypto_core.so
├── build.gradle.kts
└── local.properties

Kotlin Business Code

package com.example.crypto

import uniffi.crypto_core.*

class CryptoViewModel : ViewModel() {
    private val cryptoService = CryptoService()
    private var authService: AuthService? = null

    private val _encryptState = MutableStateFlow<EncryptUiState>(EncryptUiState.Idle)
    val encryptState: StateFlow<EncryptUiState> = _encryptState.asStateFlow()

    fun initializeAuth(secretKey: String, tokenExpirySeconds: Long) {
        authService = AuthService(secretKey, tokenExpirySeconds)
    }

    fun encryptData(plaintext: ByteArray, key: ByteArray) {
        viewModelScope.launch {
            _encryptState.value = EncryptUiState.Loading
            try {
                val encrypted = cryptoService.encrypt(
                    plaintext = plaintext,
                    key = key,
                    algorithm = EncryptionAlgorithm.AES_256_GCM
                )
                _encryptState.value = EncryptUiState.Success(encrypted)
            } catch (e: CryptoError) {
                _encryptState.value = EncryptUiState.Error(
                    when (e) {
                        is CryptoError.InvalidKey -> "Invalid encryption key"
                        is CryptoError.EncryptionFailed -> "Encryption failed: ${e.reason}"
                        else -> "Unknown error"
                    }
                )
            }
        }
    }

    fun decryptData(encryptedData: EncryptedData, key: ByteArray): ByteArray {
        return try {
            cryptoService.decrypt(encryptedData, key)
        } catch (e: CryptoError.DecryptionFailed) {
            throw CryptoException("Decryption failed: ${e.reason}")
        }
    }

    fun hashPassword(password: String, iterations: Int = 100000): PasswordHashResult {
        return cryptoService.hashPasswordSecure(password, iterations.toLong().toInt())
    }

    fun verifyPassword(password: String, storedHash: PasswordHashResult): Boolean {
        return cryptoService.verifyPassword(password, storedHash)
    }

    fun login(userId: String, password: String): String {
        return authService?.login(userId, password)
            ?: throw IllegalStateException("AuthService not initialized")
    }

    fun verifyToken(token: String): TokenPayload {
        return authService?.verify(token)
            ?: throw IllegalStateException("AuthService not initialized")
    }
}

sealed class EncryptUiState {
    object Idle : EncryptUiState()
    object Loading : EncryptUiState()
    data class Success(val encryptedData: EncryptedData) : EncryptUiState()
    data class Error(val message: String) : EncryptUiState()
}

class CryptoException(message: String) : Exception(message)

Android Activity Integration

package com.example.crypto

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import uniffi.crypto_core.*
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {
    private val viewModel = CryptoViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        viewModel.initializeAuth(
            secretKey = "my-secret-key-2026",
            tokenExpirySeconds = 86400L
        )

        lifecycleScope.launch {
            viewModel.encryptState.collect { state ->
                when (state) {
                    is EncryptUiState.Success -> {
                        val base64Ciphertext = android.util.Base64.encodeToString(
                            state.encryptedData.ciphertext.toByteArray(),
                            android.util.Base64.NO_WRAP
                        )
                        println("Encrypted: $base64Ciphertext")
                    }
                    is EncryptUiState.Error -> {
                        println("Error: ${state.message}")
                    }
                    else -> {}
                }
            }
        }

        demonstrateCrypto()
    }

    private fun demonstrateCrypto() {
        val key = ByteArray(32) { (it % 256).toByte() }
        val plaintext = "Hello, UniFFI Cross-Platform!".toByteArray(Charsets.UTF_8)

        viewModel.encryptData(plaintext, key)

        val hashResult = viewModel.hashPassword("user_password_123")
        println("Hash: ${hashResult.hash}")
        println("Salt: ${hashResult.salt}")

        val isValid = viewModel.verifyPassword("user_password_123", hashResult)
        println("Password valid: $isValid")

        val token = viewModel.login("user_001", "password")
        println("Token: $token")

        val payload = viewModel.verifyToken(token)
        println("Token user: ${payload.userId}, valid: ${payload.isValid}")
    }
}

Gradle Build Configuration

android {
    namespace = "com.example.crypto"
    compileSdk = 36

    defaultConfig {
        applicationId = "com.example.crypto"
        minSdk = 26
        targetSdk = 36
        ndk {
            abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
        }
    }

    sourceSets["main"].jniLibs.srcDirs("jniLibs")
}

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

Pattern 3: Swift Bindings and iOS Integration

Generate Swift Bindings

cargo build --target aarch64-apple-ios --release
cargo run --features=uniffi/cli -- generate \
    --library target/aarch64-apple-ios/release/libcrypto_core.a \
    --language swift \
    --out-dir generated/swift

iOS Project Structure

ios-app/
├── CryptoApp/
│   ├── CryptoViewModel.swift
│   ├── ContentView.swift
│   └── CryptoApp.swift
├── CryptoCore.xcframework/
│   ├── ios-arm64/
│   └── ios-arm64_x86_64-simulator/
└── Podfile

Swift Business Code

import Foundation
import CryptoCore

@MainActor
class CryptoViewModel: ObservableObject {
    private let cryptoService = CryptoService()
    private var authService: AuthService?

    @Published var encryptedData: EncryptedData?
    @Published var errorMessage: String?
    @Published var isLoading = false
    @Published var tokenPayload: TokenPayload?

    func initializeAuth(secretKey: String, tokenExpirySeconds: Int64) {
        authService = AuthService(secretKey: secretKey, defaultTokenExpiry: tokenExpirySeconds)
    }

    func encryptData(plaintext: Data, key: Data) {
        isLoading = true
        errorMessage = nil

        do {
            let encrypted = try cryptoService.encrypt(
                plaintext: plaintext,
                key: key,
                algorithm: .aes256Gcm
            )
            encryptedData = encrypted
            isLoading = false
        } catch let error as CryptoError {
            switch error {
            case .invalidKey:
                errorMessage = "Invalid encryption key"
            case .encryptionFailed(let reason):
                errorMessage = "Encryption failed: \(reason)"
            default:
                errorMessage = "Unknown error"
            }
            isLoading = false
        } catch {
            errorMessage = error.localizedDescription
            isLoading = false
        }
    }

    func decryptData(encryptedData: EncryptedData, key: Data) -> Data? {
        do {
            return try cryptoService.decrypt(data: encryptedData, key: key)
        } catch let error as CryptoError {
            if case .decryptionFailed(let reason) = error {
                errorMessage = "Decryption failed: \(reason)"
            }
            return nil
        } catch {
            errorMessage = error.localizedDescription
            return nil
        }
    }

    func hashPassword(password: String, iterations: Int32 = 100000) -> PasswordHashResult? {
        do {
            return try cryptoService.hashPasswordSecure(
                password: password,
                iterations: iterations
            )
        } catch {
            errorMessage = error.localizedDescription
            return nil
        }
    }

    func verifyPassword(password: String, storedHash: PasswordHashResult) -> Bool {
        do {
            return try cryptoService.verifyPassword(password: password, storedHash: storedHash)
        } catch {
            return false
        }
    }

    func login(userId: String, password: String) -> String? {
        do {
            return try authService?.login(userId: userId, password: password)
        } catch {
            errorMessage = error.localizedDescription
            return nil
        }
    }

    func verifyToken(token: String) {
        do {
            tokenPayload = try authService?.verify(token: token)
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}

SwiftUI View

import SwiftUI
import CryptoCore

struct ContentView: View {
    @StateObject private var viewModel = CryptoViewModel()

    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                if viewModel.isLoading {
                    ProgressView("Encrypting...")
                }

                if let error = viewModel.errorMessage {
                    Text(error)
                        .foregroundColor(.red)
                        .font(.caption)
                }

                Button("Encrypt Demo Data") {
                    let key = Data((0..<32).map { UInt8($0 % 256) })
                    let plaintext = "Hello, UniFFI iOS!".data(using: .utf8)!
                    viewModel.encryptData(plaintext: plaintext, key: key)
                }
                .buttonStyle(.borderedProminent)

                if let encrypted = viewModel.encryptedData {
                    VStack(alignment: .leading) {
                        Text("Ciphertext (Base64):")
                            .font(.caption)
                            .foregroundColor(.secondary)
                        Text(encrypted.ciphertext.base64EncodedString())
                            .font(.system(.caption, design: .monospaced))
                            .lineLimit(3)
                    }
                    .padding()
                    .background(Color(.systemGray6))
                    .cornerRadius(8)
                }

                Button("Login Demo") {
                    viewModel.initializeAuth(
                        secretKey: "my-secret-key-2026",
                        tokenExpirySeconds: 86400
                    )
                    if let token = viewModel.login(userId: "user_001", password: "pass") {
                        viewModel.verifyToken(token: token)
                    }
                }
                .buttonStyle(.bordered)

                if let payload = viewModel.tokenPayload {
                    VStack(alignment: .leading) {
                        Text("Token User: \(payload.userId)")
                        Text("Valid: \(payload.isValid ? "Yes" : "No")")
                        Text("Expires: \(Date(timeIntervalSince1970: TimeInterval(payload.expiresAt)).formatted())")
                    }
                    .font(.caption)
                    .padding()
                    .background(Color.green.opacity(0.1))
                    .cornerRadius(8)
                }
            }
            .padding()
            .navigationTitle("CryptoCore Demo")
        }
    }
}

XCFramework Build Script

#!/bin/bash
set -e

RUST_TARGETS=("aarch64-apple-ios" "aarch64-apple-ios-sim" "x86_64-apple-ios")
LIB_NAME="libcrypto_core.a"

for target in "${RUST_TARGETS[@]}"; do
    rustup target add "$target"
    cargo build --target "$target" --release
done

mkdir -p build/xcframework

xcodebuild -create-xcframework \
    -library "target/aarch64-apple-ios/release/$LIB_NAME" \
    -headers generated/swift \
    -library "target/aarch64-apple-ios-sim/release/$LIB_NAME" \
    -headers generated/swift \
    -output "build/xcframework/CryptoCore.xcframework"

Pattern 4: Python Bindings and Desktop Applications

Generate Python Bindings

cargo build --release
cargo run --features=uniffi/cli -- generate \
    --library target/release/libcrypto_core.so \
    --language python \
    --out-dir generated/python

Python Business Code

from generated.python import crypto_core
import base64
import os


class CryptoManager:
    def __init__(self, secret_key: str = "my-secret-key-2026", token_expiry: int = 86400):
        self.crypto_service = crypto_core.CryptoService()
        self.auth_service = crypto_core.AuthService(secret_key, token_expiry)

    def encrypt_data(self, plaintext: str, key: bytes = None) -> crypto_core.EncryptedData:
        if key is None:
            key = os.urandom(32)

        if len(key) != 32:
            raise ValueError("Key must be 32 bytes for AES-256")

        plaintext_bytes = plaintext.encode("utf-8")
        encrypted = self.crypto_service.encrypt(
            plaintext=plaintext_bytes,
            key=key,
            algorithm=crypto_core.EncryptionAlgorithm.AES_256_GCM,
        )

        print(f"Ciphertext (Base64): {base64.b64encode(encrypted.ciphertext).decode()}")
        print(f"Nonce (Base64): {base64.b64encode(encrypted.nonce).decode()}")
        print(f"Algorithm: {encrypted.algorithm}")

        return encrypted

    def decrypt_data(self, encrypted: crypto_core.EncryptedData, key: bytes) -> str:
        plaintext_bytes = self.crypto_service.decrypt(encrypted, key)
        return plaintext_bytes.decode("utf-8")

    def hash_password(self, password: str, iterations: int = 100000) -> crypto_core.PasswordHashResult:
        result = self.crypto_service.hash_password_secure(password, iterations)
        print(f"Hash: {result.hash}")
        print(f"Salt: {result.salt}")
        print(f"Iterations: {result.iterations}")
        return result

    def verify_password(self, password: str, stored_hash: crypto_core.PasswordHashResult) -> bool:
        return self.crypto_service.verify_password(password, stored_hash)

    def login(self, user_id: str, password: str) -> str:
        token = self.auth_service.login(user_id, password)
        print(f"Token: {token}")
        return token

    def verify_token(self, token: str) -> crypto_core.TokenPayload:
        payload = self.auth_service.verify(token)
        print(f"User: {payload.user_id}")
        print(f"Valid: {payload.is_valid}")
        print(f"Expires: {payload.expires_at}")
        return payload


def main():
    manager = CryptoManager()

    key = os.urandom(32)
    encrypted = manager.encrypt_data("Hello, UniFFI Python!", key)

    decrypted = manager.decrypt_data(encrypted, key)
    print(f"Decrypted: {decrypted}")

    hash_result = manager.hash_password("my_secure_password")
    is_valid = manager.verify_password("my_secure_password", hash_result)
    print(f"Password valid: {is_valid}")

    is_invalid = manager.verify_password("wrong_password", hash_result)
    print(f"Wrong password valid: {is_invalid}")

    token = manager.login("user_001", "password123")
    payload = manager.verify_token(token)

    try:
        manager.encrypt_data("test", key=b"short")
    except crypto_core.CryptoError.InvalidKey:
        print("Caught: InvalidKey error as expected")


if __name__ == "__main__":
    main()

Python Async Batch Processing

import asyncio
from concurrent.futures import ThreadPoolExecutor
from generated.python import crypto_core


class AsyncCryptoManager:
    def __init__(self, worker_count: int = 4):
        self.crypto_service = crypto_core.CryptoService()
        self.executor = ThreadPoolExecutor(max_workers=worker_count)

    async def encrypt_batch(self, items: list[tuple[str, bytes]]) -> list[crypto_core.EncryptedData]:
        loop = asyncio.get_event_loop()
        tasks = []
        for plaintext, key in items:
            task = loop.run_in_executor(
                self.executor,
                self._encrypt_sync,
                plaintext.encode("utf-8"),
                key,
            )
            tasks.append(task)
        return await asyncio.gather(*tasks)

    def _encrypt_sync(self, plaintext: bytes, key: bytes) -> crypto_core.EncryptedData:
        return self.crypto_service.encrypt(
            plaintext=plaintext,
            key=key,
            algorithm=crypto_core.EncryptionAlgorithm.AES_256_GCM,
        )

    async def hash_passwords_batch(self, passwords: list[str], iterations: int = 100000) -> list[crypto_core.PasswordHashResult]:
        loop = asyncio.get_event_loop()
        tasks = []
        for password in passwords:
            task = loop.run_in_executor(
                self.executor,
                self.crypto_service.hash_password_secure,
                password,
                iterations,
            )
            tasks.append(task)
        return await asyncio.gather(*tasks)

    def shutdown(self):
        self.executor.shutdown(wait=True)

Pattern 5: Async Operations and Callbacks

UniFFI's async support is implemented through callback interfaces, avoiding blocking the host language's main thread.

Rust Async Tasks

use std::sync::Arc;
use std::time::Duration;

#[derive(uniffi::Object)]
pub struct BatchProcessor {
    crypto: Arc<CryptoService>,
}

#[uniffi::export]
impl BatchProcessor {
    #[uniffi::constructor]
    pub fn new() -> Self {
        Self {
            crypto: Arc::new(CryptoService::new()),
        }
    }

    pub fn process_batch(
        &self,
        items: Vec<Vec<u8>>,
        key: Vec<u8>,
        callback: Box<dyn ProgressCallback>,
    ) -> Result<Vec<EncryptedData>, CryptoError> {
        let total = items.len();
        let mut results = Vec::with_capacity(total);

        for (index, plaintext) in items.iter().enumerate() {
            let encrypted = self.crypto.encrypt(
                plaintext.clone(),
                key.clone(),
                EncryptionAlgorithm::Aes256Gcm,
            )?;

            results.push(encrypted);
            callback.on_progress((index + 1) as i32, total as i32);

            if index > 0 && index % 100 == 0 {
                std::thread::sleep(Duration::from_millis(10));
            }
        }

        callback.on_complete(format!("Processed {} items", total));
        Ok(results)
    }

    pub fn benchmark_hash(
        &self,
        password: String,
        iterations: i32,
        rounds: i32,
        callback: Box<dyn ProgressCallback>,
    ) -> Result<String, CryptoError> {
        let mut results = Vec::new();

        for i in 0..rounds {
            let start = std::time::Instant::now();
            self.crypto.hash_password_secure(password.clone(), iterations)?;
            let elapsed = start.elapsed().as_millis();
            results.push(elapsed);

            callback.on_progress(i + 1, rounds);
        }

        let avg: f64 = results.iter().sum::<u128>() as f64 / results.len() as f64;
        let summary = format!(
            "Average: {:.2}ms, Min: {}ms, Max: {}ms",
            avg,
            results.iter().min().unwrap_or(&0),
            results.iter().max().unwrap_or(&0),
        );

        callback.on_complete(summary.clone());
        Ok(summary)
    }
}

Kotlin Callback Implementation

class BatchEncryptCallback : ProgressCallback {
    override fun onProgress(current: Int, total: Int) {
        val percent = (current * 100) / total
        println("Progress: $percent% ($current/$total)")
    }

    override fun onComplete(result: String) {
        println("Complete: $result")
    }
}

fun performBatchEncryption() {
    val processor = BatchProcessor()
    val key = ByteArray(32) { (it % 256).toByte() }
    val items = (1..500).map { "Item $it data payload".toByteArray() }

    CoroutineScope(Dispatchers.IO).launch {
        try {
            val results = processor.processBatch(items, key, BatchEncryptCallback())
            println("Encrypted ${results.size} items")
        } catch (e: CryptoError) {
            println("Batch error: $e")
        }
    }
}

Swift Callback Implementation

class BatchEncryptCallback: ProgressCallback {
    func onProgress(current: Int32, total: Int32) {
        let percent = Int(current) * 100 / Int(total)
        print("Progress: \(percent)% (\(current)/\(total))")
    }

    func onComplete(result: String) {
        print("Complete: \(result)")
    }
}

func performBatchEncryption() {
    let processor = BatchProcessor()
    let key = Data((0..<32).map { UInt8($0 % 256) })
    let items: [Data] = (1...500).map { "Item \($0) data payload".data(using: .utf8)! }

    DispatchQueue.global(qos: .userInitiated).async {
        do {
            let results = try processor.processBatch(
                items: items,
                key: key,
                callback: BatchEncryptCallback()
            )
            print("Encrypted \(results.count) items")
        } catch {
            print("Batch error: \(error)")
        }
    }
}

Python Callback Implementation

class BatchEncryptCallback(crypto_core.ProgressCallback):
    def on_progress(self, current: int, total: int):
        percent = (current * 100) // total
        print(f"Progress: {percent}% ({current}/{total})")

    def on_complete(self, result: str):
        print(f"Complete: {result}")


def perform_batch_encryption():
    processor = crypto_core.BatchProcessor()
    key = os.urandom(32)
    items = [f"Item {i} data payload".encode("utf-8") for i in range(1, 501)]

    results = processor.process_batch(items, key, BatchEncryptCallback())
    print(f"Encrypted {len(results)} items")

Pattern 6: Error Handling and Type Conversion

Type Mapping Table

Rust Type Kotlin Type Swift Type Python Type
i32 Int Int32 int
i64 Long Int64 int
f64 Double Double float
bool Boolean Bool bool
String String String str
Vec ByteArray Data bytes
Vec List [T] list[T]
Option T? T? Optional[T]
Result<T, E> T (throws E) T (throws E) T (raises E)
enum sealed class enum Enum
struct (Record) data class struct dataclass

Rust Error Enum

#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum DataStoreError {
    #[error("Connection failed: {reason}")]
    ConnectionFailed { reason: String },

    #[error("Record not found: {id}")]
    NotFound { id: String },

    #[error("Validation error: {field} - {message}")]
    ValidationError { field: String, message: String },

    #[error("Permission denied")]
    PermissionDenied,

    #[error("Rate limit exceeded, retry after {seconds}s")]
    RateLimitExceeded { seconds: i64 },
}

#[derive(Debug, uniffi::Record)]
pub struct UserRecord {
    pub id: String,
    pub name: String,
    pub email: String,
    pub role: UserRole,
    pub created_at: i64,
    pub updated_at: Option<i64>,
}

#[derive(Debug, uniffi::Enum)]
pub enum UserRole {
    Admin,
    Editor,
    Viewer,
}

#[derive(uniffi::Object)]
pub struct DataStore {
    connection_string: String,
}

#[uniffi::export]
impl DataStore {
    #[uniffi::constructor]
    pub fn new(connection_string: String) -> Result<Self, DataStoreError> {
        if connection_string.is_empty() {
            return Err(DataStoreError::ConnectionFailed {
                reason: "Empty connection string".to_string(),
            });
        }
        Ok(Self { connection_string })
    }

    pub fn get_user(&self, id: String) -> Result<UserRecord, DataStoreError> {
        if id.is_empty() {
            return Err(DataStoreError::ValidationError {
                field: "id".to_string(),
                message: "ID cannot be empty".to_string(),
            });
        }

        Ok(UserRecord {
            id: id.clone(),
            name: format!("User {}", id),
            email: format!("user{}@example.com", id),
            role: UserRole::Viewer,
            created_at: 1718500000,
            updated_at: None,
        })
    }

    pub fn update_user(&self, id: String, name: String, email: String) -> Result<UserRecord, DataStoreError> {
        if name.is_empty() {
            return Err(DataStoreError::ValidationError {
                field: "name".to_string(),
                message: "Name cannot be empty".to_string(),
            });
        }

        if !email.contains('@') {
            return Err(DataStoreError::ValidationError {
                field: "email".to_string(),
                message: "Invalid email format".to_string(),
            });
        }

        Ok(UserRecord {
            id,
            name,
            email,
            role: UserRole::Editor,
            created_at: 1718500000,
            updated_at: Some(1718600000),
        })
    }
}

Kotlin Error Handling

fun handleDataStoreOperations() {
    val store = try {
        DataStore("postgresql://localhost/mydb")
    } catch (e: DataStoreError.ConnectionFailed) {
        println("Connection failed: ${e.reason}")
        return
    }

    try {
        val user = store.getUser("user_001")
        println("User: ${user.name}, Role: ${user.role}")
    } catch (e: DataStoreError.NotFound) {
        println("User not found: ${e.id}")
    } catch (e: DataStoreError.ValidationError) {
        println("Validation: ${e.field} - ${e.message}")
    }

    try {
        val updated = store.updateUser("user_001", "New Name", "new@example.com")
        println("Updated: ${updated.name}")
    } catch (e: DataStoreError) {
        when (e) {
            is DataStoreError.ValidationError -> println("Invalid: ${e.field}")
            is DataStoreError.PermissionDenied -> println("No permission")
            is DataStoreError.RateLimitExceeded -> println("Retry after ${e.seconds}s")
            else -> println("Error: $e")
        }
    }
}

Swift Error Handling

func handleDataStoreOperations() {
    let store: DataStore
    do {
        store = try DataStore(connectionString: "postgresql://localhost/mydb")
    } catch let error as DataStoreError.ConnectionFailed {
        print("Connection failed: \(error.reason)")
        return
    } catch {
        print("Unexpected error: \(error)")
        return
    }

    do {
        let user = try store.getUser(id: "user_001")
        print("User: \(user.name), Role: \(user.role)")
    } catch let error as DataStoreError.NotFound {
        print("User not found: \(error.id)")
    } catch let error as DataStoreError.ValidationError {
        print("Validation: \(error.field) - \(error.message)")
    }

    do {
        let updated = try store.updateUser(id: "user_001", name: "New Name", email: "new@example.com")
        print("Updated: \(updated.name)")
    } catch let error as DataStoreError {
        switch error {
        case .validationError(let field, let message):
            print("Invalid: \(field) - \(message)")
        case .permissionDenied:
            print("No permission")
        case .rateLimitExceeded(let seconds):
            print("Retry after \(seconds)s")
        default:
            print("Error: \(error)")
        }
    }
}

5 Common Pitfalls and Solutions

Pitfall 1: UDL and Rust Code Out of Sync

// ❌ Wrong: UDL defines 3 fields, Rust struct has only 2
// UDL: dictionary User { string id; string name; string email; }
// Rust:
#[derive(uniffi::Record)]
pub struct User {
    pub id: String,
    pub name: String,
    // Missing email field!
}

// ✅ Correct: Use proc-macro instead of UDL, let Rust code be the interface
// No need to maintain a separate UDL file
#[derive(uniffi::Record)]
pub struct User {
    pub id: String,
    pub name: String,
    pub email: String,
}

Pitfall 2: Android Dynamic Library Loading Failure

// ❌ Wrong: Using System.loadLibrary directly
System.loadLibrary("crypto_core") // Library not found

// ✅ Correct: UniFFI uses JNA, ensure JNA dependency is correct
// build.gradle.kts:
dependencies {
    implementation("net.java.dev.jna:jna:5.16.0@aar")
}
// Ensure .so files are in the correct jniLibs directory
// jniLibs/arm64-v8a/libcrypto_core.so
# ❌ Wrong: Link order causes undefined symbols
# Other Linker Flags: -lcrypto_core

# ✅ Correct: Ensure link order and framework dependencies are correct
# Other Linker Flags:
#   -lcrypto_core
#   -framework Security
#   -framework Foundation
# Add XCFramework path to Framework Search Paths

Pitfall 4: Callbacks Blocking the Main Thread

// ❌ Wrong: Calling expensive Rust function on main thread
fun onButtonClick() {
    val result = cryptoService.hashPasswordSecure("password", 100000)
    // ANR! Main thread blocked
}

// ✅ Correct: Execute on IO thread
fun onButtonClick() {
    CoroutineScope(Dispatchers.IO).launch {
        val result = cryptoService.hashPasswordSecure("password", 100000)
        withContext(Dispatchers.Main) {
            updateUI(result)
        }
    }
}

Pitfall 5: Vec and String Encoding Issues

# ❌ Wrong: Python passes str directly to a bytes parameter
encrypted = crypto_service.encrypt(
    plaintext="Hello",  # Type error! Expected bytes
    key=key_bytes,
    algorithm=crypto_core.EncryptionAlgorithm.AES_256_GCM,
)

# ✅ Correct: Explicitly encode
encrypted = crypto_service.encrypt(
    plaintext="Hello".encode("utf-8"),  # bytes
    key=key_bytes,
    algorithm=crypto_core.EncryptionAlgorithm.AES_256_GCM,
)

10 Common Error Troubleshooting

# Error Message Cause Solution
1 failed to load library: libcrypto_core.so not found Android .so files not placed correctly Check jniLibs directory structure and ABI filters
2 UniffiInternalError: pointer is null Rust returned None/empty object Check if Rust constructor returns Ok value
3 TypeMismatch: expected bytes, got str Python passed str to bytes parameter Use .encode("utf-8") to convert
4 Undefined symbol: _uniffi_crypto_core_fn_init iOS link order error Check Other Linker Flags and link order
5 UDL parse error: unknown type UDL references undefined type Ensure all types are defined in UDL before use
6 cargo build error: duplicate lang item UniFFI version conflict Unify UniFFI dependency versions in Cargo.toml
7 JNA: java.lang.UnsatisfiedLinkError JNA version incompatible with .so file Update JNA version, ensure match with compile target
8 Callback dropped: on_progress not called Callback object garbage collected Keep callback object reference in Kotlin/Swift
9 cannot find type CryptoError in scope Swift binding module not imported Add import CryptoCore
10 thread 'main' panicked at 'assertion failed: key.len() == 32' Key length mismatch Ensure AES-256 uses 32-byte key

Advanced Optimization Tips

1. Using Proc-Macros Instead of UDL

// No separate UDL file needed, define interfaces directly with Rust attribute macros
use uniffi;

#[derive(uniffi::Record)]
pub struct AppConfig {
    pub api_endpoint: String,
    pub timeout_seconds: i64,
    pub max_retries: i32,
    pub enable_logging: bool,
}

#[derive(uniffi::Enum)]
pub enum ConnectionStatus {
    Connected,
    Disconnected { reason: String },
    Reconnecting { attempt: i32 },
}

#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum NetworkError {
    #[error("Timeout after {seconds}s")]
    Timeout { seconds: i64 },
    #[error("Connection refused: {host}")]
    ConnectionRefused { host: String },
}

#[derive(uniffi::Object)]
pub struct NetworkClient {
    config: AppConfig,
    status: std::sync::Mutex<ConnectionStatus>,
}

#[uniffi::export]
impl NetworkClient {
    #[uniffi::constructor]
    pub fn new(config: AppConfig) -> Result<Self, NetworkError> {
        if config.api_endpoint.is_empty() {
            return Err(NetworkError::ConnectionRefused {
                host: "empty endpoint".to_string(),
            });
        }
        Ok(Self {
            config,
            status: std::sync::Mutex::new(ConnectionStatus::Disconnected {
                reason: "Not initialized".to_string(),
            }),
        })
    }

    pub fn connect(&self) -> Result<(), NetworkError> {
        let mut status = self.status.lock().unwrap();
        *status = ConnectionStatus::Connected;
        Ok(())
    }

    pub fn get_status(&self) -> ConnectionStatus {
        self.status.lock().unwrap().clone()
    }
}

2. Cross-Platform CI/CD Build

name: Build Cross-Platform Bindings

on: [push, pull_request]

jobs:
  build-android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
      - name: Install cargo-ndk
        run: cargo install cargo-ndk
      - name: Build Android
        run: cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 build --release
      - name: Generate Kotlin bindings
        run: cargo run --features=uniffi/cli -- generate --library target/aarch64-linux-android/release/libcrypto_core.so --language kotlin --out-dir dist/kotlin
      - uses: actions/upload-artifact@v4
        with:
          name: android-bindings
          path: dist/

  build-ios:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: aarch64-apple-ios,x86_64-apple-ios,aarch64-apple-ios-sim
      - name: Build iOS
        run: |
          cargo build --target aarch64-apple-ios --release
          cargo build --target aarch64-apple-ios-sim --release
      - name: Generate Swift bindings
        run: cargo run --features=uniffi/cli -- generate --library target/aarch64-apple-ios/release/libcrypto_core.a --language swift --out-dir dist/swift
      - name: Create XCFramework
        run: |
          xcodebuild -create-xcframework \
            -library target/aarch64-apple-ios/release/libcrypto_core.a \
            -headers dist/swift \
            -library target/aarch64-apple-ios-sim/release/libcrypto_core.a \
            -headers dist/swift \
            -output dist/CryptoCore.xcframework
      - uses: actions/upload-artifact@v4
        with:
          name: ios-bindings
          path: dist/

  build-python:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - name: Build and generate Python bindings
        run: |
          cargo build --release
          cargo run --features=uniffi/cli -- generate --library target/release/libcrypto_core.so --language python --out-dir dist/python
      - uses: actions/upload-artifact@v4
        with:
          name: python-bindings
          path: dist/

3. Object Lifecycle Management

use std::sync::{Arc, Mutex};

#[derive(uniffi::Object)]
pub struct SessionManager {
    sessions: Mutex<std::collections::HashMap<String, Arc<UserSession>>>,
    max_sessions: usize,
}

#[derive(uniffi::Object)]
pub struct UserSession {
    pub id: String,
    pub user_id: String,
    created_at: i64,
    last_active: Mutex<i64>,
}

#[uniffi::export]
impl SessionManager {
    #[uniffi::constructor]
    pub fn new(max_sessions: i32) -> Self {
        Self {
            sessions: Mutex::new(std::collections::HashMap::new()),
            max_sessions: max_sessions as usize,
        }
    }

    pub fn create_session(&self, user_id: String) -> Result<Arc<UserSession>, CryptoError> {
        let mut sessions = self.sessions.lock().unwrap();

        if sessions.len() >= self.max_sessions {
            let oldest = sessions
                .iter()
                .min_by_key(|(_, s)| s.last_active.lock().unwrap().clone())
                .map(|k| k.0.clone());
            if let Some(key) = oldest {
                sessions.remove(&key);
            }
        }

        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_secs() as i64;

        let session = Arc::new(UserSession {
            id: uuid::Uuid::new_v4().to_string(),
            user_id,
            created_at: now,
            last_active: Mutex::new(now),
        });

        sessions.insert(session.id.clone(), Arc::clone(&session));
        Ok(session)
    }

    pub fn get_session(&self, id: String) -> Option<Arc<UserSession>> {
        let sessions = self.sessions.lock().unwrap();
        sessions.get(&id).cloned()
    }

    pub fn active_count(&self) -> i32 {
        self.sessions.lock().unwrap().len() as i32
    }
}

Comparison: UniFFI vs JNI vs FFI vs CXX

Dimension UniFFI JNI Raw FFI CXX
Target Languages Kotlin/Swift/Python/Ruby 7+ Java/Kotlin only Any C ABI language C++
Interface Definition UDL or proc-macro Java native methods C header + extern cxx::bridge macro
Code Generation Auto-generate bindings Manual or javah Manual Auto-generate
Type Safety Compile-time check Runtime Runtime Compile-time check
Async Support Callback interfaces Manual management Manual management Limited support
Error Mapping Auto-map to host exceptions Manual checking Manual checking Result mapping
Learning Curve ⭐ Medium ⭐ Steep ⭐ Very Steep ⭐ Medium
Mobile Support Android + iOS Android only Requires extra wrapping Requires extra wrapping
Maintenance Cost ⭐ Low ⭐ High ⭐ Very High ⭐ Medium
Production Validation Firefox/Application Services Android ecosystem Widespread Chromium/Fuchsia
Community Activity ⭐ High ⭐ Very High ⭐ Medium ⭐ High

Selection Guide

Decision Flow:
1. Only need Java/Kotlin bindings? → JNI (mature but verbose)
2. Need multi-language bindings? → UniFFI (first choice)
3. Need C++ interop? → CXX
4. Need ultimate control? → Raw FFI (not recommended unless necessary)
5. Mobile cross-platform? → UniFFI (Android + iOS one-stop solution)



Summary: The core value of Rust UniFFI cross-platform development is Write Once, Bind Everywhere — write core logic in Rust, auto-generate Kotlin/Swift/Python bindings. 2026 production practices: use proc-macros instead of UDL to simplify maintenance → Kotlin JNA for Android integration → XCFramework for iOS integration → ThreadPoolExecutor for Python async → callback interfaces to avoid main thread blocking → unified error mapping for consistency. The key is understanding the Lift/Lower type conversion mechanism — this is the foundation of UniFFI's cross-language communication. Mozilla Firefox has proven this path works, and your project can too.

Try these browser-local tools — no sign-up required →

#Rust#UniFFI#跨平台#多语言绑定#Kotlin#Swift#2026#编程语言