Skip to main content

pdfkit/
document_delegate.rs

1use std::ffi::{CStr, CString};
2use std::fmt;
3use std::os::raw::{c_char, c_void};
4use std::panic::{catch_unwind, AssertUnwindSafe};
5use std::ptr;
6
7use crate::error::Result;
8use crate::ffi;
9use crate::handle::ObjectHandle;
10use crate::notifications::PdfDocumentNotification;
11use crate::selection::PdfSelection;
12
13/// Mirrors the `PDFDocumentDelegate` callback surface.
14pub trait PdfDocumentDelegate: 'static {
15    /// Mirrors the corresponding `PDFDocumentDelegate` callback.
16    fn handle_notification(&mut self, _notification: PdfDocumentNotification) {}
17
18    /// Mirrors the corresponding `PDFDocumentDelegate` callback.
19    fn did_match_string(&mut self, _instance: PdfSelection) {}
20
21    /// Mirrors the corresponding `PDFDocumentDelegate` callback.
22    fn page_class_name(&mut self) -> Option<String> {
23        None
24    }
25
26    /// Mirrors the corresponding `PDFDocumentDelegate` callback.
27    fn annotation_class_name(&mut self, _annotation_type: &str) -> Option<String> {
28        None
29    }
30}
31
32struct DelegateState {
33    delegate: Box<dyn PdfDocumentDelegate>,
34}
35
36/// Wraps `PDFDocumentDelegateHandle`.
37pub struct PdfDocumentDelegateHandle {
38    handle: ObjectHandle,
39    _state: Box<DelegateState>,
40}
41
42impl PdfDocumentDelegateHandle {
43    /// Registers a Rust implementation of `PDFDocumentDelegate`.
44    pub fn new(delegate: impl PdfDocumentDelegate) -> Result<Self> {
45        let mut state = Box::new(DelegateState {
46            delegate: Box::new(delegate),
47        });
48        let context = ptr::addr_of_mut!(*state).cast::<c_void>();
49        let mut out_delegate = ptr::null_mut();
50        let mut out_error = ptr::null_mut();
51        let status = unsafe {
52            ffi::pdf_document_delegate_new(
53                context,
54                Some(pdf_document_delegate_notification_trampoline),
55                Some(pdf_document_delegate_match_trampoline),
56                Some(pdf_document_delegate_page_class_name_trampoline),
57                Some(pdf_document_delegate_annotation_class_name_trampoline),
58                &mut out_delegate,
59                &mut out_error,
60            )
61        };
62        crate::util::status_result(status, out_error)?;
63        Ok(Self {
64            handle: crate::util::required_handle(out_delegate, "PDFDocumentDelegate")?,
65            _state: state,
66        })
67    }
68
69    pub(crate) fn as_handle_ptr(&self) -> *mut c_void {
70        self.handle.as_ptr()
71    }
72}
73
74impl fmt::Debug for PdfDocumentDelegateHandle {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        f.debug_struct("PdfDocumentDelegateHandle")
77            .finish_non_exhaustive()
78    }
79}
80
81fn duplicate_string(value: Option<String>) -> *mut c_char {
82    value
83        .and_then(|value| CString::new(value).ok())
84        .map_or(ptr::null_mut(), |value| unsafe {
85            libc::strdup(value.as_ptr())
86        })
87}
88
89/// # Safety
90/// The caller must ensure that `context` is either null or a valid pointer to `DelegateState`
91/// that was obtained from `Box::into_raw()` and has not yet been freed.
92unsafe fn delegate_state(context: *mut c_void) -> Option<&'static mut DelegateState> {
93    context.cast::<DelegateState>().as_mut()
94}
95
96/// # Safety
97/// This is an extern "C" callback invoked by Swift. The caller must pass a valid, non-null
98/// `context` pointer that points to `DelegateState`. Panics are caught to prevent unwinding
99/// across the FFI boundary.
100unsafe extern "C" fn pdf_document_delegate_notification_trampoline(
101    context: *mut c_void,
102    raw_notification: i32,
103) {
104    let _ = catch_unwind(AssertUnwindSafe(|| {
105        // SAFETY: caller is responsible for providing valid context pointer
106        let Some(state) = (unsafe { delegate_state(context) }) else {
107            return;
108        };
109        let Some(notification) = PdfDocumentNotification::from_raw(raw_notification) else {
110            return;
111        };
112        state.delegate.handle_notification(notification);
113    }));
114}
115
116/// # Safety
117/// This is an extern "C" callback invoked by Swift. The caller must pass a valid, non-null
118/// `context` pointer that points to `DelegateState`, and `selection_handle` must be a
119/// retained PDFSelection pointer from Swift (or null). Panics are caught to prevent unwinding
120/// across the FFI boundary.
121unsafe extern "C" fn pdf_document_delegate_match_trampoline(
122    context: *mut c_void,
123    selection_handle: *mut c_void,
124) {
125    let _ = catch_unwind(AssertUnwindSafe(|| {
126        // SAFETY: caller is responsible for providing valid context and selection_handle pointers
127        let Some(state) = (unsafe { delegate_state(context) }) else {
128            return;
129        };
130        let Some(handle) = (unsafe { ObjectHandle::from_retained_ptr(selection_handle) }) else {
131            return;
132        };
133        state
134            .delegate
135            .did_match_string(PdfSelection::from_handle(handle));
136    }));
137}
138
139/// # Safety
140/// This is an extern "C" callback invoked by Swift. The caller must pass a valid, non-null
141/// `context` pointer that points to `DelegateState`. The returned pointer must be freed by
142/// the Swift caller. Panics are caught to prevent unwinding across the FFI boundary.
143unsafe extern "C" fn pdf_document_delegate_page_class_name_trampoline(
144    context: *mut c_void,
145) -> *mut c_char {
146    catch_unwind(AssertUnwindSafe(|| {
147        // SAFETY: caller is responsible for providing valid context pointer
148        let Some(state) = (unsafe { delegate_state(context) }) else {
149            return ptr::null_mut();
150        };
151        duplicate_string(state.delegate.page_class_name())
152    }))
153    .unwrap_or(ptr::null_mut())
154}
155
156/// # Safety
157/// This is an extern "C" callback invoked by Swift. The caller must pass a valid, non-null
158/// `context` pointer that points to `DelegateState`, and `annotation_type` must be either
159/// null or a valid C string pointer. The returned pointer must be freed by the Swift caller.
160/// Panics are caught to prevent unwinding across the FFI boundary.
161unsafe extern "C" fn pdf_document_delegate_annotation_class_name_trampoline(
162    context: *mut c_void,
163    annotation_type: *const c_char,
164) -> *mut c_char {
165    catch_unwind(AssertUnwindSafe(|| {
166        // SAFETY: caller is responsible for providing valid context and annotation_type pointers
167        let Some(state) = (unsafe { delegate_state(context) }) else {
168            return ptr::null_mut();
169        };
170        let Some(annotation_type) = (!annotation_type.is_null()).then(|| unsafe {
171            // SAFETY: checked for null above; Swift guarantees valid C string
172            CStr::from_ptr(annotation_type)
173                .to_string_lossy()
174                .into_owned()
175        }) else {
176            return ptr::null_mut();
177        };
178        duplicate_string(state.delegate.annotation_class_name(&annotation_type))
179    }))
180    .unwrap_or(ptr::null_mut())
181}