8.7 KiB
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
and the generated interface in
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
with platform backends for D-Bus and XPC. That means protocol changes currently
have to be reflected in multiple places:
kordophonedD-Bus/XPC server shimskordophoned-clientRust 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<String>, Option, and a threaded
worker model, which is fine internally but not a good stable FFI boundary.
The recommended layering is:
- Keep
core/kordophoned-clientas the Rust-native transport/domain layer. - Add a new FFI crate with a narrow, C-safe API.
- Add a small GLib/GObject wrapper for GTK/Vala consumption.
- 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-gliborgtk/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
KpDaemonClientGObject. - Convert C callbacks into
GTaskcompletions 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-updatedmessages-updated(conversation-id)attachment-downloaded(attachment-id)attachment-uploaded(upload-guid, attachment-guid)reconnectederror(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:
KpConversationSummaryKpChatMessageKpAttachmentInfo
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
KpDaemonClientas 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.Repositoryingtk/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.vapiartifacts 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.