Technical Article

FPDF_FORMFILLINFO Version 2 in Delphi: Follow the DLL ABI

PDFium Component now sets FPDF_FORMFILLINFO.version to 2 for every form-fill environment it initializes, because the version a native PDFium build accepts is a property of that build, not of the document being opened. An XFA-enabled pdfium.v8.dll refuses version 1 outright, so a plain AcroForm PDF opened through it used to fail in FPDFDOC_InitFormFillEnvironment with no XFA anywhere in sight. The v3.116.0 fix is small, but the mistake behind it is a general one and worth naming: a protocol version field describes the memory layout the other side expects, and it must never be derived from whether you happen to need the features that layout carries

Why does FPDFDOC_InitFormFillEnvironment fail on a plain PDF with pdfium.v8.dll?

The environment fails because an XFA-enabled PDFium build validates the version field before it does anything else, and the old wrapper logic handed it a 1 whenever the current document was not an XFA form. The symptom in a Delphi host is an EPdfError raised from TPdf.InitializeFormFill with the message Cannot initialize form fill environment, thrown while opening an ordinary invoice or tax form that has nothing but AcroForm text fields. The same file opens fine against the plain pdfium.dll. The same DLL opens a real XFA document fine. Only the combination of the V8 build and a non-XFA document breaks, which is exactly the combination a host lands in after it turns on EnableV8Engine to get AcroForm JavaScript, or after the auto-selection in LoadDocument has already committed the process to pdfium.v8.dll for an earlier XFA file. That commitment is process-wide: EnableV8Engine is read before the first LoadLibrary, and once the XFA build is loaded every later plain PDF goes through the same environment setup against the same binary. The host did nothing wrong; the wrapper asked the wrong question when it filled in the record. If you are still deciding which binary to ship at all, our note on deploying the PDFium DLL and diagnosing load failures covers the plain versus V8 selection, and this article assumes the V8 build is already in the process

PDFium Component diagram of the four combinations of plain pdfium.dll and XFA-enabled pdfium.v8.dll against AcroForm and XFA documents: a version 1 record broke only the V8 build with a plain form, EPdfError in FPDFDOC_InitFormFillEnvironment, while the corrected version 2 record opens all four
One conditional tied the ABI version to the document, so the process-wide choice of the V8 binary turned every later plain PDF into a failed environment initialization

What does the version field in FPDF_FORMFILLINFO actually promise?

FPDF_FORMFILLINFO.version tells PDFium which fields of the record it is allowed to read, and the public header fpdf_formfill.h ties the acceptable values to how the library was compiled rather than to the document. In paraphrase, the contract has three parts. Version 1 covers the stable callbacks from FFI_Invalidate through FFI_DoGoToAction plus the m_pJsPlatform pointer. A build without the XFA module accepts either 1 or 2, and with 2 it will also call the additional experimental callbacks. A build with the XFA module requires 2, full stop, and the header repeats that requirement twice as if it expected people to miss it. Nowhere does the contract mention the document. The version is a statement about the record you allocated: with a 2 you are promising that the memory after m_pJsPlatform exists and holds either valid function pointers or NULL

The version-2 region is where all of the XFA machinery lives. It starts with xfa_disabled, an FPDF_BOOL the header describes as ignored below version 2 and meaningful only when the XFA module is compiled in, and continues with seventeen function pointers, FFI_DisplayCaret through FFI_DoURIActionWithKeyboardModifier. Each of those is documented as required for XFA and otherwise to be set to NULL. That phrasing is the key to the whole fix. NULL is not an error state for those slots; it is the documented state for a host that is not driving XFA. A record that has been cleared with FillChar and then marked as version 2 satisfies the contract on a non-XFA build exactly as well as a version-1 record does, and it is the only record an XFA build will take

PDFium Component diagram of the FPDF_FORMFILLINFO record in Delphi: version 1 covers the FFI_Invalidate through FFI_DoGoToAction callbacks plus m_pJsPlatform, version 2 adds xfa_disabled and seventeen FFI_DisplayCaret era pointers, FillChar clears every byte, and NULL slots are the documented state for a host that is not driving XFA
The Pascal record is always the complete version 2 layout, so an XFA-enabled build accepts it and a plain build simply never calls the experimental slots that stay NULL

The old selection tied the ABI to the document

The defect was a single conditional that looked reasonable in isolation. TPdf.InitializeFormFill computes a RuntimeReady flag from three facts: the document reports an XFA form type through TPdf.XFA, the XFA string helpers resolved through XfaFeaturesAvailable, and the V8 exports resolved through V8FeaturesAvailable. Before v3.116.0 that same flag also chose the version

// v3.115.0 and earlier: the ABI version followed the document
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;

if RuntimeReady then
  FFormFillInfo.Info.version := 2
else
  FFormFillInfo.Info.version := 1;

// ... and the runtime-missing branch pinned it again
else if XFA then
begin
  FFormFillInfo.Info.version := 1;
  FFormFillInfo.Info.xfa_disabled := 1;
  if Assigned(FOnXfaRuntimeMissing) then
    FOnXfaRuntimeMissing(Self);
end;

Read it with the header in hand and the failure is obvious. RuntimeReady is false for every plain AcroForm document, so every plain document announced version 1. On pdfium.dll that is fine. On pdfium.v8.dll, which is the XFA-enabled build, PDFium checks the field, finds it below the required 2, and returns a null FPDF_FORMHANDLE, which CheckPdf turns into the exception above. The intent of the old code was defensive: keep version 1 so an XFA build never reads the unassigned version-2 slots. It defended against a problem the header already rules out and created one the header explicitly warns about. The corrected code decides the version once, up front, from what the record physically is

procedure TPdf.InitializeFormFill;
var
  RuntimeReady: Boolean;
begin
  FXfaRuntimeUsable := False;
  FXfaPageCountOverride := -1;   // sentinel: use the static page tree
  if not FormFill then
    Exit;

  FillChar(FFormFillInfo, SizeOf(FFormFillInfo), 0);
  FFormFillInfo.Pdf := Self;

  // The full version-2 record is allocated and cleared above. PDFium
  // accepts version 2 without XFA and requires it in every XFA-enabled
  // build, including when this document contains no XFA form.
  FFormFillInfo.Info.version := 2;
  FFormFillInfo.Info.xfa_disabled := 1;

  // RuntimeReady gates the XFA callbacks and xfa_disabled, never version.
  RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
  ...

Where RuntimeReady still belongs: the callbacks and xfa_disabled

RuntimeReady keeps its job as the gate for XFA behavior; it simply no longer touches the record layout. The version-1 callbacks, FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction, and the rest of that block, are wired unconditionally because AcroForm and XFA both depend on them. The seventeen version-2 pointers are assigned only inside the RuntimeReady branch, together with xfa_disabled := 0. When the document is XFA but the runtime is not there, the record stays at version 2 with xfa_disabled at 1 and the version-2 slots left NULL, and the wrapper raises OnXfaRuntimeMissing so the host can suggest restarting on pdfium.v8.dll. After the environment exists, FPDF_LoadXFA is called only when RuntimeReady was true, and only a true return sets FXfaRuntimeUsable, which is what TPdf.XfaRuntimeAvailable reports

  if RuntimeReady then
  begin
    FFormFillInfo.Info.xfa_disabled := 0;   // 0 = XFA enabled
    FFormFillInfo.Info.FFI_DisplayCaret := FormFillDisplayCaret;
    FFormFillInfo.Info.FFI_GetCurrentPageIndex := FormFillGetCurrentPageIndex;
    FFormFillInfo.Info.FFI_SetCurrentPage := FormFillSetCurrentPage;
    FFormFillInfo.Info.FFI_GotoURL := FormFillGotoURL;
    FFormFillInfo.Info.FFI_GetPageViewRect := FormFillGetPageViewRect;
    FFormFillInfo.Info.FFI_PageEvent := FormFillPageEvent;
    FFormFillInfo.Info.FFI_PopupMenu := FormFillPopupMenu;
    FFormFillInfo.Info.FFI_OpenFile := FormFillOpenFile;
    FFormFillInfo.Info.FFI_EmailTo := FormFillEmailTo;
    // ... FFI_UploadTo through FFI_DoURIActionWithKeyboardModifier
  end
  else if XFA then
  begin
    // Runtime unavailable: keep version 2, leave XFA disabled, tell the host.
    if Assigned(FOnXfaRuntimeMissing) then
      FOnXfaRuntimeMissing(Self);
  end;

  FFormHandle := FPDFDOC_InitFormFillEnvironment(FDocument, FFormFillInfo.Info);
  CheckPdf(FFormHandle <> nil, 'Cannot initialize form fill environment');
  if RuntimeReady then
    FXfaRuntimeUsable := FPDF_LoadXFA(FDocument) <> 0;

Two details in that block are easy to get wrong when you write your own binding. FXfaPageCountOverride is reset to -1 as a sentinel before anything else happens, so PageCount falls back to the static page tree until FFI_PageEvent reports a repagination; a zero there would silently claim an empty document. And each of the version-2 callbacks is a static cdecl routine that recovers the owning TPdf from the record and swallows any Pascal exception before returning to PDFium, which is the discipline our note on hardening the PDFium ABI in Delphi spells out for FFI_OpenFile. Nothing about the version change relaxes either rule

Is version 2 safe when the DLL has no XFA module?

Yes, and the reason is in the record, not in a promise from the library. On a non-XFA build the header says version 2 causes the experimental callbacks to be called as well, so the question is what PDFium finds when it looks. TPdfFormFillInfo is a packed record whose Info member is the complete FPDF_FORMFILLINFO including every version-2 field, and InitializeFormFill clears the whole thing with FillChar before touching a byte. So on a plain pdfium.dll with a plain document the library sees version 2, xfa_disabled set, and NULL in every experimental slot, which is precisely the state the header prescribes for a host that is not implementing XFA. There is no truncated record for the library to read past, because the record was never shorter than version 2 in the first place. The old logic was defending a layout mismatch that the Pascal declaration had already eliminated

The boundary worth stating honestly is the one the record cannot cover. Version 2 on a plain document does not turn on JavaScript, XFA scripting, or any of the host events behind those callbacks. m_pJsPlatform is attached only when V8FeaturesAvailable is true, XFA stays disabled unless RuntimeReady was true, and TPdf.XFA continues to report the form type from FPDF_GetFormType regardless of what the environment negotiated. A host that wants to know whether dynamic XFA will actually render should keep reading XfaRuntimeAvailable after Active goes true, as our note on detecting XFA forms and extracting XFA packets recommends, rather than inferring anything from the version field

procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Fires from InitializeFormFill when the document is XFA but the loaded
  // pdfium.dll cannot run the engine. The form environment still opens,
  // because version 2 was passed either way; only the XFA runtime is off.
  StatusBar.SimpleText :=
    'XFA form detected; restart with pdfium.v8.dll to enable dynamic rendering';
end;

procedure TMainForm.OpenDocument(const FileName: string);
begin
  Pdf.Active := False;
  Pdf.OnXfaRuntimeMissing := PdfXfaRuntimeMissing;
  Pdf.FormFill := True;
  Pdf.FileName := FileName;
  Pdf.Active := True;   // no longer throws on a plain PDF under pdfium.v8.dll
  if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
    ShowStaticXfaWarning;
end;

Protocol version and feature availability are two different axes

The general rule that falls out of this fix is that a version field in a callback structure answers the question "how big is this record and what may you read from it", while feature detection answers "which of those slots will do anything useful". The first is fixed by the native binary and by the Pascal declaration you compiled against. The second varies per document, per DLL export table, and per host configuration. Collapsing the two into one boolean is tempting because the XFA case happens to need both, but the moment a build enforces a minimum version the collapse breaks for every document that does not need the feature. XFA forms, described in ISO 32000-1 §12.7.8 as an XML payload living alongside the AcroForm dictionary, are the feature here; the record layout is the protocol, and PDFium is entitled to insist on the layout before it ever looks at the file. The same shape shows up anywhere a C library versions its structures: a viewer-info block, a render-options record, a platform callback table. The safe pattern is the one the corrected InitializeFormFill follows. Declare the newest layout you understand, clear it completely, set the version to match that layout unconditionally, and then let capability checks decide which slots to populate. If a future PDFium header adds a version 3, the change is to the declaration and to that one assignment, not to a document-dependent branch that will be wrong for whichever combination nobody tested

PDFium Component diagram separating the two axes behind FPDF_FORMFILLINFO: the protocol version fixed by the record layout and the native binary, and feature availability where RuntimeReady gates xfa_disabled, seventeen version 2 slots, FPDF_LoadXFA and m_pJsPlatform per document and per host
A version field describes the memory the other side may read, capability checks decide which slots do anything useful, and collapsing the two into one boolean breaks the build that enforces a minimum

The corrected form-fill initialization ships in PDFium Component for Delphi, Lazarus, and C++Builder, and it applies on Win32 and Win64 alike since both builds share the same record declaration. If your application already selects pdfium.v8.dll for JavaScript-driven AcroForms, this is the change that lets it open the rest of your PDF archive through the same binary without special-casing the form environment