Architecture
Three layers
Mirroring pypdfium2:
Rpdfium::Raw— pure FFI bindings, 1:1 with the C API (FPDF_*,FPDFText_*,FPDFBitmap_*,FPDFPath_*,FPDFImageObj_*,FPDFAnnot_*). Use directly if you need something the wrappers don’t expose.- Wrappers —
Rpdfium::Document,::Page,::TextPage,::Image::Embedded,::Annotation,::Form::Field,::Search,::Outline,::Attachment. RAII-style wrappers withObjectSpace.define_finalizerso handles are released even if you forgetclose. Rpdfium::Table::Extractor— table detection on top of layer 2, withRpdfium::Table::Debuggerfor visual debugging.
Memory safety
FPDF_LoadMemDocument64does not copy the input bytes. TheDocumentwrapper holds an FFI buffer reference for its lifetime so the GC can’t free it early.- Every PDFium handle (
*_Close*) that owns memory independently is wired toObjectSpace.define_finalizerso abandoned objects don’t leak native memory. FPDF_InitLibraryis called once per process under aMutex;FPDF_DestroyLibraryruns viaat_exit.Document#closereleases in cascade: form-fill env → cached pages → document handle.
Finalizers are only safe on handles whose parent cannot be closed out from under them.
FPDF_DOCUMENTandFPDF_PAGEget finalizers; child handles likeFPDF_STRUCTTREEdo not — defining one there segfaults when the page closes first.
Why not pure Ruby?
A correct PDF text extractor needs to interpret the content stream (operators, font encodings including CMap-based CIDs, ToUnicode maps, ActualText overrides, marked content). PDFium has ~15 years of edge-case fixes baked in. Reimplementing it in Ruby would take years and still be slower. FFI is the right call.