// Raw FFI bindings for the C ABI in native/src/ffi.rs: one lookup per entry // point, no logic. Handles are opaque pointers at this layer; the typed // methods accept plain addresses (ints) so they cross isolates freely. // Every string this ABI returns is freed with smolFreeString — Dart's GC // runs no Rust destructor. import "dart:convert"; import "dart:ffi"; import "dart:typed_data"; import "package:ffi/ffi.dart"; import "package:smol_mail/native/library.dart"; // Stable error codes, mirrored from native/src/ffi.rs. The space is split // like the wire's: 0-10 are protocol status codes, verbatim, and local // failures live from 64 up, so a recovery path can be picked by range — // RATE_LIMITED (8) retries on the live session it arrived on, while // HANDSHAKE_REFUSED (71) means there is no session to retry on. const smolOk = 0; const smolMalformed = 1; const smolBadVersion = 2; const smolUnknownUser = 3; const smolAuthRequired = 4; const smolAuthFailed = 5; const smolQuotaExceeded = 6; const smolTooLarge = 7; const smolRateLimited = 8; const smolNotPermitted = 9; const smolInternalError = 10; /// Unassigned status byte; the raw byte is in the payload's "status". const smolUnknownStatus = 11; const smolNotPinned = 64; const smolPinMismatch = 65; const smolKeyChanged = 66; const smolNotRegistered = 67; const smolChainLimit = 68; const smolSchemaVersion = 69; const smolUnreachable = 70; const smolHandshakeRefused = 71; const smolStorage = 72; const smolNoise = 73; const smolIo = 74; const smolOther = 75; const smolPanic = 76; /// What a failed native call reports: the decision code plus the structured /// payload the ABI carries (host, known, offered — keys as base32). class NativeSmolException implements Exception { final int code; final String message; final Map details; const NativeSmolException(this.code, this.message, this.details); @override String toString() => message; } /// The raw ABI. Opened per isolate — handles are plain addresses, so the /// same store can be called from any isolate, which is what the async /// wrapper relies on. class SmolFfi { SmolFfi._(this._lib); final DynamicLibrary _lib; static SmolFfi? _cached; factory SmolFfi.open() => _cached ??= SmolFfi._(openSmolLibrary()); Pointer _h(int address) => Pointer.fromAddress(address); late final Pointer Function() _lastError = _lib .lookupFunction Function(), Pointer Function()>( "smol_last_error"); late final void Function(Pointer) _freeString = _lib.lookupFunction< Void Function(Pointer), void Function(Pointer)>("smol_free_string"); late final Pointer Function(Pointer) _storeOpen = _lib .lookupFunction Function(Pointer), Pointer Function(Pointer)>("smol_store_open"); late final Pointer Function() _storeMemory = _lib.lookupFunction< Pointer Function(), Pointer Function()>("smol_store_memory"); late final void Function(Pointer) _storeFree = _lib.lookupFunction< Void Function(Pointer), void Function(Pointer)>("smol_store_free"); late final Pointer Function(Pointer, int) _accountNew = _lib .lookupFunction< Pointer Function(Pointer, Uint32), Pointer Function( Pointer, int)>("smol_account_new"); late final void Function(Pointer) _accountFree = _lib.lookupFunction< Void Function(Pointer), void Function(Pointer)>("smol_account_free"); late final Pointer Function(Pointer) _accountPk = _lib .lookupFunction Function(Pointer), Pointer Function(Pointer)>("smol_account_pk"); late final Pointer Function() _flagNew = _lib.lookupFunction< Pointer Function(), Pointer Function()>("smol_flag_new"); late final void Function(Pointer, int) _flagSet = _lib.lookupFunction< Void Function(Pointer, Int32), void Function(Pointer, int)>("smol_flag_set"); late final void Function(Pointer) _flagFree = _lib.lookupFunction< Void Function(Pointer), void Function(Pointer)>("smol_flag_free"); late final int Function(Pointer, Pointer, Pointer, int) _pinServer = _lib.lookupFunction< Int32 Function(Pointer, Pointer, Pointer, Uint16), int Function(Pointer, Pointer, Pointer, int)>("smol_pin_server"); late final Pointer Function(Pointer, Pointer, int) _serverPin = _lib.lookupFunction< Pointer Function(Pointer, Pointer, Uint16), Pointer Function(Pointer, Pointer, int)>( "smol_server_pin"); late final Pointer Function(Pointer) _storeAccount = _lib .lookupFunction Function(Pointer), Pointer Function(Pointer)>("smol_store_account"); late final int Function(Pointer) _rotations = _lib.lookupFunction< Int32 Function(Pointer), int Function(Pointer)>("smol_rotations"); late final int Function(Pointer, Pointer, Pointer, Pointer, Pointer, int) _register = _lib.lookupFunction< Int32 Function(Pointer, Pointer, Pointer, Pointer, Pointer, Uint64), int Function(Pointer, Pointer, Pointer, Pointer, Pointer, int) >("smol_register"); late final int Function(Pointer, Pointer, Pointer, Pointer, int) _restore = _lib.lookupFunction< Int32 Function(Pointer, Pointer, Pointer, Pointer, Uint64), int Function(Pointer, Pointer, Pointer, Pointer, int)>("smol_restore"); late final Pointer Function(Pointer, Pointer, Pointer, int) _rotate = _lib.lookupFunction< Pointer Function( Pointer, Pointer, Pointer, Uint64), Pointer Function(Pointer, Pointer, Pointer, int)>("smol_rotate"); late final Pointer Function(Pointer, Pointer, int, int, Pointer, int, Pointer) _fetch = _lib.lookupFunction< Pointer Function(Pointer, Pointer, Int32, Int32, Pointer, Uint64, Pointer), Pointer Function(Pointer, Pointer, int, int, Pointer, int, Pointer)>("smol_fetch"); late final Pointer Function( Pointer, Pointer, Pointer, Pointer, Pointer, Pointer, int, int, Pointer, int) _send = _lib.lookupFunction< Pointer Function( Pointer, Pointer, Pointer, Pointer, Pointer, Pointer, Int32, Int32, Pointer, Uint64), Pointer Function(Pointer, Pointer, Pointer, Pointer, Pointer, Pointer, int, int, Pointer, int)>("smol_send"); late final int Function(Pointer, Pointer, Pointer, Pointer, int) _delete = _lib.lookupFunction< Int32 Function( Pointer, Pointer, Pointer, Pointer, Uint64), int Function(Pointer, Pointer, Pointer, Pointer, int)>("smol_delete"); late final Pointer Function(Pointer, Pointer) _mail = _lib .lookupFunction Function(Pointer, Pointer), Pointer Function( Pointer, Pointer)>("smol_mail"); late final Pointer Function(Pointer, Pointer, Pointer) _describe = _lib.lookupFunction< Pointer Function(Pointer, Pointer, Pointer), Pointer Function( Pointer, Pointer, Pointer)>("smol_describe"); late final Pointer Function(Pointer) _contacts = _lib .lookupFunction Function(Pointer), Pointer Function(Pointer)>("smol_contacts"); late final Pointer Function(Pointer, Pointer) _contact = _lib.lookupFunction< Pointer Function(Pointer, Pointer), Pointer Function(Pointer, Pointer)>( "smol_contact"); late final Pointer Function(Pointer, Pointer, Pointer, Pointer, int) _accept = _lib.lookupFunction< Pointer Function( Pointer, Pointer, Pointer, Pointer, Uint64), Pointer Function(Pointer, Pointer, Pointer, Pointer, int)>("smol_accept"); late final int Function(Pointer, Pointer, Pointer, Pointer, int) _block = _lib.lookupFunction< Int32 Function( Pointer, Pointer, Pointer, Pointer, Uint64), int Function(Pointer, Pointer, Pointer, Pointer, int)>("smol_block"); late final Pointer Function( Pointer, Pointer, Pointer, int) _resolve = _lib .lookupFunction< Pointer Function( Pointer, Pointer, Pointer, Uint64), Pointer Function(Pointer, Pointer, Pointer, int)>("smol_resolve"); late final int Function(Pointer, Pointer) _accountSet = _lib .lookupFunction, Pointer), int Function(Pointer, Pointer)>("smol_account_set"); late final int Function(Pointer, Pointer, Pointer, int) _contactSave = _lib.lookupFunction< Int32 Function( Pointer, Pointer, Pointer, Int32), int Function(Pointer, Pointer, Pointer, int)>("smol_contact_save"); late final int Function(Pointer, Pointer, int) _unpinServer = _lib.lookupFunction< Int32 Function(Pointer, Pointer, Uint16), int Function( Pointer, Pointer, int)>("smol_unpin_server"); late final Pointer Function(Pointer) _pins = _lib.lookupFunction< Pointer Function(Pointer), Pointer Function(Pointer)>("smol_pins"); late final int Function(Pointer) _syncOk = _lib.lookupFunction< Int32 Function(Pointer), int Function(Pointer)>("smol_sync_ok"); late final int Function(Pointer, Pointer, Pointer) _deleteLocal = _lib.lookupFunction< Int32 Function(Pointer, Pointer, Pointer), int Function( Pointer, Pointer, Pointer)>("smol_delete_local"); late final Pointer Function(Pointer) _parseAddress = _lib .lookupFunction Function(Pointer), Pointer Function(Pointer)>("smol_parse_address"); late final int Function(Pointer, Pointer) _importContact = _lib .lookupFunction, Pointer), int Function(Pointer, Pointer)>("smol_import_contact"); late final Pointer Function(Pointer, Pointer) _exportBackup = _lib.lookupFunction< Pointer Function(Pointer, Pointer), Pointer Function( Pointer, Pointer)>("smol_export_backup"); late final Pointer Function(Pointer, Pointer, Pointer) _importBackup = _lib.lookupFunction< Pointer Function( Pointer, Pointer, Pointer), Pointer Function( Pointer, Pointer, Pointer)>( "smol_import_backup"); // --- helpers over the raw surface ------------------------------------------ /// Throws when a status call returned non-zero, decoding the error slot. /// The slot is thread-local: this only works because it runs inside the /// same Isolate.run closure that made the failing call — after the /// closure returns, the slot on that thread is gone. Never _fail() { final slot = _lastError(); if (slot == nullptr) { throw const NativeSmolException(smolOther, "the native call failed", {}); } final text = take(slot); final Map details; try { details = jsonDecode(text) as Map; } on FormatException { throw NativeSmolException(smolOther, text, {}); } throw NativeSmolException( details["code"] as int? ?? smolOther, details["message"] as String? ?? "the native call failed", details); } /// Reads one returned buffer into a String and frees the buffer — the /// ownership rule every caller owes a returned pointer. String take(Pointer ptr) { if (ptr == nullptr) _fail(); final text = ptr.toDartString(); _freeString(ptr); return text; } Pointer _text(String text) => text.toNativeUtf8(); Pointer _bytes(Uint8List bytes) { final buf = malloc(32); buf.asTypedList(32).setAll(0, bytes); return buf; } // --- typed surface: handles cross as ints ---------------------------------- int openStore(String path) { final ptr = _storeOpen(_text(path)); if (ptr == nullptr) _fail(); return ptr.address; } int openMemoryStore() { final ptr = _storeMemory(); if (ptr == nullptr) _fail(); return ptr.address; } void closeStore(int store) => _storeFree(_h(store)); int newAccount(Uint8List master, int index) { final buf = _bytes(master); final ptr = _accountNew(buf, index); malloc.free(buf); if (ptr == nullptr) _fail(); return ptr.address; } void freeAccount(int account) => _accountFree(_h(account)); String accountPk(int account) => take(_accountPk(_h(account))); int newFlag() => _flagNew().address; void setFlag(int flag, bool value) => _flagSet(_h(flag), value ? 1 : 0); void freeFlag(int flag) => _flagFree(_h(flag)); void pinServer(int store, String host, String keyB32, int port) { if (_pinServer(_h(store), _text(host), _text(keyB32), port) != smolOk) { _fail(); } } void unpinServer(int store, String host, int port) { if (_unpinServer(_h(store), _text(host), port) != smolOk) _fail(); } /// Every pin: [{host, key}] with keys as base32. List pins(int store) => jsonDecode(take(_pins(_h(store)))) as List; /// Whether the local accept-token set may replace the server's (sec 4). bool syncOk(int store) => _syncOk(_h(store)) == 1; void saveContact(int store, String address, String keyB32, {required bool verified}) => _contactSave( _h(store), _text(address), _text(keyB32), verified ? 1 : 0); /// Null when nothing is pinned. String? serverPin(int store, String host, int port) { final text = take(_serverPin(_h(store), _text(host), port)); return text.isEmpty ? null : text; } /// Null when not registered. String? accountAddress(int store) { final text = take(_storeAccount(_h(store))); return text.isEmpty ? null : text; } int rotations(int store) => _rotations(_h(store)); /// Binds the account locally, as register and restore do at their end. void setAccount(int store, String address) { if (_accountSet(_h(store), _text(address)) != smolOk) _fail(); } void register(int store, int account, String address, {String? invite, String? dial, int timeout = 30}) { if (_register( _h(store), _h(account), _text(address), invite == null ? nullptr : _text(invite), dial == null ? nullptr : _text(dial), timeout) != smolOk) { _fail(); } } void restore(int store, Uint8List master, String address, {String? dial, int timeout = 30}) { final buf = _bytes(master); final code = _restore(_h(store), buf, _text(address), dial == null ? nullptr : _text(dial), timeout); malloc.free(buf); if (code != smolOk) _fail(); } Map rotate(int store, int account, {String? dial, int timeout = 30}) => jsonDecode(take(_rotate(_h(store), _h(account), dial == null ? nullptr : _text(dial), timeout))) as Map; Map fetch(int store, int account, {bool keep = false, bool reset = false, String? dial, int timeout = 30, int? cancel}) { final text = take(_fetch(_h(store), _h(account), keep ? 1 : 0, reset ? 1 : 0, dial == null ? nullptr : _text(dial), timeout, cancel == null ? nullptr : _h(cancel))); return jsonDecode(text) as Map; } Map send(int store, int account, String to, String body, {String? subject, String? replyTo, bool anonymous = false, bool noPad = false, String? dial, int timeout = 30}) { final text = take(_send( _h(store), _h(account), _text(to), subject == null ? nullptr : _text(subject), _text(body), replyTo == null ? nullptr : _text(replyTo), anonymous ? 1 : 0, noPad ? 1 : 0, dial == null ? nullptr : _text(dial), timeout)); return jsonDecode(text) as Map; } /// Ids are 64 hex characters everywhere — the same shape `mail` and /// `describe` return, so no caller hex-decodes solely to delete. void delete(int store, int account, List ids, {String? dial, int timeout = 30}) { if (_delete(_h(store), _h(account), _text(jsonEncode(ids)), dial == null ? nullptr : _text(dial), timeout) != smolOk) { _fail(); } } List mail(int store, String folder) => jsonDecode(take(_mail(_h(store), _text(folder)))) as List; Map describe(int store, int account, String idHex) => jsonDecode(take(_describe(_h(store), _h(account), _text(idHex)))) as Map; List contacts(int store) => jsonDecode(take(_contacts(_h(store)))) as List; Map contact(int store, String address) => jsonDecode(take(_contact(_h(store), _text(address)))) as Map; int accept(int store, int account, String address, {String? dial, int timeout = 30}) => (jsonDecode(take(_accept(_h(store), _h(account), _text(address), dial == null ? nullptr : _text(dial), timeout))) as Map)["held"] as int; void block(int store, int account, String address, {String? dial, int timeout = 30}) { if (_block(_h(store), _h(account), _text(address), dial == null ? nullptr : _text(dial), timeout) != smolOk) { _fail(); } } Map resolve(int store, String address, {String? dial, int timeout = 30}) { final text = take(_resolve(_h(store), _text(address), dial == null ? nullptr : _text(dial), timeout)); return jsonDecode(text) as Map; } /// The reader's delete: removes locally and marks seen, so a kept /// server copy is not re-stored by the next fetch. void deleteLocal(int store, String folder, List ids) { if (_deleteLocal(_h(store), _text(folder), _text(jsonEncode(ids))) != smolOk) { _fail(); } } /// One address for the UI: {user, host, port, short, scheme}, or a /// NativeSmolException when it does not parse. Map parseAddress(String text) { return jsonDecode(take(_parseAddress(_text(text)))) as Map; } void importContact(int store, String uri) { if (_importContact(_h(store), _text(uri)) != smolOk) _fail(); } String exportBackup(int store, Uint8List master) { final buf = _bytes(master); final text = take(_exportBackup(_h(store), buf)); malloc.free(buf); return text; } String importBackup(int store, Uint8List master, String text) { final buf = _bytes(master); final summary = take(_importBackup(_h(store), buf, _text(text))); malloc.free(buf); return summary; } }