# 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.