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
- UniFFI Core Concepts
- Pattern 1: UDL Interface Definition and Type System
- Pattern 2: Kotlin Bindings and Android Integration
- Pattern 3: Swift Bindings and iOS Integration
- Pattern 4: Python Bindings and Desktop Applications
- Pattern 5: Async Operations and Callbacks
- Pattern 6: Error Handling and Type Conversion
- 5 Common Pitfalls and Solutions
- 10 Common Error Troubleshooting
- Advanced Optimization Tips
- Comparison: UniFFI vs JNI vs FFI vs CXX
- 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
Pitfall 3: iOS Static Library Link Order
# ❌ 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)
Recommended Online Tools
- JSON Formatter: /en/json/format — Debug UniFFI-generated JSON configurations
- Base64 Encode/Decode: /en/encode/base64 — Handle Base64 encoding of encrypted data
- Code Formatter: /en/dev/code-formatter — Format generated binding code
Related Reading
- Rust Axum Web Framework: 5 Production-Grade Patterns from Route Design to Middleware — Production Axum framework practices
- Rust Embedded Linux Development: Complete Guide from Cross-Compilation to Deployment — Rust in embedded scenarios
- Rust Tokio Channel Patterns: 5 Production-Grade Concurrent Communication Solutions — Tokio async channel in practice
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 →