# Pdfium Core Memory

> 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

- Skill: `impertio-studio/pdfium-core-memory` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add impertio-studio/pdfium-core-memory`
- Raw SKILL.md: https://api.skillmd.com/api/skills/impertio-studio/pdfium-core-memory/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- License: MIT
- Author: Impertio-Studio (https://skillmd.com/u/impertio-studio)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/impertio-studio/pdfium-core-memory

---


# 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.

```text
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?

```text
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?

```text
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

```text
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`.

```rust
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.

```rust
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`:

```rust
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.

```rust
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.

```rust
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:

```rust
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.

