Skip to main content

firecrawl_pdfium/
library.rs

1//! Library loading, discovery, and the process-wide PDFium instance.
2
3use std::path::{Path, PathBuf};
4use std::sync::{Mutex, MutexGuard, OnceLock};
5
6use crate::error::{Error, LoadError, Result};
7use crate::sys;
8
9/// The name of the PDFium shared library on this platform
10/// (`libpdfium.dylib`, `libpdfium.so`, or `pdfium.dll`).
11pub fn platform_library_name() -> &'static str {
12    if cfg!(target_os = "macos") {
13        "libpdfium.dylib"
14    } else if cfg!(target_os = "windows") {
15        "pdfium.dll"
16    } else {
17        "libpdfium.so"
18    }
19}
20
21/// The platform slug used by `pdfium-binaries` release assets and by
22/// `cargo xtask fetch-pdfium` (`mac-arm64`, `linux-x64`, `win-x64`, ...).
23pub fn platform_slug() -> &'static str {
24    match (std::env::consts::OS, std::env::consts::ARCH) {
25        ("macos", "aarch64") => "mac-arm64",
26        ("macos", "x86_64") => "mac-x64",
27        ("linux", "aarch64") => "linux-arm64",
28        ("linux", "x86_64") => "linux-x64",
29        ("windows", "aarch64") => "win-arm64",
30        ("windows", "x86_64") => "win-x64",
31        _ => "unknown",
32    }
33}
34
35/// Environment variable consulted first by [`Pdfium::load`]: a path to the
36/// PDFium library file, or to a directory containing it.
37pub const PDFIUM_LIB_PATH_ENV: &str = "PDFIUM_LIB_PATH";
38
39struct LibraryInner {
40    bindings: Box<sys::Bindings>,
41    ffi_lock: Mutex<()>,
42    loaded_from: Option<PathBuf>,
43}
44
45static INSTANCE: OnceLock<LibraryInner> = OnceLock::new();
46/// Serializes load attempts so exactly one thread initializes PDFium.
47static INIT_LOCK: Mutex<()> = Mutex::new(());
48
49/// Handle to the process-wide PDFium library.
50///
51/// # Loading model
52///
53/// PDFium has process-global state, so this crate maintains **one instance
54/// per process**: the first successful [`Pdfium::load`] /
55/// [`Pdfium::load_from_path`] / [`Pdfium::load_from_directory`] call
56/// initializes PDFium and every later call returns a handle to the same
57/// instance ([`Pdfium::load_from_path`] with a *different* path returns
58/// [`Error::AlreadyLoaded`] instead of silently using the wrong binary).
59/// The library is never unloaded; see `docs/DESIGN.md` for why.
60///
61/// # Thread safety
62///
63/// `Pdfium` (and every handle derived from it) is `Send + Sync`. PDFium
64/// itself is single-threaded, so all FFI calls are serialized through one
65/// process-wide mutex — concurrent use is safe but not parallel. For
66/// CPU-bound throughput, use multiple processes (PDFium upstream's own
67/// recommendation).
68#[derive(Clone, Copy)]
69pub struct Pdfium {
70    inner: &'static LibraryInner,
71}
72
73impl std::fmt::Debug for Pdfium {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        f.debug_struct("Pdfium")
76            .field("loaded_from", &self.inner.loaded_from)
77            .finish_non_exhaustive()
78    }
79}
80
81impl Pdfium {
82    /// Loads PDFium using the documented discovery chain.
83    ///
84    /// Candidates are tried in order; the first that exists wins:
85    ///
86    /// 1. The [`PDFIUM_LIB_PATH`](PDFIUM_LIB_PATH_ENV) environment variable
87    ///    (library file, or directory containing
88    ///    [`platform_library_name`]). If set but unloadable, this is a hard
89    ///    error — no silent fallback.
90    /// 2. The directory containing the current executable.
91    /// 3. `./target/pdfium/<platform>/lib` then `.../bin` (the archives use
92    ///    `lib` everywhere except Windows, which uses `bin`; both are
93    ///    probed on every platform) — the layout produced by
94    ///    `cargo xtask fetch-pdfium`.
95    /// 4. The system loader's default search path, by bare library name.
96    ///
97    /// If PDFium is already loaded, returns the existing instance without
98    /// consulting the chain.
99    pub fn load() -> Result<Pdfium> {
100        let _init = lock_ignore_poison(&INIT_LOCK);
101        if let Some(inner) = INSTANCE.get() {
102            return Ok(Pdfium { inner });
103        }
104
105        let mut searched: Vec<String> = Vec::new();
106
107        // 1. Environment variable — explicit configuration never falls through.
108        if let Some(raw) = std::env::var_os(PDFIUM_LIB_PATH_ENV) {
109            let p = PathBuf::from(&raw);
110            let file = if p.is_dir() {
111                p.join(platform_library_name())
112            } else {
113                p
114            };
115            return init_from_file(&file);
116        }
117
118        // 2. Next to the current executable.
119        if let Ok(exe) = std::env::current_exe() {
120            if let Some(dir) = exe.parent() {
121                let candidate = dir.join(platform_library_name());
122                searched.push(candidate.display().to_string());
123                if candidate.is_file() {
124                    return init_from_file(&candidate);
125                }
126            }
127        }
128
129        // 3. The `cargo xtask fetch-pdfium` layout under ./target.
130        let slug = platform_slug();
131        for sub in ["lib", "bin"] {
132            let candidate = PathBuf::from("target")
133                .join("pdfium")
134                .join(slug)
135                .join(sub)
136                .join(platform_library_name());
137            searched.push(candidate.display().to_string());
138            if candidate.is_file() {
139                return init_from_file(&candidate);
140            }
141        }
142
143        // 4. System loader by bare name.
144        searched.push(format!("<system loader: {}>", platform_library_name()));
145        // SAFETY: opening a shared library runs its initializers. The name
146        // is the fixed platform PDFium library name resolved through the
147        // system loader's trusted search path, and the symbol table is
148        // validated by `Bindings::load_from_library` before any call.
149        match unsafe { libloading::Library::new(platform_library_name()) } {
150            Ok(lib) => init_from_library(lib, None),
151            Err(_) => Err(Error::Load(LoadError::LibraryNotFound { searched })),
152        }
153    }
154
155    /// Loads PDFium from an explicit library file path (the recommended
156    /// production configuration).
157    ///
158    /// Returns [`Error::AlreadyLoaded`] if PDFium was already loaded from a
159    /// different path in this process.
160    pub fn load_from_path(path: impl AsRef<Path>) -> Result<Pdfium> {
161        let path = path.as_ref();
162        let _init = lock_ignore_poison(&INIT_LOCK);
163        if let Some(inner) = INSTANCE.get() {
164            // Canonicalize before comparing so different spellings of the
165            // same file (relative vs absolute, symlinks) are recognized as
166            // the already-loaded library. `loaded_from` is stored
167            // canonicalized by init_from_file.
168            let requested = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
169            return if inner.loaded_from.as_deref() == Some(requested.as_path()) {
170                Ok(Pdfium { inner })
171            } else {
172                Err(Error::AlreadyLoaded {
173                    loaded_from: inner.loaded_from.clone(),
174                    requested,
175                })
176            };
177        }
178        init_from_file(path)
179    }
180
181    /// Loads PDFium from `dir/`[`platform_library_name()`].
182    pub fn load_from_directory(dir: impl AsRef<Path>) -> Result<Pdfium> {
183        Self::load_from_path(dir.as_ref().join(platform_library_name()))
184    }
185
186    /// Returns the already-loaded instance, if any, without attempting a
187    /// load.
188    pub fn instance() -> Option<Pdfium> {
189        INSTANCE.get().map(|inner| Pdfium { inner })
190    }
191
192    /// The path the active library was loaded from (`None` when it was
193    /// resolved by bare name through the system loader).
194    pub fn loaded_from(&self) -> Option<&Path> {
195        self.inner.loaded_from.as_deref()
196    }
197
198    /// Acquires the process-wide FFI lock guarding all PDFium calls.
199    ///
200    /// Only needed when calling into [`crate::sys`] directly: hold the
201    /// guard for the duration of every raw call sequence, and never call
202    /// safe-API methods while holding it (they would deadlock re-acquiring
203    /// the same lock).
204    pub fn ffi_lock(&self) -> MutexGuard<'static, ()> {
205        lock_ignore_poison(&self.inner.ffi_lock)
206    }
207
208    /// The raw bindings table, for use with [`Pdfium::ffi_lock`].
209    ///
210    /// # Safety
211    ///
212    /// See [`crate::sys::Bindings`]: all calls must be serialized via
213    /// [`Pdfium::ffi_lock`], and PDFium's per-function preconditions apply.
214    pub unsafe fn raw(&self) -> &sys::Bindings {
215        &self.inner.bindings
216    }
217
218    /// Runs `f` with the bindings while holding the FFI lock.
219    pub(crate) fn ffi<R>(&self, f: impl FnOnce(&sys::Bindings) -> R) -> R {
220        let _guard = lock_ignore_poison(&self.inner.ffi_lock);
221        f(&self.inner.bindings)
222    }
223}
224
225/// A panic while holding the FFI lock leaves PDFium state consistent from
226/// the C side (each call completed or never started), so we recover the
227/// guard rather than propagate poisoning to unrelated threads.
228fn lock_ignore_poison<'a, T>(m: &'a Mutex<T>) -> MutexGuard<'a, T> {
229    m.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
230}
231
232fn init_from_file(path: &Path) -> Result<Pdfium> {
233    if !path.is_file() {
234        return Err(Error::Load(LoadError::LibraryNotFound {
235            searched: vec![path.display().to_string()],
236        }));
237    }
238    // SAFETY: opening a shared library runs its initializers. The path was
239    // explicitly configured by the caller or produced by the documented
240    // discovery chain, and the symbol table is validated by
241    // `Bindings::load_from_library` before any call.
242    let lib = unsafe { libloading::Library::new(path) }.map_err(|source| {
243        Error::Load(LoadError::OpenFailed {
244            path: path.to_path_buf(),
245            source,
246        })
247    })?;
248    // Store the canonical path so `load_from_path` can recognize other
249    // spellings of the same file, and so a discovery-relative path stays
250    // meaningful after a cwd change.
251    let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
252    init_from_library(lib, Some(canonical))
253}
254
255/// Caller must hold `INIT_LOCK` and have verified `INSTANCE` is unset.
256fn init_from_library(lib: libloading::Library, loaded_from: Option<PathBuf>) -> Result<Pdfium> {
257    // SAFETY: the file was either explicitly configured or found via the
258    // documented discovery chain; `load_from_library` validates it exports
259    // the full PDFium symbol table before any call is made.
260    let bindings = unsafe { sys::Bindings::load_from_library(lib) }
261        .map_err(|e| Error::Load(LoadError::MissingSymbol(e)))?;
262
263    let config = sys::FPDF_LIBRARY_CONFIG {
264        version: 2,
265        m_pUserFontPaths: std::ptr::null(),
266        m_pIsolate: std::ptr::null_mut(),
267        m_v8EmbedderSlot: 0,
268        // Version >2 fields are zeroed and ignored (version = 2).
269        m_pPlatform: std::ptr::null_mut(),
270        m_RendererType: 0,
271        m_FontLibraryType: 0,
272        m_BrotliEnabled: 0,
273    };
274    // SAFETY: single-threaded here (INIT_LOCK held, instance unset), config
275    // is a valid version-2 struct, and null font paths are documented as
276    // "use the default paths".
277    unsafe { bindings.FPDF_InitLibraryWithConfig(&config) };
278
279    let inner = LibraryInner {
280        bindings,
281        ffi_lock: Mutex::new(()),
282        loaded_from,
283    };
284    // Cannot race: INIT_LOCK is held. `set` only fails if already set,
285    // which the callers ruled out.
286    let _ = INSTANCE.set(inner);
287    Ok(Pdfium {
288        inner: INSTANCE.get().expect("instance just set"),
289    })
290}