Skip to main content

cpdb_sys/
callbacks.rs

1//! Closure-friendly wrappers over the two cpdb-libs C callback shapes.
2//!
3//! cpdb-libs has two callback types:
4//!
5//! 1. `cpdb_printer_callback` — fires when a printer is added, removed, or
6//!    changes state. It carries no `user_data`, so a thin-pointer Box
7//!    trampoline cannot work. We use a global registry keyed on the
8//!    frontend pointer; the trampoline looks the closure up by that key.
9//!
10//! 2. `cpdb_async_callback` — completion for `cpdbAcquireDetails` and
11//!    `cpdbAcquireTranslations`. It does carry `user_data`, so we use the
12//!    standard `Box<Box<dyn FnOnce>>` thin-pointer trampoline.
13//!
14//! Both trampolines wrap the user closure in `catch_unwind` so a Rust
15//! panic does not unwind across the FFI boundary (which is UB).
16
17use crate::bindings as ffi;
18use crate::printer::Printer;
19use std::collections::HashMap;
20use std::panic::AssertUnwindSafe;
21use std::sync::{Arc, Mutex, OnceLock};
22
23// ─── PrinterUpdate enum ──────────────────────────────────────────────────────
24
25/// The change reported by a printer callback.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum PrinterUpdate {
28    /// A new printer was discovered.
29    Added,
30    /// A previously known printer was removed.
31    Removed,
32    /// An existing printer's state field changed.
33    StateChanged,
34}
35
36impl PrinterUpdate {
37    /// Maps the raw `cpdb_printer_update_t` value to the safe enum.
38    pub(crate) fn from_raw(update: ffi::cpdb_printer_update_t) -> Option<Self> {
39        match update as i64 {
40            0 => Some(Self::Added),
41            1 => Some(Self::Removed),
42            2 => Some(Self::StateChanged),
43            _ => None,
44        }
45    }
46}
47
48// ─── Printer observer (no user_data) ─────────────────────────────────────────
49
50/// A boxed FnMut closure invoked for every printer-update event.
51type PrinterObserver = dyn FnMut(&Printer<'_>, PrinterUpdate) + Send;
52
53/// One slot in the registry — wrapped in an Arc so the trampoline can
54/// release the global lock before invoking the user's closure.
55type ObserverSlot = Arc<Mutex<Box<PrinterObserver>>>;
56
57/// The frontend-pointer-keyed observer registry. Globals are unavoidable
58/// here because `cpdb_printer_callback` carries no `user_data`.
59fn registry() -> &'static Mutex<HashMap<usize, ObserverSlot>> {
60    static R: OnceLock<Mutex<HashMap<usize, ObserverSlot>>> = OnceLock::new();
61    R.get_or_init(|| Mutex::new(HashMap::new()))
62}
63
64fn lock_registry() -> std::sync::MutexGuard<'static, HashMap<usize, ObserverSlot>> {
65    // Recover from poisoning — a panicking trampoline must not permanently
66    // disable registration for the rest of the process.
67    registry().lock().unwrap_or_else(|p| p.into_inner())
68}
69
70/// Inserts `observer` into the registry keyed by `frontend`.
71///
72/// Replaces any previously registered observer for that frontend.
73pub(crate) fn register_printer_observer(
74    frontend: *mut ffi::cpdb_frontend_obj_t,
75    observer: Box<PrinterObserver>,
76) {
77    lock_registry().insert(frontend as usize, Arc::new(Mutex::new(observer)));
78}
79
80/// Removes the observer for `frontend`, if any. Idempotent.
81pub(crate) fn unregister_printer_observer(frontend: *mut ffi::cpdb_frontend_obj_t) {
82    lock_registry().remove(&(frontend as usize));
83}
84
85/// `extern "C"` trampoline plugged into `cpdbGetNewFrontendObj`.
86///
87/// # Safety
88/// cpdb-libs guarantees `frontend` and `printer` are valid pointers for the
89/// duration of this call. The trampoline must not unwind across the FFI
90/// boundary — panics are absorbed by `catch_unwind`.
91pub(crate) unsafe extern "C" fn printer_trampoline(
92    frontend: *mut ffi::cpdb_frontend_obj_t,
93    printer: *mut ffi::cpdb_printer_obj_t,
94    update: ffi::cpdb_printer_update_t,
95) {
96    let slot = {
97        let map = lock_registry();
98        match map.get(&(frontend as usize)) {
99            Some(slot) => Arc::clone(slot),
100            None => return,
101        }
102    };
103    let Some(update) = PrinterUpdate::from_raw(update) else {
104        return;
105    };
106    let Ok(printer) = Printer::from_raw_borrowed(printer) else {
107        return;
108    };
109
110    let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
111        // Poisoning is benign here — the closure will get a fresh frame on
112        // the next event; previous panic state cannot affect this call.
113        let mut closure = slot.lock().unwrap_or_else(|p| p.into_inner());
114        closure(&printer, update);
115    }));
116}
117
118// ─── Async completion (with user_data) ───────────────────────────────────────
119
120/// A boxed FnOnce closure invoked exactly once when an acquire completes.
121pub(crate) type AcquireCompletion = dyn FnOnce(&Printer<'_>, bool) + Send;
122
123/// Converts a boxed completion into a raw `user_data` pointer suitable for
124/// `cpdbAcquireDetails` / `cpdbAcquireTranslations`.
125///
126/// The thin-pointer Box ensures we can round-trip through `*mut c_void`.
127pub(crate) fn into_completion_user_data(completion: Box<AcquireCompletion>) -> *mut libc::c_void {
128    Box::into_raw(Box::new(completion)) as *mut libc::c_void
129}
130
131/// `extern "C"` trampoline for `cpdb_async_callback`.
132///
133/// # Safety
134/// `user_data` must be a pointer returned by [`into_completion_user_data`]
135/// for this exact callback firing. cpdb-libs guarantees the firing happens
136/// at most once.
137pub(crate) unsafe extern "C" fn acquire_trampoline(
138    printer: *mut ffi::cpdb_printer_obj_t,
139    status: libc::c_int,
140    user_data: *mut libc::c_void,
141) {
142    if user_data.is_null() {
143        return;
144    }
145    // SAFETY: caller contract — `user_data` was created by
146    // `into_completion_user_data` and is owned by us now.
147    let outer: Box<Box<AcquireCompletion>> =
148        unsafe { Box::from_raw(user_data as *mut Box<AcquireCompletion>) };
149
150    let Ok(printer) = Printer::from_raw_borrowed(printer) else {
151        return;
152    };
153    let closure: Box<AcquireCompletion> = *outer;
154
155    let _ = std::panic::catch_unwind(AssertUnwindSafe(move || {
156        closure(&printer, status != 0);
157    }));
158}