PDFium 元件透過一條固定、有序的搜尋鏈尋找原生程式庫,而不是交給作業系統載入器,因為明確的部署樹才是您除得了錯的部署樹。在 Windows 上,這條鏈尋找安裝程式已隨附的 Win32 或 Win64 子目錄。在其他目標上,它從 Free Pascal 目標巨集建出子目錄名稱,形式為 <cpu>-<os>,所以部署樹讀起來與編譯單元樹完全一致。最後這個決定引入了一個值得整篇文章的錯誤,因為成因是一個大寫字母,症狀卻是一片寂靜
搜尋鏈,按順序
四個位置依序嘗試,然後才把平台載入器當最後手段。第一是偏好配置:執行檔旁的 DLLs 目錄,內含每個目標一個子目錄。第二是替代配置:目標子目錄直接位於執行檔旁。第三是扁平舊式配置:程式庫與執行檔同層、完全沒有子目錄。第四,僅限 Windows,是系統目錄——這裡要小心,因為 32 位元行程必須找 SysWOW64、64 位元行程找 System32,而在 32 位元 Windows 上前者不存在,查找必須回退。以上全部落空之後,才讓載入器自行搜尋
刻意不在非 Windows 平台安排系統目錄步驟。平台載入器自己的搜尋路徑,由執行期連結器設定與程式庫路徑環境驅動,已經涵蓋那片地基,在 Pascal 裡重做一遍,等於重新實作隨發行版而異的規則。Windows 鏈上的故障診斷在部署 PDFium DLL 與診斷載入失敗中單獨說明
子目錄名稱從哪裡來
Windows 上是 Win32 或 Win64,由執行中行程的位元數而不是作業系統的位元數決定,因為決定哪個二進位檔能被載入的是前者。其他所有地方,名稱由編譯器目標巨集建出,讓為兩種架構建置的機器產出兩棵清楚分離的樹,也讓存放原生程式庫的資料夾與存放編譯單元的資料夾同名相鄰
function BuildDllSubDir(UseV8: Boolean): string;
begin
{$IFDEF MSWINDOWS}
if IsWin64 then
Result := 'Win64'
else
Result := 'Win32';
{$ELSE}
// 編譯器巨集把 OS 寫成首字母大寫("Linux"、"Darwin"),
// 而 package 單元輸出目錄不是,所以兩者要大小寫摺疊後才一致。
// 在區分大小寫的檔案系統上,這個差異就是整個查找
Result := LowerCase({$I %FPCTARGETCPU%} + '-' + {$I %FPCTARGETOS%});
{$ENDIF}
end;
為什麼一個大寫字母弄斷了整條鏈
編譯器巨集把目標作業系統拼成首字母大寫:Win64、Linux、Darwin。Lazarus package 把單元輸出寫進以自己的目標變數命名的目錄,那是小寫:win64、linux、darwin。同一件事的兩種拼法,而且在 Windows 上無從察覺,因為檔案系統不區分它們
在 Linux 上它們是兩個不同的目錄。一份把 shared object 放進 DLLs/x86_64-linux 的部署,對尋找 DLLs/x86_64-Linux 的載入器是不可見的,所以鏈上四個明確步驟全部落空,程式碼落到讓平台載入器自行搜尋。有時這碰巧可用——如果程式庫剛好以全系統方式安裝——有時不行,而無論哪種,精心安排的部署樹都沒有貢獻。這個失敗沒有錯誤訊息,因為沒有任何東西失敗:每一步都正確回報了「檔案不在我看的地方」
探測程式:既編譯也執行
這類錯誤靠讀找不到,靠編譯也找不到。驗證一段在開發機上永遠不會編譯的平台分支,慣用手法是:把單元複製到暫存目錄、改名、把平台條件換成一個永不被定義的符號,然後編譯那份複本;如果編得過,那條路徑上的 uses 子句與呼叫簽章至少自洽。對自包含的單元,這招很好用
在這裡不管用。主要繫結單元非常大,又拉進 LCL,沒辦法簡單地關掉 Windows 符號複製編譯。所以改為把變更涉及的那幾個函式逐字謄進一個小的自包含程式,然後把那個程式執行。它印出 x86_64-Win64,不匹配在一行輸出裡看得清清楚楚。只是編譯同一個程式什麼也告訴不了您,因為那個字串完全合法;錯的只是它的值
program ProbeSubDir;
{$MODE DELPHI}
uses
SysUtils;
begin
// 列印,不要斷言。重點是看巨集在這個工具鏈上
// 實際展開成什麼值
Writeln('raw: ', {$I %FPCTARGETCPU%}, '-', {$I %FPCTARGETOS%});
Writeln('folded: ', LowerCase({$I %FPCTARGETCPU%} + '-' +
{$I %FPCTARGETOS%}));
end.
一般教訓:當跨平台變更關乎某物的值而不是型別時,只編譯的驗證不是驗證。把它印出來。Delphi 與 Free Pascal 之間更廣的跨編譯器差異彙整在Delphi 與 FPC 跨編譯器陷阱文章
讓平台自己解釋它的載入失敗
載入器的 Windows 分支手動列舉載入可能失敗的原因,因為那裡有用的區分——架構不符、缺遞移相依、路徑解析不了——對應到值得逐一命名的錯誤碼。非 Windows 上,可攜式載入器單元已經回傳一條涵蓋同樣範圍的描述字串,所以非 Windows 分支直接用它,而不是從一個在不同系統上意義不同的錯誤號碼重新推導類別
克制把兩者正規化成一則訊息的衝動是刻意的。載入失敗是部署問題,讀訊息的人需要平台自己的詞彙去搜尋
一個會遞迴的名稱相撞
還有一個又小又利的陷阱。可攜式載入器單元匯出一個叫 UnloadLibrary 的程序,繫結單元裡也有一個同名程序,它在釋放 handle 之前做自己的簿記。在那個程序內部,不帶限定的 UnloadLibrary 呼叫解析到當前單元裡那個,於是呼叫自己。修法是以單元名稱限定呼叫
這與 Free Pascal 移植中普遍主導的識別字遮蔽問題同一形狀:Windows 單元匯出整數型的最小、最大函式遮蔽浮點版,匯出一個同步型別遮蔽同名類別,而每種情況的解析都取決於 uses 子句的順序。在呼叫點限定名稱,是那個不依賴日後有人維護順序的修法
部署檢查清單
路徑算術正確之後,大多數載入失敗歸結為三件事。架構必須匹配行程而不是機器,所以 64 位元 Windows 上的 32 位元應用程式需要 32 位元二進位檔。啟用 V8 的建置檔名不同,混用兩者的部署看起來正確卻什麼也載不進來。而且一個系統目錄同一時間只能容納一個變體,這正是偏好明確子目錄配置、而非把任何東西安裝到全系統的好理由
就 Lazarus 而言,把原生程式庫放在執行檔旁的小寫 DLLs/<cpu>-<os> 下,它在每個目標上都會被鏈的第一步找到。在 Lazarus 上演練這一切的檢視器範例見Lazarus 與 FPC 檢視器文章,目前的平台支援列於 PDFium Delphi component 產品頁