技術記事

あらゆるターゲットでの PDFium ネイティブライブラリのロード

PDFium コンポーネントは、ネイティブライブラリを固定された順序付きの検索チェーンで見つけます。OS ローダーに任せるのではありません。明示的な配備ツリーは、デバッグできる配備ツリーだからです。Windows では、このチェーンはインストーラがすでに同梱する Win32 または Win64 のサブディレクトリを探します。他のターゲットでは、Free Pascal のターゲットマクロから <cpu>-<os> の形でサブディレクトリ名を組み立てます。配備ツリーがコンパイル済みユニットツリーとまったく同じように読めるようにするためです。この最後の判断が、記事全体の値があるバグを持ち込みました。原因は大文字 1 つであり、症状は静寂でした

チェーン、順番に

4 つの場所を順に試し、その後、最後の手段としてプラットフォームローダーを使います。1 つ目は推奨レイアウトで、実行ファイルの隣の DLLs ディレクトリが、ターゲットごとに 1 つのサブディレクトリを含みます。2 つ目は、ターゲットサブディレクトリが実行ファイルのすぐ隣にある別のレイアウトです。3 つ目はフラットなレガシーレイアウトで、ライブラリがサブディレクトリなしに実行ファイルの隣に座っています。4 つ目は Windows のみ、システムディレクトリです。32 ビットプロセスは SysWOW64 を、64 ビットプロセスは System32 を見なければならず、32 ビット Windows では前者が存在しないため検索がフォールバックしなければなりません。このすべての後に初めて、ローダー自身に検索が依頼されます

Delphi 向け PDFium ネイティブライブラリ検索チェーンの図。DLLs ターゲットサブディレクトリから、別レイアウト、フラット、Windows システムディレクトリの各レイアウトを経てプラットフォームローダーまで。
OS ローダー自身に検索が依頼される前に、4 つの明示的な場所が順に探査されます

Windows 以外にシステムディレクトリのステップがないのは意図的です。ランタイムリンカーの設定とライブラリパス環境に駆動される、プラットフォームローダー自身の検索パスがすでにその地面をカバーしており、Pascal でそれを複製するのは、ディストリビューションごとに変わる規則を再実装することを意味します。Windows のチェーンでの障害の診断は、PDFium DLL の配備とロード障害の診断で別途扱っています

サブディレクトリ名の出所

Windows では Win32Win64 で、OS のビット数ではなく実行中のプロセスのビット数で決まります。どのバイナリをロードできるかを決めるのはこちらだからです。その他の場所では、名前はコンパイラのターゲットマクロから組み立てられます。2 つのアーキテクチャ向けにビルドするマシンが、明確に分かれた 2 つのツリーを生み出すためであり、ネイティブライブラリを保持するフォルダが、同じ名前でコンパイル済みユニットを保持するフォルダの隣に座るためです

function BuildDllSubDir(UseV8: Boolean): string;
begin
{$IFDEF MSWINDOWS}
  if IsWin64 then
    Result := 'Win64'
  else
    Result := 'Win32';
{$ELSE}
  // コンパイラマクロは OS を頭大文字にする("Linux"、"Darwin")が、
  // パッケージのユニット出力ディレクトリはそうしないため、両者は
  // 小文字化の後に初めて一致する。大文字小文字を区別するファイル
  // システムでは、この違いが検索のすべてである
  Result := LowerCase({$I %FPCTARGETCPU%} + '-' + {$I %FPCTARGETOS%});
{$ENDIF}
end;

大文字 1 つがチェーン全体を壊した理由

コンパイラマクロはターゲット OS を頭大文字で綴ります。Win64LinuxDarwin です。Lazarus パッケージはユニット出力を、自身のターゲット変数から名付けられたディレクトリへ書きます。そちらは小文字です。win64linuxdarwin。同じものの 2 つの綴りであり、ファイルシステムが区別しない Windows では気づく方法がありません

Linux では 2 つの異なるディレクトリです。共有オブジェクトを DLLs/x86_64-linux へ置く配備は、DLLs/x86_64-Linux を探すローダーには見えません。チェーンの明示的な 4 ステップすべてが外れ、コードはプラットフォームローダーに検索を任せることへ落ちます。ライブラリがたまたまシステム全体にインストールされていれば機能することもあり、そうでないこともあります。いずれにせよ、入念に整えられた配備ツリーは何も寄与しません。この障害にエラーメッセージがないのは、何も失敗していないからです。すべてのステップが、ファイルは自分が見た場所になかったと正しく報告しました

FPC ターゲットマクロの大文字 1 つが Linux で PDFium DLL の検索を壊す仕組み。ローダーは DLLs/x86_64-Linux を探し、配備されたフォルダは DLLs/x86_64-linux であり、一致するのは大文字小文字を区別しない Windows だけ。
同じターゲットの 2 通りの綴りは、Windows では一致し、大文字小文字を区別するファイルシステムでは黙って外れます

プローブプログラム、コンパイルされそして実行される

この種のバグは読んで見つかりません。コンパイルでも見つかりません。開発マシンで決してコンパイルされないプラットフォーム分岐を検証する常用の技法は、ユニットを一時ディレクトリへコピーし、名前を変え、プラットフォーム条件を決して定義されないシンボルへ置き換えて、そのコピーをコンパイルすることです。コンパイルが通れば、その経路の uses 節と呼び出しシグネチャは少なくとも自己整合的です。自己完結したユニットにはこれがよく機能します

ここでは機能しません。メインのバインディングユニットは非常に大きく、LCL を引き込むため、Windows シンボルを切ったまま単純にコピーしてコンパイルすることはできません。そこで代わりに、変更が触れた一握りの関数をそのまま小さな自己完結プログラムへ書き写し、そのプログラムを実行しました。x86_64-Win64 と出力され、不一致が 1 行の出力で見えました。同じプログラムをコンパイルしても何も分かりませんでした。文字列は完全に有効であり、誤っているのは値だけだからです

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 分岐は、システムごとに違う意味を持つエラー番号からカテゴリを再導出する代わりに、それを直接使います

この 2 つを 1 つのメッセージへ正規化したい衝動に抗うのは意図的です。ロード障害は配備の問題であり、メッセージを読む人は、それを検索するためのプラットフォーム自身の語彙を必要とします

再帰する名前の衝突

もう 1 つの罠があり、小さくて鋭いです。ポータブルローダーユニットは UnloadLibrary という procedure を公開し、バインディングユニットは同じ名前の procedure を持ち、ハンドルを解放する前に自分の後片付けをします。その procedure の内側で、修飾されていない UnloadLibrary の呼び出しは現在のユニットのものへ解決され、自分自身を呼びます。修正は、呼び出しをユニット名で修飾することです

これは、Free Pascal 移植全般を支配する識別子シャドウイングの問題と同じ形です。Windows ユニットは、浮動小数点版を隠す整数型の最小・最大関数を公開し、同名のクラスを隠す同期型を公開しており、いずれの場合も解決は uses 節の順序に依存します。呼び出し箇所を修飾することは、誰かが後でその順序を守り続けることに依存しない修正です

PDFium コンポーネントの UnloadLibrary 名前衝突。Delphi バインディングユニット内の修飾なし呼び出しは自分自身へ再帰し、ユニット修飾された呼び出しはポータブルローダーユニットへ届いてハンドルを解放する。
呼び出し箇所を修飾すると、解放はバインディングユニットへの再帰の代わりに、ローダーユニットを通って送られます

配備チェックリスト

パスの計算が正しくなった後も、ロード障害の大部分は 3 つのことに帰着します。アーキテクチャはマシンではなくプロセスと一致しなければならず、64 ビット Windows 上の 32 ビットアプリケーションは 32 ビットバイナリを必要とします。V8 有効ビルドは異なるファイル名を持ち、それらを混ぜた配備は正しく見えて何もロードしません。そして 1 つのバリアントだけが、ある時点でシステムディレクトリに住めます。何かをシステム全体にインストールするよりも明示的なサブディレクトリレイアウトを好む、良い理由です

Lazarus については特に、ネイティブライブラリを小文字で DLLs/<cpu>-<os> の下に、実行ファイルの隣へ置いてください。そうすればすべてのターゲットでチェーンの第 1 ステップが見つけます。Lazarus でこれを試すビューアサンプルはLazarus と FPC のビューアの記事に述べており、現在のプラットフォーム対応は、PDFium Delphi component の製品ページに一覧があります