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}