Same functionality. Same runtime behavior. One is ~40 lines. The other is ~400+.
#[derive(uniffi::Object)]
pub struct WalletManager {
service: WalletService,
}
#[uniffi::export(async_runtime = "tokio")]
impl WalletManager {
#[uniffi::constructor]
pub fn new(mnemonic: String) -> Result<Self, WalletError> {
let service = WalletService::new(&mnemonic)?;
Ok(Self { service })
}
// Sync method
pub fn generate_address(
&self,
network: Network,
) -> Result<String, WalletError> {
self.service.derive_address(network)
}
// Async method
pub async fn get_balance(&self) -> Result<Vec<Balance>, WalletError> {
self.service.fetch_balances().await
}
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum WalletError {
#[error("Invalid mnemonic: {0}")]
InvalidMnemonic(String),
#[error("Network error: {0}")]
NetworkError(String),
}// All of this is generated by UniFFI ✅
let wallet = try WalletManager(mnemonic: "senior awful soon ...")
// Sync - normal call
let address = try wallet.generateAddress(network: .bitcoin)
// Async - native Swift async/await
let balances = try await wallet.getBalance()
// Errors - typed Swift exceptions
do {
let addr = try wallet.generateAddress(network: .solana)
} catch WalletError.InvalidMnemonic(let msg) {
print("Bad mnemonic: \(msg)")
} catch WalletError.NetworkError(let msg) {
print("Network failed: \(msg)")
}
// Memory - automatic. No manual free.
// wallet is dropped when Swift ARC releases it.// All of this is generated by UniFFI ✅
val wallet = WalletManager("senior awful soon ...")
// Sync - normal call
val address = wallet.generateAddress(network = Network.BITCOIN)
// Async - native Kotlin coroutine
val balances = wallet.getBalance() // suspend function
// Errors - typed Kotlin exceptions
try {
val addr = wallet.generateAddress(network = Network.SOLANA)
} catch (e: WalletError.InvalidMnemonic) {
println("Bad mnemonic: ${e.message}")
} catch (e: WalletError.NetworkError) {
println("Network failed: ${e.message}")
}
// Memory - automatic via Cleaner / pointers handled by generated code.UniFFI total: 1 Rust file, ~40 lines, 0 unsafe, 0 manual free, native async/await on both platforms
// wallet_ffi.h
typedef void* WalletHandle;
// Constructor / destructor
WalletHandle wallet_new(const char* mnemonic, int32_t* out_error);
void wallet_free(WalletHandle handle);
// Sync method
char* wallet_generate_address(
WalletHandle handle,
int32_t network,
int32_t* out_error
);
// Async method - callback-based
typedef void (*BalanceCallback)(
const char* json_result,
int32_t error_code,
void* user_data
);
void wallet_get_balance(
WalletHandle handle,
BalanceCallback callback,
void* user_data
);
// Memory cleanup
void string_free(char* s);
char* get_last_error(void);// lib.rs - manual FFI layer
#[no_mangle]
pub extern "C" fn wallet_new(
mnemonic: *const c_char,
out_error: *mut i32,
) -> *mut WalletManager {
let mnemonic = unsafe { CStr::from_ptr(mnemonic).to_string_lossy() };
match WalletService::new(&mnemonic) {
Ok(service) => {
unsafe { *out_error = 0 };
Box::into_raw(Box::new(WalletManager { service }))
}
Err(e) => {
set_last_error(e.to_string());
unsafe { *out_error = 1 };
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub extern "C" fn wallet_free(handle: *mut WalletManager) {
if !handle.is_null() {
unsafe { drop(Box::from_raw(handle)) };
}
}
#[no_mangle]
pub extern "C" fn wallet_generate_address(
handle: *mut WalletManager,
network: i32,
out_error: *mut i32,
) -> *mut c_char {
let mgr = unsafe { &*handle };
let net = match network {
0 => Network::Bitcoin,
1 => Network::Ethereum,
_ => {
set_last_error("Unknown network");
unsafe { *out_error = 2 };
return std::ptr::null_mut();
}
};
match mgr.service.derive_address(net) {
Ok(addr) => {
unsafe { *out_error = 0 };
CString::new(addr).unwrap().into_raw()
}
Err(e) => {
set_last_error(e.to_string());
unsafe { *out_error = 1 };
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub extern "C" fn wallet_get_balance(
handle: *mut WalletManager,
callback: BalanceCallback,
user_data: *mut c_void,
) {
let mgr = unsafe { &*handle };
let ud = user_data as usize;
RUNTIME.spawn(async move {
let mgr = unsafe { &*(ud as *const WalletManager) };
match mgr.service.fetch_balances().await {
Ok(balances) => {
let json = serde_json::to_string(&balances).unwrap();
let c = CString::new(json).unwrap();
callback(c.as_ptr(), 0, ud as *mut c_void);
}
Err(e) => {
set_last_error(e.to_string());
callback(std::ptr::null(), 1, ud as *mut c_void);
}
}
});
}// WalletManager.swift - manual wrapper
class WalletManager {
private let handle: OpaquePointer
init(mnemonic: String) throws {
var err: Int32 = 0
guard let h = wallet_new(mnemonic, &err) else {
throw WalletError.from(code: err)
}
self.handle = h
}
deinit {
wallet_free(handle) // forget this = leak
}
// Sync
func generateAddress(_ network: Network) throws -> String {
var err: Int32 = 0
let ptr = wallet_generate_address(handle, network.rawValue, &err)
guard err == 0, let p = ptr else {
throw WalletError.from(code: err)
}
defer { string_free(p) } // forget this = leak
return String(cString: p)
}
// Async - bridge callback to async/await
func getBalance() async throws -> [Balance] {
try await withCheckedThrowingContinuation { continuation in
let ctx = UnsafeMutablePointer<
CheckedContinuation<[Balance], Error>
>.allocate(capacity: 1)
ctx.initialize(to: continuation)
wallet_get_balance(
handle,
{ jsonPtr, errCode, userData in
let cont = userData!.assumingMemoryBound(
to: CheckedContinuation<[Balance], Error>.self
).pointee
if errCode != 0 {
cont.resume(throwing: WalletError.from(code: errCode))
} else {
let json = String(cString: jsonPtr!)
let data = json.data(using: .utf8)!
let balances = try! JSONDecoder().decode(
[Balance].self, from: data
)
cont.resume(returning: balances)
}
userData!.deallocate()
},
ctx
)
}
}
}// WalletManager.kt - manual JNI wrapper
class WalletManager(mnemonic: String) : AutoCloseable {
private var handle: Long
init {
val errBuf = IntArray(1)
handle = nativeNew(mnemonic, errBuf)
if (errBuf[0] != 0) {
throw WalletError.from(errBuf[0], nativeGetLastError())
}
}
// Sync
fun generateAddress(network: Network): String {
val errBuf = IntArray(1)
val result = nativeGenerateAddress(handle, network.ordinal, errBuf)
?: throw WalletError.from(errBuf[0], nativeGetLastError())
if (errBuf[0] != 0) {
throw WalletError.from(errBuf[0], nativeGetLastError())
}
return result
}
// Async - bridge callback to coroutine
suspend fun getBalance(): List<Balance> = suspendCancellableCoroutine { cont ->
nativeGetBalance(handle, object : BalanceCallback {
override fun onResult(json: String?, errorCode: Int) {
if (errorCode != 0) {
cont.resumeWithException(
WalletError.from(errorCode, nativeGetLastError())
)
} else {
val balances = Gson().fromJson(
json, Array<Balance>::class.java
).toList()
cont.resume(balances)
}
}
})
}
override fun close() {
if (handle != 0L) {
nativeFree(handle)
handle = 0L
}
}
protected fun finalize() { close() } // backup if close() not called
// JNI declarations - you also write the C++ JNI bridge
private external fun nativeNew(mnemonic: String, err: IntArray): Long
private external fun nativeFree(handle: Long)
private external fun nativeGenerateAddress(
handle: Long, network: Int, err: IntArray
): String?
private external fun nativeGetBalance(handle: Long, callback: BalanceCallback)
private external fun nativeGetLastError(): String
interface BalanceCallback {
fun onResult(json: String?, errorCode: Int)
}
companion object {
init { System.loadLibrary("wallet_ffi") }
}
}// wallet_jni.cpp - JNI bridge (you ALSO write)
extern "C" JNIEXPORT jlong JNICALL
Java_WalletManager_nativeNew(
JNIEnv* env, jobject, jstring mnemonic, jintArray err
) {
const char* m = env->GetStringUTFChars(mnemonic, nullptr);
int32_t errCode = 0;
auto handle = wallet_new(m, &errCode);
env->ReleaseStringUTFChars(mnemonic, m);
jint buf[1] = { errCode };
env->SetIntArrayRegion(err, 0, 1, buf);
return reinterpret_cast<jlong>(handle);
}
// ... repeat for every method ...C bindings total: 5 files (header + Rust FFI + Swift + Kotlin + JNI C++), ~400+ lines, 8 unsafe blocks, manual free on every platform, callback-to-coroutine/continuation bridges for async