91 lines
4.9 KiB
Markdown
91 lines
4.9 KiB
Markdown
# ADR-0005: FFI Bridge via Flutter Rust Bridge v2
|
||
|
||
- **Status**: Accepted
|
||
- **Date**: 2026-08-20
|
||
- **Task**: 1.5 — Generate flutter_rust_bridge FFI bindings bridging Rust storage methods to Dart
|
||
|
||
## Context
|
||
|
||
Tasks 1.1–1.4 established the Rust core engine (`chronogrid_core` with SQLCipher + Automerge CRDT) and the cryptographic primitives (`chronogrid_crypto` with Argon2id KDF + OS keychain). The Flutter presentation layer needs a type-safe, zero-copy FFI bridge to invoke these Rust functions from Dart.
|
||
|
||
The project's architectural mandate (`.clinerules`) requires: *"FFI Bridge: Use `flutter_rust_bridge` C-ABI generation ONLY."*
|
||
|
||
## Decision
|
||
|
||
### 1. New Crate: `chronogrid_ffi`
|
||
|
||
A dedicated bridge crate (`crates/chronogrid_ffi/`) was created with:
|
||
|
||
- **`[lib] crate-type = ["lib", "cdylib", "staticlib"]`** — produces a shared library for Flutter's FFI loader on all platforms, plus a static library for iOS.
|
||
- **Dependencies**: `chronogrid_core`, `chronogrid_crypto`, `flutter_rust_bridge = "=2.7.0"`, `anyhow = "1"`.
|
||
|
||
### 2. API Surface Design — Serialized CRDT Bytes + Engine Singleton
|
||
|
||
Rather than passing opaque Rust pointers across FFI, the API uses a **serialized-bytes pattern**:
|
||
|
||
- **CRDT documents** are serialized to/from `Vec<u8>` (Automerge binary format).
|
||
- **Mutation functions** accept `doc_bytes: Vec<u8>`, apply the edit, and return updated `Vec<u8>`.
|
||
- **Read functions** (`task_crdt_read`, `note_crdt_read`) deserialize bytes into FFI-safe DTO structs (`FfiTaskData`, `FfiNoteData`).
|
||
- **Merge functions** are stateless — they accept two document byte arrays and return the merged result.
|
||
|
||
This avoids opaque pointer lifetime management, ensures Dart never holds stale Rust references, and leverages Automerge's natural serialization boundary.
|
||
|
||
### 3. Engine Singleton (`Mutex<Option<Engine>>`)
|
||
|
||
A global `Engine` struct holds:
|
||
- `db: Database` — the open SQLCipher connection.
|
||
- `hlc: Hlc` — the node's Hybrid Logical Clock.
|
||
|
||
The engine is stored in a `static Mutex<Option<Engine>>` with helper functions `with_engine()` and `with_engine_mut()` for safe concurrent access from FRB's thread pool.
|
||
|
||
- `init_engine(path, key, node_id)` / `init_engine_in_memory(key, node_id)` — opens the database and seeds the HLC.
|
||
- `close_engine()` — drops the connection.
|
||
- `is_engine_initialized()` — status check.
|
||
|
||
### 4. Module Structure
|
||
|
||
| Module | Functions | Category |
|
||
|----------------|-----------|----------|
|
||
| `engine` | 4 | Lifecycle (init/close/status) |
|
||
| `database` | 3 | CRDT doc persistence (save/load/delete) |
|
||
| `crypto` | 5 | Key derivation + keychain (platform-gated) |
|
||
| `task_crdt` | 18 | Task CRDT create/read/set_*/clear_*/merge |
|
||
| `note_crdt` | 8 | Note CRDT create/read/set_*/clear_*/merge |
|
||
| `types` | 3 structs | `FfiDerivedKey`, `FfiTaskData`, `FfiNoteData` |
|
||
|
||
**Total**: 38 public FFI functions + 3 DTO structs.
|
||
|
||
### 5. Error Handling
|
||
|
||
All fallible functions return `anyhow::Result<T>`, which FRB v2 automatically maps to Dart exceptions with the error message string. Internal `CoreError` and `CryptoError` variants are converted via their `Display` impls.
|
||
|
||
### 6. Platform Gating
|
||
|
||
Keychain functions (`crypto_store_credential`, `crypto_retrieve_credential`, `crypto_delete_credential`) compile on all platforms but return an error on Android, where credential storage is delegated to the Flutter KeyStore bridge.
|
||
|
||
### 7. Code Generation Config
|
||
|
||
`flutter_rust_bridge.yaml` at workspace root configures the codegen:
|
||
- **Rust input**: `crates/chronogrid_ffi/src/api/`
|
||
- **Rust output**: `crates/chronogrid_ffi/src/frb_generated.rs`
|
||
- **Dart output**: `apps/chronogrid_flutter/lib/src/rust/`
|
||
|
||
After running `flutter_rust_bridge_codegen generate`, the `frb_generated` module must be uncommented in `lib.rs`.
|
||
|
||
### 8. Flutter Side
|
||
|
||
`pubspec.yaml` updated with `flutter_rust_bridge: ^2.7.0`, `ffi: ^2.1.0`, and `meta: ^1.11.0`.
|
||
|
||
## Testing
|
||
|
||
- **25 unit tests** covering: engine lifecycle (init/close/double-init/bad-key), database CRDT doc persistence (save/load/delete/missing), task CRDT (create/read/set-all-fields/clear-optionals/merge/stateless-read/no-engine-error), note CRDT (create/read/set-all-fields/clear-optionals/merge/stateless-read/no-engine-error), and crypto (derive/deterministic/invalid-salt/generate-salt).
|
||
- All 25 tests pass with `--test-threads=1` (required due to global engine singleton).
|
||
- Verification pipeline (`scripts/verify_agent_work.ps1`) passes all checks.
|
||
|
||
## Consequences
|
||
|
||
- The Dart presentation layer can invoke all core Rust functionality via generated type-safe bindings after a single `flutter_rust_bridge_codegen generate` run.
|
||
- CRDT documents flow as opaque byte arrays across FFI, maintaining the local-first zero-cloud mandate.
|
||
- The engine singleton pattern means only one database can be open at a time per process, which matches the single-user design.
|
||
- Future phases can extend the API by adding new `pub fn` entries to the `api/` modules and re-running codegen.
|