Safe Rust bindings for the Common Print Dialog Backends
(cpdb-libs) library from
OpenPrinting.
cpdb-rs lets Rust applications drive cpdb-libs over D-Bus: discover
printers, inspect their options and translations, and submit print jobs.
The crate is built around safe owning/borrowing types and Result-based
error handling on top of bindgen-generated FFI.
- Printer discovery over D-Bus
- Job submission with per-job options and titles
- Settings management — global (
Settings) and per-printer - Option & translation lookup, including localised labels
- Media information — sizes and per-media margin tables
- Memory-safe — owned/borrowed split enforced by lifetimes
- Linux-first; macOS supports a headers-only verification build
Install the cpdb-libs C library and GLib headers.
# Debian / Ubuntu
sudo apt-get install libcpdb-dev libglib2.0-dev
# Fedora / RHEL / CentOS
sudo dnf install cpdb-libs-devel glib2-develBuilding from source:
git clone https://github.com/OpenPrinting/cpdb-libs.git
cd cpdb-libs
./autogen.sh
./configure --prefix=/usr
make -j"$(nproc)"
sudo make install
sudo ldconfigRust 1.85+ (2024 edition) is required.
[dependencies]
cpdb-rs = "0.1.0"use cpdb_rs::{Frontend, init};
fn main() -> cpdb_rs::Result<()> {
init();
let frontend = Frontend::new()?;
frontend.connect_to_dbus()?;
for printer in frontend.get_printers()? {
println!("Printer: {}", printer.name()?);
println!(" Backend: {}", printer.backend_name()?);
println!(" State: {}", printer.get_updated_state()?);
println!(" Accepts: {}", printer.is_accepting_jobs()?);
}
Ok(())
}use cpdb_rs::{Frontend, init};
fn list_printers() -> cpdb_rs::Result<()> {
init();
let frontend = Frontend::new()?;
frontend.connect_to_dbus()?;
for printer in frontend.get_printers()? {
println!("Name: {}", printer.name()?);
println!("Location: {}", printer.location()?);
println!("Description: {}", printer.description()?);
println!("Make & Model: {}", printer.make_and_model()?);
}
Ok(())
}use cpdb_rs::{Frontend, PrinterUpdate, init};
use std::time::Duration;
fn watch() -> cpdb_rs::Result<()> {
init();
let frontend = Frontend::new_with_observer(|printer, update| {
let name = printer.name().unwrap_or_default();
match update {
PrinterUpdate::Added => println!("+ {name}"),
PrinterUpdate::Removed => println!("- {name}"),
PrinterUpdate::StateChanged => println!("~ {name}"),
}
})?;
frontend.connect_to_dbus()?;
// Keep the frontend alive while the D-Bus thread delivers events.
std::thread::sleep(Duration::from_secs(30));
Ok(())
}use cpdb_rs::{Frontend, init};
fn find_one() -> cpdb_rs::Result<()> {
init();
let frontend = Frontend::new()?;
frontend.connect_to_dbus()?;
// By (id, backend) — the canonical lookup; O(1) inside cpdb-libs.
let p = frontend.find_printer("HP_LaserJet_4", "CUPS")?;
println!("found {} on {}", p.name()?, p.backend_name()?);
Ok(())
}use cpdb_rs::{Frontend, init};
fn submit(printer_name: &str, file_path: &str) -> cpdb_rs::Result<()> {
init();
let frontend = Frontend::new()?;
frontend.connect_to_dbus()?;
let printer = frontend.get_printer(printer_name)?;
// No-options print.
let job_id = printer.print_file(file_path)?;
println!("job: {job_id}");
// With options and a title — options are applied to the printer's
// setting table before submission.
let job_id = printer.submit_job(
file_path,
&[("copies", "2"), ("sides", "two-sided-long-edge")],
"My Job",
)?;
println!("job: {job_id}");
Ok(())
}use cpdb_rs::{Settings, init};
fn manage() -> cpdb_rs::Result<()> {
init();
let mut s = Settings::new()?;
s.add_setting("copies", "1")?;
s.add_setting("orientation-requested", "portrait")?;
s.add_setting("media", "A4")?;
// Persists to the cpdb-managed user config directory.
s.save_to_disk()?;
let _loaded = Settings::read_from_disk()?;
Ok(())
}use cpdb_rs::{Frontend, init};
fn details(printer_name: &str) -> cpdb_rs::Result<()> {
init();
let frontend = Frontend::new()?;
frontend.connect_to_dbus()?;
let p = frontend.get_printer(printer_name)?;
println!("default copies: {:?}", p.get_default("copies")?);
println!("current quality: {:?}", p.get_current("print-quality")?);
let size = p.get_media_size("iso_a4_210x297mm")?;
println!("A4: {} x {} (1/100 mm)", size.width, size.length);
if let Some(label) = p.get_option_translation("copies", "en_US")? {
println!("option label: {label}");
}
if let Some(label) = p.get_choice_translation("sides", "two-sided-long-edge", "en_US")? {
println!("choice label: {label}");
}
Ok(())
}# Basic usage — list printers, check version, submit a tiny file
cargo run --example basic_usage
# Interactive CLI — list, inspect, configure printers
cargo run --example cli_printer_manager
# Full cpdb-text-frontend port — every cpdb-rs API exercised
cargo run --example cpdb-text-frontend ┌───────────────────────────────────────────┐
│ 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::frontend |
Frontend — D-Bus lifecycle, printer discovery, default printer |
cpdb_rs::printer |
Printer, Margin/Margins, MediaSize, TranslationMap, |
PrintFdHandle, PrintSocketHandle |
|
cpdb_rs::settings |
Settings, Options, Media |
cpdb_rs::options |
OptionInfo, OptionsCollection (owned snapshot of cpdb_options_t) |
cpdb_rs::callbacks |
Closure trampolines + PrinterUpdate enum |
cpdb_rs::common |
init, version, path/config helpers |
cpdb_rs::error |
CpdbError and the crate-wide Result alias |
cpdb_rs::util |
Internal CStr helpers + the COptions C-array builder |
cpdb_rs::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- "cpdb-libs not found" — Install
libcpdb-dev/cpdb-libs-develso 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.
- "No printers found" — Verify printers are configured and the relevant backend services are reachable over D-Bus.
- Linker errors — Make sure pkg-config can resolve
cpdbandcpdb-frontend; on non-standard installs setPKG_CONFIG_PATH=<prefix>/lib/pkgconfig.
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.