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}