| name | pdfium-core-memory |
| description | Use when storing pdfium-render handles in a struct, fighting borrow-checker lifetime errors, deciding drop order, or seeing "borrowed value does not live long enough" or "cannot return value referencing local variable" around Pdfium, PdfDocument, or PdfPage. Prevents the self-referential-struct trap and dropping a parent handle while child handles are still alive. Covers the Pdfium to PdfDocument to PdfPage ownership tree, lifetime-bound handles, the 'static bound on save_to_writer, byte-slice versus byte-vec loader lifetimes, and what pdfium-render 0.9.0 simplified. Keywords: pdfium-render lifetimes, PdfDocument lifetime parameter, drop order, self-referential struct, borrow checker error, borrowed value does not live long enough, save_to_writer 'static bound, cannot return value referencing local variable, RAII handle, how do I store a PDF document in a struct, why won't my pdfium code compile, Send Sync thread_safe, OnceLock global Pdfium
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires pdfium-render 0.8,0.9. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
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
Pdfium instance alive at least as long as every
PdfDocument, PdfPage, and page object derived from it.
- ALWAYS bind PDFium ONCE per process and reuse that one
Pdfium. Re-binding
per request is slow (see pdfium-impl-performance).
- NEVER store a
Pdfium and a PdfDocument borrowed from it in the same
struct. That is a self-referential struct and does NOT compile.
- NEVER return a
PdfDocument or PdfPage from a function that owns the
Pdfium as a local variable. The borrow ends with the function.
- ALWAYS pass
&Pdfium into helper functions and return PdfDocument<'a>
tied to that reference, OR give Pdfium a 'static home via OnceLock.
- NEVER call
std::mem::drop on a parent handle while a child is alive. It
will not compile; NEVER try to force it with unsafe or transmute.
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();
let document = pdfium.load_pdf_from_file(path, None)?;
Ok(document.pages().len() as usize)
}
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>,
}
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;
document.save_to_writer(&mut File::create("out.pdf")?)?;
document.save_to_writer(&mut BufWriter::new(File::create("out.pdf")?))?;
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
Send and Sync for all object instances (under the
default-on thread_safe feature). A PdfDocument can now be moved across
threads or held in an Arc. This does NOT add concurrency: the thread_safe
mutex still serializes every PDFium call. For real throughput use
process-level parallelism (see pdfium-impl-performance).
- 0.9.0 changed
PdfPageIndex from u16 to c_int. Code that stored a page
index as u16 must change the type.
- The self-referential-struct trap (
Pdfium + PdfDocument in 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,
and Drop/Send/Sync facts 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 the Pdfium instance.
pdfium-impl-performance: the once-per-process OnceLock binding pattern
and threading reality.
pdfium-impl-saving: save_to_file / save_to_writer / save_to_bytes
usage in depth.
pdfium-syntax-document-loading: every load_pdf_from_* loader in detail.