pdfium-core-memory
pdfium-render is a RAII wrapper: every handle is a Rust struct that owns or
borrows a piece of a bound PDFium library and runs C++ cleanup in its Drop.
Almost every compile failure new users hit is a lifetime error, not a logic
error. This skill covers the ownership tree, how to store handles in a struct,
the self-referential-struct trap, the 'static bound on save_to_writer, and
what pdfium-render 0.9.0 simplified.
Scope: lifetimes, drop order, struct storage, Send/Sync. For binding setup
see pdfium-core-bindings-setup; for the conceptual map see
pdfium-core-architecture.
Quick Reference
The ownership tree
Every handle below the root borrows its parent. A child can NEVER outlive its parent; the borrow checker enforces this at compile time.
Pdfium root. owns Box<dyn PdfiumLibraryBindings>. no lifetime. no Drop.
PdfDocument<'a> 'a borrows the Pdfium. has Drop (closes the document).
PdfPages<'a> borrowed view: PdfDocument::pages() -> &PdfPages<'a>.
PdfPage<'a> owned handle, tagged with the document lifetime. has Drop.
PdfPageObjects<'a> objects() -> &PdfPageObjects<'a>.
PdfPageText<'p> text() -> Result<PdfPageText<'_>, PdfiumError>.
PdfPageAnnotations<'a> annotations() -> &PdfPageAnnotations<'a>.
The proof is in the signature (verified, 0.9.x):
load_pdf_from_file<'a>(&'a self, ...) -> Result<PdfDocument<'a>, PdfiumError>.
The document's 'a IS the borrow of the Pdfium. Keep Pdfium alive at least
as long as every document, page, and object derived from it.
Core rules
- ALWAYS keep the
Pdfiuminstance alive at least as long as everyPdfDocument,PdfPage, and page object derived from it. - ALWAYS bind PDFium ONCE per process and reuse that one
Pdfium. Re-binding per request is slow (seepdfium-impl-performance). - NEVER store a
Pdfiumand aPdfDocumentborrowed from it in the same struct. That is a self-referential struct and does NOT compile. - NEVER return a
PdfDocumentorPdfPagefrom a function that owns thePdfiumas a local variable. The borrow ends with the function. - ALWAYS pass
&Pdfiuminto helper functions and returnPdfDocument<'a>tied to that reference, OR givePdfiuma'statichome viaOnceLock. - NEVER call
std::mem::dropon a parent handle while a child is alive. It will not compile; NEVER try to force it withunsafeortransmute.
Lifetime cheat-sheet
| Handle | Lifetime | Owns or borrows | Drop | Send + Sync |
|---|---|---|---|---|
Pdfium |
none | owns Box<dyn PdfiumLibraryBindings> |
no Drop |
yes, with thread_safe |
PdfDocument<'a> |
'a = borrow of Pdfium |
borrows Pdfium |
yes, closes document | yes, with thread_safe |
PdfPages<'a> |
'a of the document |
borrowed view of document | no own Drop |
yes, with thread_safe |
PdfPage<'a> |
'a of the document |
owned handle, document-tagged | yes, closes page | yes, with thread_safe |
PdfPageText<'p> |
borrow of the PdfPage |
borrows the page | scoped to the call | n/a |
thread_safe is a default-on feature, so by default 0.9.x handles are Send
and Sync. See "Version Notes" for what that does and does NOT buy you.
Decision Trees
Where do I store a pdfium-render handle?
Do you need to keep a PdfDocument past one function scope?
NO -> bind, load, process, return plain data (Vec<u8>, String, image).
Let everything drop at end of scope. This is Pattern 1. STOP.
YES -> Will the document live for the whole process (web server, GUI app)?
YES -> put Pdfium in a global OnceLock. Documents are then
PdfDocument<'static> and store in any struct. Pattern 3.
NO -> have the caller own the Pdfium and pass &'a Pdfium in;
return PdfDocument<'a>. Add a '<'a> parameter to the struct
that stores the document. Pattern 2.
Which loader: byte_slice or byte_vec?
You have PDF bytes in memory and want a PdfDocument.
Can you keep the source buffer alive separately, longer than the document?
YES -> load_pdf_from_byte_slice(&'a self, bytes: &'a [u8], ...).
The document also borrows the slice; no copy is made.
NO -> load_pdf_from_byte_vec(&self, bytes: Vec<u8>, ...).
The document takes ownership of the Vec; only the Pdfium borrow
constrains the lifetime. Use this when the buffer would drop first.
I have a lifetime or borrow-checker error
Error mentions ...
"cannot return value referencing local variable `pdfium`"
-> the function owns the Pdfium. Move ownership to the caller and take
&'a Pdfium; OR use a OnceLock global. See Pattern 2 / Pattern 3.
storing two fields, "borrowed data escapes" / cyclic lifetime
-> self-referential struct (Pdfium + PdfDocument together). Split them:
only the Pdfium is owned anywhere; the document borrows it.
"borrowed value does not live long enough" around a byte buffer
-> load_pdf_from_byte_slice borrowed a buffer that drops too early.
Switch to load_pdf_from_byte_vec, or hoist the buffer up a scope.
save_to_writer: "type does not satisfy `'static`"
-> the writer holds a borrowed reference. Use an owning writer (File,
Vec<u8>, BufWriter<File>) or call save_to_bytes(). See Pattern 4.
Patterns
Pattern 1: Scoped processing (the default)
Bind, load, process, and drop inside one function. The borrow checker forces
correct declaration order, so drop order is automatically correct: page drops
before document, document before Pdfium.
use pdfium_render::prelude::*;
fn page_count(path: &str) -> Result<usize, PdfiumError> {
let pdfium = Pdfium::default(); // declared first, dropped last
let document = pdfium.load_pdf_from_file(path, None)?;
Ok(document.pages().len() as usize) // returns plain data, not a handle
}
Return plain owned data (Vec<u8>, String, image::DynamicImage), never a
handle. This pattern needs no lifetime annotations and never hits the trap.
Pattern 2: A function that opens a document
When a document must outlive the function that opens it, the CALLER owns the
Pdfium. The helper borrows it and returns a document tied to that borrow.
fn open<'a>(
pdfium: &'a Pdfium,
path: &str,
) -> Result<PdfDocument<'a>, PdfiumError> {
pdfium.load_pdf_from_file(path, None)
}
A struct that stores the document must then carry the same 'a:
struct Report<'a> {
document: PdfDocument<'a>,
}
The Pdfium lives somewhere that outlives every Report<'a>. This works but
infects callers with a lifetime parameter. For process-lifetime documents,
Pattern 3 is cleaner.
Pattern 3: Process-lifetime binding via OnceLock (struct storage)
Put the Pdfium in a 'static home. A document loaded from a &'static Pdfium
is PdfDocument<'static>, which stores in ANY struct with no lifetime parameter.
This is the clean fix for the self-referential-struct trap.
use std::sync::OnceLock;
use pdfium_render::prelude::*;
static PDFIUM: OnceLock<Pdfium> = OnceLock::new();
fn pdfium() -> &'static Pdfium {
PDFIUM.get_or_init(Pdfium::default)
}
struct DocStore {
document: PdfDocument<'static>, // no lifetime parameter on the struct
}
fn load(path: &str) -> Result<DocStore, PdfiumError> {
let document = pdfium().load_pdf_from_file(path, None)?;
Ok(DocStore { document })
}
This is also the single-binding performance pattern (the axum_once_cell
example); see pdfium-impl-performance. Box::leak(Box::new(Pdfium::default()))
is an equivalent way to obtain a &'static Pdfium (a deliberate, one-time leak
for a process-lifetime binding).
Pattern 4: save_to_writer and the 'static bound
Verified signature (0.9.x):
save_to_writer<W: Write + 'static>(&self, writer: &mut W) -> Result<(), PdfiumError>.
The 'static bound means the writer type must NOT hold any borrowed reference.
use std::fs::File;
use std::io::BufWriter;
// OK: File is 'static.
document.save_to_writer(&mut File::create("out.pdf")?)?;
// OK: BufWriter<File> is 'static.
document.save_to_writer(&mut BufWriter::new(File::create("out.pdf")?))?;
// OK: Vec<u8> is 'static and implements Write.
let mut buffer: Vec<u8> = Vec::new();
document.save_to_writer(&mut buffer)?;
A writer that borrows, such as std::io::Cursor<&mut [u8]>, is NOT 'static
and is rejected. When the destination is a borrowed buffer, call
save_to_bytes() and write the returned Vec<u8> yourself:
let bytes: Vec<u8> = document.save_to_bytes()?;
borrowed_slice.copy_from_slice(&bytes);
Version Notes: 0.8.x vs 0.9.x
- 0.9.0 "simplifies lifetime handling across all object instances". Some struct-storage patterns that failed the borrow checker on 0.8.x compile on 0.9.x. Default the API surface and patterns above to 0.9.x.
- 0.9.0 implements
SendandSyncfor all object instances (under the default-onthread_safefeature). APdfDocumentcan now be moved across threads or held in anArc. This does NOT add concurrency: thethread_safemutex still serializes every PDFium call. For real throughput use process-level parallelism (seepdfium-impl-performance). - 0.9.0 changed
PdfPageIndexfromu16toc_int. Code that stored a page index asu16must change the type. - The self-referential-struct trap (
Pdfium+PdfDocumentin one struct) is a Rust language limit, not a crate bug. 0.9.0 reduces related friction but does NOT make a self-referential struct legal. Patterns 2 and 3 still apply.
Anti-Patterns (summary)
| Anti-pattern | Why it fails | Fix |
|---|---|---|
struct { pdfium: Pdfium, document: PdfDocument } |
self-referential: the document borrows a field of its own struct | Pattern 3 (OnceLock) or Pattern 2 (caller owns Pdfium) |
fn open() -> PdfDocument owning Pdfium locally |
document borrows a local that drops at function end | take &'a Pdfium, return PdfDocument<'a> |
std::mem::drop(pdfium) while a document is alive |
parent dropped before child; will not compile | let scope end drop them in order |
save_to_writer with Cursor<&mut [u8]> |
the writer holds a borrow, fails W: 'static |
owning writer, or save_to_bytes() |
load_pdf_from_byte_slice with a short-lived buffer |
document borrows the slice; buffer drops first | load_pdf_from_byte_vec, or hoist the buffer |
Full failure transcripts, exact compiler messages, and fixes are in
references/anti-patterns.md.
Reference Files
references/methods.md: exact verified signatures, lifetime parameters, andDrop/Send/Syncfacts for every handle, with version annotations.references/examples.md: complete, verified Rust programs for all four patterns plus the byte-slice versus byte-vec contrast.references/anti-patterns.md: each lifetime failure with the compiler error it produces, WHY it fails, and the corrected code.
Related Skills
pdfium-core-architecture: the conceptual ownership map and runtime model.pdfium-core-bindings-setup: how to obtain and bind thePdfiuminstance.pdfium-impl-performance: the once-per-processOnceLockbinding pattern and threading reality.pdfium-impl-saving:save_to_file/save_to_writer/save_to_bytesusage in depth.pdfium-syntax-document-loading: everyload_pdf_from_*loader in detail.