diff --git a/docs/plans/GLIB_BINDINGS.md b/docs/plans/GLIB_BINDINGS.md new file mode 100644 index 0000000..d77a250 --- /dev/null +++ b/docs/plans/GLIB_BINDINGS.md @@ -0,0 +1,263 @@ +# GLib Bindings Plan + +## Status + +Proposed. Not started. + +## Context + +Today the GTK app talks to `kordophoned` directly over D-Bus in +[`gtk/src/service/repository.vala`](/home/buzzert/src/Kordophone/gtk/src/service/repository.vala) +and the generated interface in +[`gtk/src/service/interface/dbusservice.vala`](/home/buzzert/src/Kordophone/gtk/src/service/interface/dbusservice.vala). + +At the same time, the Rust-side daemon client logic already exists in +[`core/kordophoned-client/src/worker.rs`](/home/buzzert/src/Kordophone/core/kordophoned-client/src/worker.rs) +with platform backends for D-Bus and XPC. That means protocol changes currently +have to be reflected in multiple places: + +- `kordophoned` D-Bus/XPC server shims +- `kordophoned-client` Rust transport layer +- GTK/Vala D-Bus interface and proxy code +- Swift XPC client code + +For GTK/Vala specifically, the goal is to stop binding the application directly +to the daemon protocol surface. + +## Recommendation + +Add a GTK-facing GLib/GObject wrapper on top of a small C ABI exported from the +Rust daemon client stack. + +Do not expose the current `kordophoned-client` Rust API directly as raw C. +The current surface uses Rust enums, `Vec`, `Option`, and a threaded +worker model, which is fine internally but not a good stable FFI boundary. + +The recommended layering is: + +1. Keep `core/kordophoned-client` as the Rust-native transport/domain layer. +2. Add a new FFI crate with a narrow, C-safe API. +3. Add a small GLib/GObject wrapper for GTK/Vala consumption. +4. Migrate the GTK app to that wrapper and remove its direct D-Bus binding code. + +This keeps one transport implementation in Rust while giving Vala a natural +GObject-style API with methods, async operations, and signals. + +## Why Not Direct Rust GObject Export? + +Exporting a GObject API directly from Rust is possible in principle, but the +tooling for generating the introspection artifacts that Vala wants is still much +less straightforward than plain C/GObject. + +For this repo, the lower-risk path is: + +- Rust for the daemon client implementation +- C ABI as the stable binary boundary +- a thin C/GObject wrapper for GI/Vala + +That gives us standard GLib ownership rules, normal `.gir` / `.typelib` / +`.vapi` generation, and a cleaner Meson integration story for the GTK app. + +## Proposed Layout + +Add a new crate: + +- `core/kordophoned-client-c` + +This crate should export a small `extern "C"` interface around the existing +daemon client logic. + +Add a new Linux-focused wrapper library: + +- `gtk/libkordophone-client-glib` or `gtk/src/service/glib/` + +This wrapper should be written in C and expose a GObject API that Vala can use. +It should depend on the Rust C ABI library, not on D-Bus directly. + +## Proposed Responsibilities + +### `core/kordophoned-client` + +- Own request/response/signal semantics. +- Own platform transport handling: + - D-Bus on Linux + - XPC on macOS +- Stay Rust-native. + +### `core/kordophoned-client-c` + +- Define opaque client handles. +- Define FFI-safe request/response structs. +- Define callback registration for async completions and daemon signals. +- Marshal Rust events onto C callbacks. +- Hide Rust enums and collections from C consumers. + +### GLib Wrapper + +- Expose a `KpDaemonClient` GObject. +- Convert C callbacks into `GTask` completions and GObject signals. +- Marshal all callbacks onto the GLib main context. +- Expose Vala-friendly model objects or boxed structs. + +## Draft Public Surface + +The GTK-facing API should look like a normal GLib client, not like a transport +binding. + +Suggested primary type: + +- `KpDaemonClient` + +Suggested async methods: + +- `get_conversations_async(limit, offset, cancellable, callback)` +- `get_messages_async(conversation_id, last_message_id, cancellable, callback)` +- `reply_async(conversation_id, text, attachment_guids, cancellable, callback)` +- `new_conversation_async(handle_ids, text, attachment_guids, cancellable, callback)` +- `mark_conversation_as_read_async(conversation_id, cancellable, callback)` +- `sync_conversation_async(conversation_id, cancellable, callback)` +- `sync_conversation_list_async(cancellable, callback)` +- `upload_attachment_async(path, cancellable, callback)` +- `download_attachment_async(attachment_id, preview, cancellable, callback)` +- `get_attachment_info_async(attachment_id, cancellable, callback)` + +Suggested synchronous or utility methods: + +- `open_attachment_fd(attachment_id, preview, error)` +- `start()` +- `stop()` + +Suggested signals: + +- `conversations-updated` +- `messages-updated(conversation-id)` +- `attachment-downloaded(attachment-id)` +- `attachment-uploaded(upload-guid, attachment-guid)` +- `reconnected` +- `error(message)` + +The first pass does not need to expose every daemon event. It only needs enough +surface to replace the current GTK repository layer. + +## Suggested Model Types + +Avoid returning raw hash tables to Vala. + +Add small typed model objects or boxed structs for: + +- `KpConversationSummary` +- `KpChatMessage` +- `KpAttachmentInfo` + +If send acknowledgements matter to the UI, add: + +- `KpQueuedMessage` + +The GTK app can keep its own higher-level `Repository` wrapper initially, but it +should be wrapping typed client results instead of raw D-Bus maps. + +## Signal Handling + +Signals are the main reason this should be a GLib wrapper instead of plain C +calls from Vala. + +Required behavior: + +- daemon signal subscriptions must stay alive for the lifetime of the client +- transport callbacks must never call into GTK from a non-main thread +- all emitted GObject signals must be marshalled onto the GLib main context + +The C ABI should therefore support registration of signal callbacks plus a user +data pointer, while the GLib wrapper owns the main-context handoff. + +## Migration Plan + +### Phase 1: Stabilize Rust FFI Boundary + +- Add FFI-safe request/response types instead of exposing the current worker + enums directly. +- Keep the Rust worker and transport code internal. +- Decide which operations are callback-based and which can be blocking. + +### Phase 2: Add `kordophoned-client-c` + +- Expose opaque client construction/destruction. +- Expose request entry points for the operations GTK already uses. +- Expose signal subscription hooks. +- Add explicit allocation/free helpers for returned strings and arrays. + +### Phase 3: Add GLib Wrapper + +- Implement `KpDaemonClient` as a GObject in C. +- Convert C callbacks into `GTask`-based async completion methods. +- Emit GObject signals for daemon events. +- Generate introspection artifacts for Vala. + +### Phase 4: Migrate GTK + +- Replace direct use of `DBusService.Repository` in + [`gtk/src/service/repository.vala`](/home/buzzert/src/Kordophone/gtk/src/service/repository.vala). +- Remove the generated D-Bus binding dependency from the GTK app. +- Keep the existing GTK-side repository shape initially to minimize churn. + +### Phase 5: Revisit Swift + +Optional. + +If this turns out cleaner than the current Swift XPC wrapper, add a Swift-facing +wrapper around the same C ABI later. This is not required for the GTK migration. + +## Build System Notes + +This plan introduces a Cargo + Meson integration boundary. + +Expected follow-up work: + +- decide whether the Rust C ABI library is built via `cargo build`, `cargo-c`, + or a Meson custom target +- decide where generated headers live +- decide where `.gir`, `.typelib`, and `.vapi` artifacts are produced and + installed + +The cleanest packaging story is likely: + +- Cargo builds the Rust library +- Meson builds the GLib wrapper and generates introspection data +- GTK links to the GLib wrapper + +## Non-Goals + +- replacing D-Bus and XPC with a custom socket transport +- unifying the macOS app onto GLib +- exposing the entire daemon protocol on day one +- redesigning GTK application architecture beyond the service boundary + +## Risks + +- FFI ownership mistakes across Rust, C, and GLib +- callback threading bugs if signal delivery is not marshalled correctly +- build complexity from mixed Cargo and Meson workflows +- over-exposing the current daemon protocol instead of defining a cleaner client + API + +## Open Questions + +- Should the C ABI be Linux-only at first, or cross-platform from day one? +- Should the first GTK-facing layer expose send acknowledgements, or just fire + and rely on message update signals? +- Should handle resolution be part of the GLib client API immediately, or added + only when GTK gains compose-new-conversation UI? +- Is it worth creating a higher-level shared protocol schema before building the + C ABI, or should that wait until after the GTK migration? + +## Short Version + +If we do this later, the best path is probably: + +- Rust daemon client stays as the implementation core +- add a small C ABI on top of it +- add a tiny C/GObject wrapper for Vala +- move GTK off direct D-Bus bindings + +That removes one of the protocol surfaces we currently maintain without forcing +the GTK app to consume a Rust-native API directly.