Skip to content

zbus backend#6

Open
bakayu wants to merge 13 commits into
OpenPrinting:mainfrom
bakayu:zbus-backend
Open

zbus backend#6
bakayu wants to merge 13 commits into
OpenPrinting:mainfrom
bakayu:zbus-backend

Conversation

@bakayu
Copy link
Copy Markdown
Contributor

@bakayu bakayu commented May 23, 2026

Description

This PR introduces a pure native, async Rust client for CPDB using zbus, completely eliminating the dependency on GLib.

The new zbus-backend provides a modern async API that allows developers to seamlessly integrate print dialogs without blocking the UI thread. The cpdb-libs C bindings have been moved behind an optional ffi feature flag.

Key Features

  • Native Async D-Bus Client: Implemented CpdbClient which automatically discovers and connects to all active CPDB backends.
  • Live Event Stream: Added discovery_stream() which yields real-time PrinterAdded, PrinterRemoved, and PrinterStateChanged signals, making it easy to build reactive print dialog UIs.
  • Complete Feature Parity: Full support for querying printer details, options, media dimensions, localized translations, and filtering remote/temporary printers.
  • New Examples: Added comprehensive, well-documented examples demonstrating real-world usage.

Note: The new zbus-backend is now the default feature for the crate.

@bakayu
Copy link
Copy Markdown
Contributor Author

bakayu commented May 28, 2026

@TitikshaBansal
The PR is ready for review, please let me know if any changes are needed. Thanks.

bakayu added 9 commits May 31, 2026 22:14
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
- added tests, documentation and examples for the zbus backend

Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2490a08..80dd49c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -44,15 +44,15 @@ jobs:
         uses: Swatinem/rust-cache@v2

       - name: Build (all targets)
-        run: cargo build --all-targets --verbose
+        run: cargo build --all-targets --all-features --verbose

-      - name: Run tests
+      - name: Run unit tests
         env:
           LD_LIBRARY_PATH: /usr/lib:/usr/local/lib
-        run: cargo test --all --verbose -- --nocapture
+        run: cargo test --all-targets --all-features --verbose

       - name: Build examples
-        run: cargo build --examples --verbose
+        run: cargo build --examples --all-features --verbose

   docs:
     name: Documentation
@@ -78,7 +78,7 @@ jobs:
       - name: Docs
         env:
           RUSTDOCFLAGS: -D warnings
-        run: cargo doc --no-deps -p cpdb-rs
+        run: cargo doc --no-deps -p cpdb-rs --all-features

   audit:
     name: Security Audit
@@ -126,6 +126,6 @@ jobs:
         uses: Swatinem/rust-cache@v2

       - name: Build Rust library (bindgen + compile, no link)
-        run: cargo build --lib --verbose
+        run: cargo build --lib --all-features --verbose
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
- improve get_all_printers error handling

Signed-off-by: Ayush <mail@ayuch.dev>
bakayu added 3 commits June 1, 2026 10:03
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
@bakayu
Copy link
Copy Markdown
Contributor Author

bakayu commented Jun 1, 2026

@TitikshaBansal
I have resolved the merge conflicts.

Copy link
Copy Markdown
Collaborator

@TitikshaBansal TitikshaBansal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, @bakayu — and sorry for the delayed review. This is a strong
direction: dropping GLib for native async zbus is a real upgrade, and the effort
shows (clean FFI safety docs, #[non_exhaustive] error enum, the docs.rs
stub-bindings handling in build.rs, the owned/borrowed split, and gating FFI
behind an optional feature — all good calls).

I've left inline comments. Grouping them by priority below so it's easy to
triage. A few blockers, then some things worth addressing before we call this an
industrial release, then minor nits.

Blockers (let's resolve before merge):

  1. client.rs ~L162 uses std::thread::sleep inside an async fn. This blocks
    the executor thread — on a current-thread runtime the whole app freezes for
    200ms. Since the PR's goal is non-blocking async, this one's worth fixing
    first: tokio::time::sleep(...).await.

  2. The README's "automatic keep-alive in the background" — from what I can tell
    keep_alive_all() is fully manual (its own doc says to call it periodically),
    with no spawned task. Could we either spawn a real stream-bound keep-alive
    task, or adjust the README so it matches the current behavior?

  3. Similarly, "auto-retry handles activation races automatically" — the retry
    looks one-shot and only in get_all_printers; the other methods
    (get_filtered_printers, get_printer_details, print_fd, etc.) hit the
    same race with no retry. Either generalize it or soften the wording.

Worth addressing before an industrial release:

  1. links = "cpdb" is unconditional, but the default build is pure-Rust (no FFI).
    links reserves the name globally and can't be feature-gated, so a pure-Rust
    default could conflict with a real cpdb-sys. Splitting FFI into a separate
    cpdb-sys crate would be the clean fix.

  2. The library writes recoverable errors to stderr via eprintln! in several
    places. Since log is already a dependency, log::warn!/error! would let
    consumers control output.

  3. proxy_for (~L447) matches with ends_with, so "PS" resolves to
    "...Backend.CUPS". Matching the last .-segment exactly would avoid
    ambiguity once there are multiple backends.

  4. The printer list is decoded by hand from OwnedValue (unpack_printer_variants
    / field_as_str), which silently drops structs on a field mismatch. Since the
    proxy already has typed structs (RawOption, RawMedia), a typed RawPrinter
    would make this much more robust — this is the most fragile path right now.

  5. discovery_stream + backend auto-exit (~30s) + manual keep-alive means a
    consumer just awaiting the stream silently stops getting events. Worth coupling
    keep-alive to the stream's lifetime, or at minimum a loud doc warning.

Minor / nits:

  • A couple of examples/ are named like tests; test_print_job actually submits
    a real job, so a cargo run --example would have a side effect. Maybe rename to
    discover_printers / print_a_document?
  • _connection field looks dead — zbus proxies hold their own Arc<Connection>.
  • This is a sweeping breaking change (default feature flip, FFI now behind
    features = ["ffi"]) but still 0.1.0 — a short migration note for existing
    cpdb_rs::Frontend users would help.
  • Doc typo in get_printer_details: "in a D-Bus call" → "in a single D-Bus call".
  • Magic numbers (200ms retry, ~30s auto-exit) could be named consts.

On tests: the count is great, but most of the zbus tests that exercise real logic
are #[ignore] (need a live bus), so the decode path, retry logic, and
proxy_for matching aren't covered in CI. If field_as_str / unpack_printer_variants
were made testable, the suffix-matching bug in (6) would be a nice unit test.

Really good work overall — happy to talk through any of these. Once the blockers
are in I think this is close.

Comment thread src/client.rs
// If we get an `UnknownMethod` error on the very first call, wait and retry.
if let Err(zbus::Error::MethodError(ref name, _, _)) = result {
if name.as_str() == "org.freedesktop.DBus.Error.UnknownMethod" {
std::thread::sleep(std::time::Duration::from_millis(200));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::thread::sleep inside an async fn blocks the executor thread (200ms freeze on a current-thread runtime). Could we switch this to tokio::time::sleep(...).await?

Comment thread src/client.rs
fn proxy_for(&self, backend: &str) -> Result<&PrintBackendProxy<'static>> {
self.backends
.iter()
.find(|b| b.service_name.ends_with(backend) || b.service_name == backend)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ends_with matches partial names — e.g. "PS" would resolve to ...Backend.CUPS.
Matching the final .-separated segment exactly would prevent ambiguity with multiple backends.

Comment thread src/client.rs
}
}

/// Extracts a string from a zbus `Value`, returning an empty string on mismatch.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hand-rolled decode silently drops a printer on any field/type mismatch. Since the proxy already has typed structs, deriving a RawPrinter for the (sssssbss) signature would give a real typed error instead of a vanished printer.

Comment thread src/client.rs
/// ```
#[derive(Clone)]
pub struct CpdbClient {
_connection: zbus::Connection,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field looks dead — zbus proxies already hold their own Arc<Connection>, so storing _connection to "keep it alive" is redundant. Probably safe to remove (or a quick comment explaining why it's kept).

Comment thread src/client.rs
);
}

for bh in &self.backends {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The errors from do_listing(true) are swallowed with let _, and backends auto-exit after ~30s. Combined with keep-alive being manual, a consumer that just awaits this stream will silently stop receiving events after ~30s with no error. Could we tie a keep-alive task to the stream's lifetime, or at least document this loudly?

Comment thread src/proxy.rs
fn print_fd(
&self,
printer_id: &str,
num_settings: i32,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

num_settings mirrors the C API, but a D-Bus array already carries its own length, so the wrapper could compute this internally rather than asking the caller for it.

Comment thread Cargo.toml
repository = "https://github.com/OpenPrinting/cpdb-rs"
rust-version = "1.85"
build = "build.rs"
links = "cpdb"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

links = "cpdb" is unconditional, but the default build is now pure-Rust with no C linking. links reserves the name globally for the whole dependency graph and can't be feature-gated, so a pure-Rust default crate would conflict with a real cpdb-sys. Splitting the FFI into a separate cpdb-sys crate would resolve this cleanly, can you please do it also?!

Comment thread Cargo.toml
glib-sys = "0.22"
glib-sys = { version = "0.22", optional = true }
serde = { version = "1.0", features = ["derive"] }
zbus = { version = "5.15.0", default-features = false, features = [
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zbus is pulled in with only the tokio feature, so the "native async" client is tokio-only (async-std users are out). Reasonable choice, but worth documenting as a limitation — ideally the runtime feature could be selectable.

Comment thread README.md

// Spawn a background task to keep the backends from automatically
// timing out and exiting after 30 seconds of inactivity.
let keep_alive_client = client.clone();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This describes automatic background keep-alive, but keep_alive_all() looks fully manual (its own doc says to call it periodically) with no spawned task. Could we either spawn a real stream-bound keep-alive task, or reword this so it matches the current behavior?

Comment thread README.md
- **Pure Rust D-Bus Client:** No `libcpdb-dev` or C compiler needed! Everything runs natively over D-Bus using `zbus`.
- **Async First:** All methods are `async` and powered by Tokio.
- **Live Discovery:** Subscribe to a native Rust `Stream` for real-time printer additions, removals, and state changes.
- **Robust Auto-Retry:** Handles D-Bus activation race conditions automatically.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The retry here is one-shot, uses a blocking sleep, and only covers get_all_printers — but get_filtered_printers, get_printer_details, get_translations, print_fd, and print_socket hit the same activation race with no retry. Either generalizing it or softening "robust/automatic" would make the docs match the code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants