Skip to content

zbus backend#6

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

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

Conversation

@bakayu

@bakayu bakayu commented May 23, 2026

Copy link
Copy Markdown
Contributor

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

bakayu commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

@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

bakayu commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

@TitikshaBansal
I have resolved the merge conflicts.

@TitikshaBansal TitikshaBansal left a comment

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.

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 Outdated
// 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 Outdated
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 Outdated
}
}

/// 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 Outdated
/// ```
#[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These functions were generated via zbus-xmlgen. If we change the function definition the D-Bus serialization would fail.
However, the public facing function of print_fd already handles internally calculating the array lenght:

https://github.com/bakayu/cpdb-rs/blob/55014a666b8f188d1dae5cfc8122080c53c575f6/src/client.rs#L397

Comment thread Cargo.toml Outdated

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 Outdated
- **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.

bakayu added 4 commits June 8, 2026 00:09
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
@bakayu

bakayu commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

@TitikshaBansal I covered all the feedback points, Please review the PR again and let me know if I missed anything.
Thanks!

@bakayu

bakayu commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Also, I was just wondering, why do we have a random rustup-init.exe https://github.com/OpenPrinting/cpdb-rs/blob/bb70758c0a75a691da1c7f604521c15ff6e6595f/rustup-init.exe in the master branch?

@TitikshaBansal TitikshaBansal left a comment

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 is a really strong iteration, @bakayu — thanks for working through all of it.
Nearly everything from the last round is properly addressed: the blocking sleep is
gone (and retry now covers all methods, not just get_all_printers), the README
claims match the code now, FFI is cleanly split into a cpdb-sys workspace crate,
eprintln!log, proxy_for does exact segment matching, and the printer
decode is a typed RawPrinter now. Turning DiscoveryStream into a real type that
cancels keep-alive on Drop is especially clean.

The restructure did introduce two new blockers, plus a couple of things tied to
them. Once these are in I think we're basically there.

Blockers:

  1. The ffi-only build doesn't compile. config / events / media are gated
    behind zbus-backend, but their pub use re-exports in lib.rs are
    unconditional, so cargo build --no-default-features --features ffi fails with
    unresolved imports — and that's exactly the migration command the README gives
    legacy users. Adding #[cfg(feature = "zbus-backend")] to those three re-exports
    (like client::CpdbClient already has) should fix it.

  2. rustup-init.exe got committed to the repo. It would ship to crates.io on
    publish — worth a git rm and a .gitignore entry.

Worth doing alongside these:

  1. tokio is a non-optional dependency with no features declared, but the code now
    uses tokio::spawn (needs rt) and tokio::time::sleep (needs time) directly.
    It compiles today only because zbus transitively enables them — if that ever
    changes, or someone builds without zbus-backend, it breaks. Declaring the
    features we use directly (and making it optional + part of the zbus-backend
    feature) would be the robust fix, and it'd stop ffi-only consumers from pulling
    in the whole tokio tree.

  2. The CI only runs --all-features, which is why (1) slipped through — with all
    features on, the gated modules exist. A small feature matrix (ffi-only,
    zbus-backend-only) would catch this class of issue. cargo-hack --feature-powerset does it in one line.

Cleanup before publish:

  • include/wrapper.h at the root is now an orphan — build.rs moved to
    cpdb-sys/ and uses cpdb-sys/include/wrapper.h. The root copy is an unreferenced
    duplicate that would still ship.
  • PRODUCTION_READINESS.md now contradicts HEAD (it says the cpdb-sys split was
    deferred, but this PR did it; paths point to the old layout). Since it's an
    internal planning doc it'd also ship on publish — either update it or add it to
    Cargo.toml exclude.
  • Publishing order: cpdb-rs now depends on cpdb-sys via a path+version dep, so
    cpdb-sys has to be published to crates.io first. It's also missing repository
    / readme / rust-version. Worth documenting the release order.

Minor:

  • examples/zbus_test.rs — the rename pass missed this one (the others got
    filter_printers / print_a_document etc.); something like discover_printers
    would match.
  • discovery_stream (~L323) still swallows the do_listing error with let _, so
    a failed initial listing returns Ok(stream) and the consumer silently gets an
    empty stream. A per-backend log::warn! would help.
  • retry_dbus! wrapping print_fd / print_socket is safe (retry only fires on
    UnknownMethod, i.e. no job was created), but that idempotency assumption is
    non-obvious — worth a one-line comment.
  • Multiple discovery_stream() calls each spawn their own keep-alive task pinging
    all backends; might be worth deduping or documenting.
  • Still 0.1.0 for a breaking re-arch (carryover).

Great work overall — happy to pair on the ffi-only fix if useful.

Comment thread src/lib.rs Outdated
#[cfg(feature = "zbus-backend")]
pub mod proxy;

// Re-export core types for convenience.

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.

These three modules are gated behind zbus-backend, but their pub use re-exports just below are unconditional, so cargo build --no-default-features --features ffi fails with unresolved import config/events/media. That's the same command the README hands legacy users for migration. Adding #[cfg(feature = "zbus-backend")] to these re-exports (matching client::CpdbClient on L62) should
resolve it.

Comment thread .github/workflows/ci.yml
Comment thread src/client.rs
Comment thread src/client.rs
printer_id: &str,
backend: &str,
settings: &[(&str, &str)],
title: &str,

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.

Wrapping print_fd / print_socket in retry_dbus! is safe since retry only fires
on UnknownMethod (no job was created), but that idempotency reasoning isn't
obvious to a future reader — worth a short comment on the macro or call sites. Minor:
the macro evaluates $method($args) twice; fine here since args are side-effect-free,
but noting for macro hygiene.

Comment thread src/client.rs Outdated
}

for bh in &self.backends {
let _ = bh.proxy.do_listing(true).await;

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.

Keep-alive actually spawning now is great. But if this initial do_listing fails,
discovery_stream still returns Ok(stream) and the consumer gets a silent empty
stream. A per-backend log::warn! here would surface it.

Comment thread cpdb-sys/Cargo.toml
Comment thread cpdb-sys/include/wrapper.h
Comment thread Cargo.toml
], optional = true }
futures-util = "0.3"
tokio = "1"
cpdb-sys = { path = "cpdb-sys", version = "0.1.0", optional = true }

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.

tokio is non-optional with no features, but the code now uses tokio::spawn
(rt) and tokio::time::sleep (time) directly — it only compiles because zbus
enables those transitively. Cargo's guidance is to declare features you use
directly. Suggest:

tokio = { version = "1", features = ["rt", "time"], optional = true }

and adding "dep:tokio" to the zbus-backend feature. That also keeps ffi-only
consumers from pulling in the full tokio tree.

@TitikshaBansal

Copy link
Copy Markdown
Collaborator

Also, I was just wondering, why do we have a random rustup-init.exe https://github.com/OpenPrinting/cpdb-rs/blob/bb70758c0a75a691da1c7f604521c15ff6e6595f/rustup-init.exe in the master branch?

You are right. It must have been added by mistake. Can you also add this file in .gitignore and remove it as a part of your PR?

bakayu added 5 commits June 13, 2026 20:58
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
@bakayu

bakayu commented Jun 13, 2026

Copy link
Copy Markdown
Contributor Author

@TitikshaBansal the PR is ready for review again! all the feedback has been addressed.

bakayu added 2 commits June 13, 2026 21:59
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Comment thread .github/workflows/release.yml Outdated
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
LD_LIBRARY_PATH: /usr/lib:/usr/local/lib
run: cargo publish --verbose
run: cargo publish -p cpdb-sys --verbose

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 publishes cpdb-sys on every tagged release with no "skip if exists" guard. On
a release where only cpdb-rs changed, this step hard-fails with "version already
exists" and stops the whole release (the retry loop only covers cpdb-rs). Either
guard it on the index, e.g.:

if cargo search cpdb-sys | grep -q "cpdb-sys = \"$VER\""; then
  echo "cpdb-sys $VER already published, skipping"
else
  cargo publish -p cpdb-sys
fi

or commit to always bumping both crates in lockstep — in which case the tag check
(~L58-65) should validate both Cargo.tomls, not just the root.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ok, so for this I just pushed a change, I added a similar guard on cpdb-rs as well. With this we can release either or both through the workflow now.

Comment thread cpdb-sys/Cargo.toml Outdated
license = "MIT"
keywords = ["printing", "cups", "openprinting", "ffi", "cpdb"]
categories = ["api-bindings", "external-ffi-bindings", "os::unix-apis"]
readme = "../README.md"

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.

readme = "../README.md" means cpdb-sys ships the cpdb-rs README — so its
crates.io/docs.rs front page would say "Pure Rust, no C compiler needed," which is
the opposite of what cpdb-sys is (the FFI layer that needs libcpdb + a C compiler).
A short dedicated cpdb-sys/README.md plus a crate-level //! doc would point users
to cpdb-rs instead.

Comment thread cpdb-sys/Cargo.toml
@@ -0,0 +1,22 @@
[package]

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.

Minor: missing homepage / documentation here (cpdb-rs has both) — worth adding
for parity before publish.

Comment thread src/client.rs
.map_err(CpdbError::from)?;
all.push(
added
.filter_map(|sig| async move {

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.

sig.args().ok()? drops a malformed signal silently. Fine coming from a typed,
trusted proxy, but a log::debug! on the drop would make future debugging easier.

bakayu added 3 commits June 14, 2026 19:56
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
Signed-off-by: Ayush <mail@ayuch.dev>
@bakayu

bakayu commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

@TitikshaBansal addressed the latest feedback with these. Ready for review again!

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