Safe, native Rust async bindings for the Common Print Dialog Backends (CPDB) D-Bus interface.
cpdb-rs lets Rust applications communicate with CPDB print backends (like cpdb-backend-cups) directly over D-Bus without requiring any C dependencies.
The library uses zbus and tokio to provide a fully asynchronous, memory-safe, and pure-Rust implementation of the CPDB client protocol.
- Pure Rust D-Bus Client: No
libcpdb-devor C compiler needed! Everything runs natively over D-Bus usingzbus. - Async First: All methods are
asyncand powered by Tokio. - Live Discovery: Subscribe to a native Rust
Streamfor real-time printer additions, removals, and state changes. - Activation Retry: Gracefully retries printer discovery to handle D-Bus activation race conditions.
- Keep-Alive Management: Automatically pings backends to keep them active in the background.
- (Optional) Legacy C-FFI bindings available via the
ffifeature flag.
Because cpdb-rs (zbus-backend feature only) communicates directly over D-Bus, you do not need to install the cpdb-libs C development headers to build this project.
However, your system must have CPDB backend installed to actually discover any printers.
| Target | Status | Notes |
|---|---|---|
| Linux (any glibc distro) | ✅ Fully supported | The intended target. CI runs on Ubuntu. |
| macOS | Bindgen can parse the headers and the crate compiles with CPDB_NO_LINK=1, but linking requires Linux D-Bus. Useful only for compile-checking. |
|
| Windows | ❌ Not supported | cpdb-libs has no Windows port (D-Bus / GLib stack). Compilation will hard-fail with a compile_error!. Develop inside WSL Ubuntu — the repository on /mnt/c/… is reachable from WSL. |
cpdb-rs targets the cpdb-libs 3.x ABI.
⚠️ Distro packages may be too old. As of mid-2026, Debian / Ubuntu ship cpdb-libs 2.0~b5 inlibcpdb-dev. That is incompatible with this crate — installing it leaves you with bothlibcpdb.so.2(from apt) andlibcpdb.so.3(from source) and the linker picks the wrong one. Either:
- Build from source (recommended until distros catch up), or
- Verify your package gives
libcpdb.so.3.*withls /usr/lib*/libcpdb.so.*before relying on it.
Build cpdb-libs 3.x from source:
sudo apt-get install -y \
build-essential pkg-config autoconf automake libtool libtool-bin \
gettext autopoint libglib2.0-dev libdbus-1-dev libclang-dev \
libcups2-dev cups libavahi-common-dev libavahi-client-dev
# If you previously installed apt's older libcpdb*, remove it first:
sudo apt-get remove --purge libcpdb-dev libcpdb2t64 2>/dev/null
git clone --depth=1 https://github.com/OpenPrinting/cpdb-libs.git
cd cpdb-libs
./autogen.sh || autoreconf -fi
./configure --prefix=/usr
make -j"$(nproc)"
sudo make install
sudo ldconfigFedora / RHEL: install cpdb-libs-devel from a 3.x-shipping repository,
or build from source the same way.
Rust 1.85+ (2024 edition) is required.
bindgen needs libclang at build time. On Debian/Ubuntu:
sudo apt-get install -y libclang-dev clang[dependencies]
cpdb-rs = "0.1.0"
tokio = { version = "1.0", features = ["full"] }use cpdb_rs::CpdbClient;
#[tokio::main]
async fn main() -> cpdb_rs::Result<()> {
// Connect to D-Bus and auto-activate available CPDB backends
let client = CpdbClient::new().await?;
println!("Connected to {} backend(s).\n", client.backend_count());
// Retrieve all active printers
let printers = client.get_all_printers().await?;
for p in &printers {
println!("Printer: {} (ID: {})", p.name, p.id);
println!(" Make & Model: {}", p.make_model);
println!(" State: {}", p.state);
println!(" Accepts Jobs: {}", p.accepting_jobs);
}
Ok(())
}use cpdb_rs::CpdbClient;
#[tokio::main]
async fn main() -> cpdb_rs::Result<()> {
let client = CpdbClient::new().await?;
let printers = client.get_all_printers().await?;
if let Some(p) = printers.first() {
// Fetch specific details using the printer's ID and backend name
let (options, media) = client.get_printer_details(&p.id, &p.backend).await?;
println!("Options for {}:", p.name);
for opt in options {
println!(" {}: default='{}', choices=[{}]",
opt.name, opt.default_value, opt.supported_values.join(", "));
}
}
Ok(())
}Watch for new printers appearing and disappearing in real-time.
use cpdb_rs::{CpdbClient, DiscoveryEvent};
use futures_util::StreamExt;
#[tokio::main]
async fn main() -> cpdb_rs::Result<()> {
let client = CpdbClient::new().await?;
// 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();
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(15));
loop {
interval.tick().await;
keep_alive_client.keep_alive_all().await;
}
});
let mut stream = client.discovery_stream().await?;
println!("Listening for printer changes...");
while let Some(event) = stream.next().await {
match event {
DiscoveryEvent::PrinterAdded(snap) => {
println!("+ Added: {} ({})", snap.name, snap.backend);
}
DiscoveryEvent::PrinterRemoved { id, backend } => {
println!("- Removed: {} ({})", id, backend);
}
DiscoveryEvent::PrinterStateChanged { id, state, accepting_jobs, .. } => {
println!("~ State Changed: {} is now {} (accepting={})", id, state, accepting_jobs);
}
}
}
Ok(())
}You can run the full interactive test example using:
cargo run --example zbus_testInstead of linking against libcpdb.so and using bindgen (which required unsafe C memory management, callbacks, and manual lifetime tracking), cpdb-rs now uses zbus to speak the D-Bus protocol directly to the print backends. This provides massive benefits:
- 100% Safe Rust: No raw pointers, no manual memory management, no undefined behavior.
- Zero C Dependencies: You don't need
libcpdb-devto compile. - Async Tokio Integration:
zbusintegrates perfectly with Tokio, allowing you to await D-Bus calls and use RustStreams for live discovery events. - Activation Retries: Automatically retries initial calls to handle
UnknownMethodrace conditions when systemd auto-activates D-Bus backends.
Warning
The C-FFI interface is behind the ffi feature flag and has been moved to the underlying cpdb-sys crate. The default feature is now zbus-backend. To continue using cpdb_rs::Frontend, update your Cargo.toml to: cpdb-rs = { default-features = false, features = ["ffi"] }.
If you have legacy code that still requires the synchronous C-FFI wrappers around cpdb-libs, they are still available by enabling the ffi feature flag in your Cargo.toml. See the ffi module documentation for details.
┌───────────────────────────────────────────┐
│ cpdb_rs::Frontend │
│ (D-Bus connection, backend list, hash │
│ table of discovered printers) │
└─────┬─────────────────────────────────┬───┘
│ borrowed │ owned
│ (lifetime tied to &Frontend) │ (Drop frees)
▼ ▼
┌────────────────────┐ ┌──────────────────────┐
│ Printer<'frontend> │ │ Printer<'static> │
│ from get_printer / │ │ from load_from_file │
│ find_printer / ... │ └──────────────────────┘
└────────┬───────────┘
│
├── per-printer settings (add_setting / clear_setting)
├── option lookups (get_default / get_current / get_option)
├── translations ─ TranslationMap (owned snapshot)
├── media ─ MediaSize, Margins
└── job submission (print_file / submit_job / print_fd / print_socket)
┌──────────────────────────┐
│ cpdb_rs::Settings │
│ (free-standing serial- │
│ isable settings object) │
└──────────────────────────┘
▲
│ persisted via save_to_disk / read_from_disk
▼
~/.config/cpdb/ (cpdb-libs-managed location)
| Method | Scope | Persists across runs? |
|---|---|---|
Printer::add_setting |
This printer only, in-memory on the printer object | Only if you re-add on each run |
Settings::add_setting |
Free-standing settings collection | Yes, via Settings::save_to_disk() |
Printer::add_setting is the per-job knob: tweak copies, sides, etc.
before calling print_file / submit_job. Settings is the global,
serialisable view that cpdb-libs reads back from disk on startup.
| Module | What lives here |
|---|---|
cpdb_rs::client |
(zbus) CpdbClient — Main async D-Bus client & discovery logic |
cpdb_rs::events |
(zbus) DiscoveryEvent, PrinterSnapshot for async streams |
cpdb_rs::media |
(zbus) MediaCollection, MediaInfo, MarginInfo |
cpdb_rs::config |
(zbus) PrinterConfig for job submission configuration |
cpdb_rs::options |
OptionInfo, OptionsCollection (shared across both implementations) |
cpdb_rs::error |
CpdbError and the crate-wide Result alias |
cpdb_rs::proxy |
(zbus) Auto-generated zbus proxy trait PrintBackend |
cpdb_rs::frontend |
(ffi) Frontend — D-Bus lifecycle, discovery, default printer |
cpdb_rs::printer |
(ffi) Printer, Margin/Margins, MediaSize, TranslationMap |
cpdb_rs::settings |
(ffi) Settings, Options, Media |
cpdb_rs::callbacks |
(ffi) Closure trampolines + PrinterUpdate enum |
cpdb_rs::common |
(ffi) init, version, path/config helpers |
cpdb_rs::util |
(ffi) Internal CStr helpers + the COptions C-array builder |
cpdb_rs::ffi |
(ffi) Raw bindgen output; everything unsafe |
Printer carries a lifetime tied to the Frontend it came from. Borrowed
printers (those returned by get_printers, get_printer, find_printer,
get_default_printer, ...) cannot outlive their frontend — the compiler
checks this for you. Owned printers (Printer::load_from_file) have a
'static lifetime and are freed when dropped.
Printer is intentionally not Send or Sync. cpdb-libs does not
lock internally; if you need cross-thread access, wrap the printer in a
Mutex (or, more typically, run your printer operations on a single
thread).
Frontend is Send but not Sync — for the same reason.
CpdbError is #[non_exhaustive], so always include a wildcard arm:
use cpdb_rs::CpdbError;
match some_op() {
Ok(value) => { /* ... */ }
Err(CpdbError::NullPointer) => eprintln!("null pointer"),
Err(CpdbError::NotFound(what)) => eprintln!("not found: {what}"),
Err(CpdbError::JobFailed(msg)) => eprintln!("job failed: {msg}"),
Err(e) => eprintln!("other: {e}"),
}macOS is supported for header parsing and compilation only. Linking
requires a Linux environment with D-Bus. Use CPDB_NO_LINK=1 to skip
link directives:
CPDB_NO_LINK=1 cargo build --lib# Tests that do not need a live D-Bus
cargo test
# Integration tests — require a running session bus and cpdb backends
cargo test -- --ignored-
undefined symbol: cpdbGetVersion/libcpdb.so.3, may conflict with libcpdb.so.2— You have apt's older cpdb-libs 2.x installed alongside the source-built 3.x. The linker is picking the v2 library. Fix:sudo apt-get remove --purge libcpdb-dev libcpdb2t64 sudo ldconfig cd ~/cpdb-libs && sudo make install # reinstall headers v2 took with it cd /path/to/your/project && cargo clean && cargo build
-
fatal error: 'cpdb/cpdb.h' file not found— Headers are missing from/usr/include/cpdb/. Reinstall cpdb-libs from source (see Prerequisites). -
Unable to find libclang(bindgen) — Installlibclang-devandclang(Debian/Ubuntu) or the equivalent on your distro. -
cpdb-libs not found— Install cpdb-libs 3.x so pkg-config can locatecpdb.pc. Override the discovery path withCPDB_LIBS_PATH=<prefix>when working against an uninstalled checkout. -
D-Bus connection failed— Confirm a session bus is running and that print backends (CUPS, ...) are active. In headless environments usedbus-launch --exit-with-session <command>to spin up an ephemeral session bus. -
No printers found— Verify printers are configured and the relevant backend services are reachable over D-Bus. -
Linker errors on a non-standard cpdb-libs prefix — Set
PKG_CONFIG_PATH=<prefix>/lib/pkgconfigso pkg-config can resolvecpdbandcpdb-frontendfrom your install. -
error: linker 'link.exe' not foundon Windows native — cpdb-rs does not support Windows targets. Develop inside WSL Ubuntu — the repository on/mnt/c/…is reachable from WSL without copying.
See CONTRIBUTING.md.
- Fork and clone.
cargo build— verify the toolchain finds cpdb-libs.- Make changes, add tests.
- Ensure
cargo test,cargo fmt --check, andcargo clippy --all-targets -- -D warningspass. - Open a pull request.
MIT — see LICENSE.
- cpdb-libs — the C library this crate binds to.
- OpenPrinting
- CUPS
See CHANGELOG.md.