# Understanding the Thread Safety Mechanism of PDFium within LiteParse

> Discover LiteParse's PDFium thread safety mechanism. Learn how a process-wide mutex prevents undefined behavior in the native library, ensuring safe PDF operations.

- Repository: [LlamaIndex/liteparse](https://github.com/run-llama/liteparse)
- Tags: internals
- Published: 2026-06-25

---

**LiteParse serializes all PDFium operations behind a process-wide mutex that is acquired when initializing the `Library` handle and held for its entire lifetime, preventing undefined behavior in the underlying non-thread-safe native library.**

LiteParse provides Rust bindings for the PDFium library in the `run-llama/liteparse` repository, but the underlying C library is **not thread-safe** and can exhibit heap corruption or double-free errors when accessed concurrently. To solve this, LiteParse implements a **strict serialization lock** that governs all PDFium interactions across threads, enforced through a combination of global static initialization and Rust's ownership system.

## Why PDFium Requires External Synchronization

The native PDFium library maintains internal global state that is not protected against concurrent access. Without external synchronization, simultaneous calls from multiple threads lead to race conditions, memory corruption, and undefined behavior. LiteParse acknowledges this constraint by treating PDFium as a **single-threaded resource** regardless of the host application's concurrency model.

## The Process-Wide Serialization Strategy

LiteParse employs a three-layer defense strategy to ensure thread safety without sacrificing ergonomics. The implementation lives primarily in [`crates/pdfium/src/library.rs`](https://github.com/run-llama/liteparse/blob/main/crates/pdfium/src/library.rs).

### Global Initialization with `Once`

A `static INIT: Once` variable guarantees that the PDFium library is initialized exactly once per process. This prevents the race condition where multiple threads might attempt to call `FPDF_InitLibrary()` simultaneously during startup.

### The `Mutex<()>` Lock Mechanism

On non-Wasm targets, LiteParse creates a `static LOCK: OnceLock<Mutex<()>>` that acts as a **process-wide synchronization primitive**. The `pdfium_lock()` function provides access to this mutex. Every `Library` instance holds a `MutexGuard<'static, ()>` for its entire lifetime, ensuring that only one thread can hold a `Library` handle—and thus interact with PDFium—at any given moment.

### Lifetime Constraints via the Borrow Checker

All PDFium resources—including `Document`, `Page`, `TextPage`, and `Bitmap`—carry a `'lib` lifetime tied to the owning `Library` handle. This **compile-time guarantee** ensures that resources cannot outlive the lock, making it impossible to accidentally use PDFium functions after the mutex has been released. The borrow checker enforces these constraints in [`crates/pdfium/src/document.rs`](https://github.com/run-llama/liteparse/blob/main/crates/pdfium/src/document.rs) and related resource files.

## How `Library::init()` Implements the Lock

The `Library::init()` method (lines 61-80 in [`library.rs`](https://github.com/run-llama/liteparse/blob/main/library.rs)) orchestrates the synchronization:

1. **Acquire the global lock**: The first call to `init()` blocks on the mutex until it becomes available.
2. **Initialize the library**: Once the lock is held, it calls `FPDF_InitLibrary()` to prepare PDFium.
3. **Hold until drop**: The `Library` struct stores the `MutexGuard`, keeping the lock held until the instance is dropped.

Subsequent callers from other threads block on the mutex until the current `Library` is dropped, creating a **serialized access pattern** that serializes all PDFium operations across the process.

## Cross-Platform Considerations for WebAssembly

On the `wasm32` target (lines 40-41), LiteParse omits the mutex entirely. WebAssembly's single-threaded execution model eliminates the risk of concurrent access, so the synchronization overhead is unnecessary. This conditional compilation ensures optimal performance on Wasm while maintaining safety on multi-threaded targets.

## Practical Usage Example

The following example demonstrates how the lock automatically serializes PDF processing across multiple threads:

```rust
use liteparse_pdfium::{Library, Document};

fn parse_one_file(path: &str) -> Result<(), liteparse_pdfium::PdfiumError> {
    // Acquire the global PDFium lock (blocks if another thread holds it)
    let lib = Library::init();

    // All PDFium operations are now safe
    let doc: Document<'_> = lib.load_document(path, None)?;
    println!("Pages: {}", doc.page_count());

    // `lib` is dropped here, releasing the lock for other threads
    Ok(())
}

// In a multi-threaded context the lock serializes the work:
use std::thread;

let paths = vec!["a.pdf", "b.pdf", "c.pdf"];
let handles: Vec<_> = paths.into_iter()
    .map(|p| thread::spawn(move || parse_one_file(p)))
    .collect();

for h in handles { h.join().unwrap().unwrap(); }

```

In this pattern, each thread must call `Library::init()`. The call blocks until the process-wide mutex becomes available, guaranteeing safe PDFium usage without additional synchronization code from the caller.

## Summary

- **PDFium is not thread-safe**: The underlying C library requires external synchronization to prevent memory corruption.
- **Process-wide mutex**: LiteParse uses a `static LOCK: OnceLock<Mutex<()>>` in [`crates/pdfium/src/library.rs`](https://github.com/run-llama/liteparse/blob/main/crates/pdfium/src/library.rs) to serialize all access.
- **Lifetime enforcement**: The `'lib` lifetime on resources ensures they cannot outlive the lock held by the `Library` handle.
- **Automatic blocking**: `Library::init()` acquires the lock for the object's entire lifetime, forcing other threads to wait their turn.
- **Wasm optimization**: The lock is omitted on `wasm32` targets where threading is unavailable.

## Frequently Asked Questions

### Is PDFium thread-safe by default?

No. The native PDFium library maintains global internal state that is not protected by mutexes or atomic operations. Concurrent access from multiple threads causes undefined behavior, including heap corruption and double-free errors. LiteParse addresses this limitation by wrapping all PDFium calls in a process-wide serialization lock.

### How does LiteParse prevent concurrent PDFium access?

LiteParse prevents concurrent access through a `static LOCK: OnceLock<Mutex<()>>` defined in [`library.rs`](https://github.com/run-llama/liteparse/blob/main/library.rs). When any thread calls `Library::init()`, it attempts to acquire this mutex. If another thread already holds the lock, the caller blocks until the mutex is released. This ensures that only one `Library` instance—and thus only one thread—interacts with PDFium at any moment.

### Why does the lock last for the entire `Library` lifetime?

The lock lasts for the entire lifetime because `Library` stores a `MutexGuard<'static, ()>` acquired during `init()`. This design choice guarantees that all dependent resources (`Document`, `Page`, `TextPage`, etc.) are accessed while the lock is held. When the `Library` is dropped, the guard is released, allowing other threads to acquire the lock. This prevents TOCTOU (time-of-check-time-of-use) vulnerabilities in multi-threaded PDF processing.

### Does this mechanism work on WebAssembly?

No, the mechanism is bypassed on WebAssembly. On `wasm32` targets, LiteParse omits the mutex because WebAssembly operates in a single-threaded environment (without shared memory threads). The conditional compilation at lines 40-41 of [`library.rs`](https://github.com/run-llama/liteparse/blob/main/library.rs) ensures that the synchronization code is excluded from Wasm builds, avoiding unnecessary overhead where concurrent access is impossible.