當 pdfium.dll 拒絕在客戶的電腦上載入時,適用於 Delphi 和 Lazarus 的 PDFium 元件會將難懂的原生錯誤轉換為具體原因。CheckLoadLibrary 常式會將 ERROR_BAD_EXE_FORMAT (193) 解讀為 32/64 位元架構不符,且 CheckGetProcAddress 會將缺失的匯出(export)回報為 pdfium.dll 版本比繫結(binding)更舊。這兩條訊息都指明了修正方法,而無需讓您瞎猜
這非常重要,因為原生 DLL 的失敗有三種不同的方式,而作業系統的原始文字會將它們全部混為一談。可能是架構不正確、部署的二進位檔案對於呼叫漸進式編譯器來說太舊,或者兩個執行緒在同一時刻競爭對其進行繫結。每種情況都有不同的修正方法,而在 v2.11.0 載入生命週期工作中加入診斷的全部目的,就是告訴您實際遇到的究竟是哪一種
為什麼 pdfium.dll 載入失敗並出現錯誤的 EXE 格式錯誤?
Windows 錯誤 193(ERROR_BAD_EXE_FORMAT)表示已找到並開啟檔案,但其 PE 機器類型與主機程序不符。64 位元可執行檔無法載入 32 位元 pdfium.dll,而 32 位元可執行檔無法載入 64 位元 pdfium.dll。由於檔案確實存在,因此尋找缺失 DLL 的直覺會引導您走向完全錯誤的方向
PDFium 元件在 CheckLoadLibrary 中攔截了這種情況:當 LoadLibrary 傳回 Null 控點且 GetLastError 為 193 時,它會附加一條提示,指出已找到 DLL,但其架構與主機程序不相符,並指向相對應的 DLLs/Win32 或 DLLs/Win64 建置版本。子目錄由 BuildDllSubDir 選擇,當 IsWin64 為 True 時傳回 Win64,否則傳回 Win32。將 DLL 部署在繫結實際搜尋的目錄樹下,架構不相符的問題就會消失
uses
SysUtils, PDFium;
procedure OpenDocument(const AFileName: string);
var
Pdf: TPdf;
begin
// 將 pdfium.dll 部署在可執行檔旁,與建置目標相符合:
// <AppDir>\DLLs\Win32\pdfium.dll 適用於 32 位元主機程序
// <AppDir>\DLLs\Win64\pdfium.dll 適用於 64 位元主機程序
Pdf := TPdf.Create(nil);
try
Pdf.FileName := AFileName;
try
Pdf.Active := True; // 首次使用時延遲繫結 pdfium.dll
except
on E: EPdfError do
// 該訊息已經指明了真實原因:32/64 位元架構不符
// (ERROR_BAD_EXE_FORMAT)或 pdfium.dll 版本比此繫結更舊
raise Exception.CreateFmt('PDF engine did not start: %s', [E.Message]);
end;
if Pdf.Active then
RenderFirstPage(Pdf);
finally
Pdf.Free;
end;
end;
缺失 PDFium 匯出函數說明了什麼?
第二類失敗是版本不一致(version skew)。隨時間推移,PDFium 會發布新的匯出函數,且 Pascal 繫結在 LoadLibrary 期間透過 CheckGetProcAddress 解析其所需的每個函數。如果缺少必要的匯出函數,說明繫結比磁碟上的 pdfium.dll 更新,實際診斷是部署的 DLL 已過期而非損毀。CheckGetProcAddress 函數明確指出了這一點:它會引發 EPdfError,回報部署的 pdfium.dll 版本比此 PDFiumPas 繫結建置版本更舊,並指定了相對應的二進位檔案所屬的 DLLs/<subdir> 路徑
兩個細節使該路徑非常穩健。首先, CheckGetProcAddress 在引發錯誤之前會呼叫 UnloadLibrary,因此部分繫結絕不會留下解析到一半的函數指標,防止下一次嘗試時發生錯誤。其次,它回報的 DLL 名稱來自 BuildDllName,在一般情況下傳回 pdfium.dll,當設定了全域 EnableV8Engine 旗標時傳回 pdfium.v8.dll。如果您的應用程式針對 XFA 或腳本表單啟用了 V8 JavaScript 引擎,錯誤文字會指向 V8 建置版本而非一般版本,因此您能在第一時間替換正確的檔案
從背景執行緒載入 PDFium DLL 是否安全?
現在從任何執行緒載入函式庫都是安全的;但從多個執行緒呼叫 PDFium API 依然不安全。這是兩個獨立的保證,將它們分開是穩定工作執行緒集區與間歇性當機之間的差別。背景轉譯通常會透過 TPdfFuture<T> 工作執行緒來執行每個頁面的轉譯,而第一個觸及 TPdf 的 Future 會觸發延遲繫結。如果沒有保護,兩個工作執行緒可能會同時觀察到未載入的函式庫、同時執行繫結序列、覆寫模組控點,並洩漏第一次載入的模組
v2.11.0 的工作透過 PDFiumLoadLock 關閉了這個空檔,這是一個程序全域的 TRTLCriticalSection,包裝了 LoadLibrary 和 UnloadLibrary 的進入點。該關鍵區段使「檢查後載入」序列具有不可分割性(atomic),因此不會有兩個執行緒同時看到 Loaded=False 並進行雙重繫結,也不會有執行緒在另一個執行緒繫結期間釋放 DLL。該鎖定是在單元的 initialization 區段中建立,並在 finalization 區段中終止,受 PDFiumLoadLockReady 旗標保護,因此即使在關機期間這種配對也是安全的。如果您需要包含取消功能的完整工作與回覆模式,隨附文章使用可取消 Future 進行背景轉譯提供了端到端的引導
uses
PDFium, FPdfAsync;
// 工作執行緒方法在背景執行緒中執行。第一個繫結的 Future
// pdfium.dll 由 PDFiumLoadLock 序列化,因此第二個同時進行的
// 工作執行緒無法雙重繫結或洩漏模組控點。
function TReportForm.RenderThumbnail(
const AToken: IPdfCancellationToken): TBitmap;
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Active := True; // 安全的並行繫結
AToken.ThrowIfCancelled;
Result := Pdf.Thumbnail; // 該 TPdf 僅由一個執行緒擁有
finally
Pdf.Free;
end;
end;
procedure TReportForm.StartRender;
begin
TPdfFuture<TBitmap>.Run(RenderThumbnail, ThumbnailReady);
end;
procedure TReportForm.ThumbnailReady(
const AResult: TPdfFutureResult<TBitmap>);
begin
if AResult.IsSuccess then
Preview.Picture.Assign(AResult.Value);
end;
載入鎖定無法保護的範圍
此處的界限值得清楚說明,因為很容易過度解讀此修正。PDFiumLoadLock 僅序列化全域繫結狀態:模組控點和 FPDF_* 函數指標分配。正如上游標頭檔所宣告的,底層的 PDFium C API 依然不是執行緒安全的,因此呼叫 FPDF_* 函數或跨執行緒共用一個 FPDF_DOCUMENT 仍然是您自己需要負責序列化的職責。安全的形式如上所示:每個工作執行緒擁有自己的 TPdf,且絕不將動態文件傳遞給另一個執行緒。若要深入瞭解該界限及其周圍的 ABI 規則,請參考增強 PDFium VCL 繫結以抵禦 ABI 和記憶體錯誤
為什麼結束處理(Finalization)和 FPDF_DestroyLibrary 十分重要
在關機路徑中曾存在一個更隱蔽的漏洞。PDFium 單元之前沒有 finalization 區段,這意味著 FPDF_DestroyLibrary 在程序結束時從未執行;作業系統收回了 DLL 記憶體並跳過了 PDFium 自身的清理工作,即工作執行緒彙整(join)、設定了 EnableV8Engine 時的 V8 Isolate 銷毀,以及字型和快取的拆除。在單純的轉譯工作負載中,這只是結束時的悄悄洩漏,但在啟用 V8 引擎的情況下,每次執行都會洩漏一個 JavaScript Isolate,這在重複的自動化作業中會迅速顯現出來
現在 finalization 區段會呼叫 UnloadLibrary,這會叫用 FPDF_DestroyLibrary 並釋放模組。這之所以存在於單元範圍是有原因的:TPdf.Destroy 僅將 Active := False 設定為關閉目前文件,且它刻意不卸載全域函式庫,因此多執行個體(multi-instance)應用程式在整個程序生命週期中會保持一個共用的 PDFium 繫結活動。將拆除工作置於 finalization 中意味著共用函式庫在單元卸載時只會釋放一次,且 UnloadLibrary 會先檢查其 Loaded 旗標,因此如果從未使用過 PDFium,該呼叫將是無害的無作業(no-op)
簡短的部署檢查清單
三種習慣可以防止現場幾乎所有的載入失敗。使 DLL 架構符合主機程序,並將其放置於 DLLs/Win32 或 DLLs/Win64 下;使 pdfium.dll 與繫結版本保持一致,以防缺少必要的匯出函數;且絕不跨執行緒共用 TPdf 或文件控點,即使繫結本身現在是不可分割的。如果您針對 Delphi 和 Lazarus 建置相同的原始碼,導致跨目標部署出錯的封裝差異已在關於Delphi 和 FPC 跨平台編譯器陷阱的筆記中介紹。此處說明的診斷、載入鎖定和結束處理清理功能均隨附於適用於 Delphi 和 C++Builder 的 PDFium 元件 中