あなたのpdfium.dllは正常に読み込まれ、それでも1つのプロシージャが欠落している。PDFium Componentは、そのバインディングを2つのクラスに分割することでこれを処理する:読み込みを完全に中断するCheckGetProcAddressで解決される必須エクスポートと、nilポインタとケーパビリティチェックを代わりに残すTryGetProcAddressで解決される任意エクスポートだ
これは見つからないDLLと同じ問題ではない。アプリケーションが不正なEXE形式エラー、ファイルの欠落、アーキテクチャの不一致で死ぬなら、そのストーリーはpdfium.dllの展開とロード失敗の診断に関する併載記事で語られている。ここではローダーは成功している。モジュールハンドルは有効で、何百ものエクスポートが解決され、それでも1ページ目がレンダリングされる前に実行が終わる。新しいPDFiumビルドで到着した1つのエントリポイントが、ディスク上のバイナリには存在しないからだ
なぜ1つの欠落エクスポートがライブラリ全体を壊すのか
必須バインディングはハードな契約であり、1回のオールオアナッシングなバインドシーケンスの中で施行されるからだ。PDFium Componentは、自身のエクスポートテーブル全体をLoadLibraryの中で、CheckGetProcAddress呼び出しを次々と重ねて解決する。最初のnilの結果はEPdfErrorを発生させ、その前にUnloadLibraryを呼び出すが、これは意図的なものだ:部分的なバインドはそうしなければ、すでに解決済みのポインタを、まさに解放されようとしているモジュールに向けたまま残してしまい、下流にあるすべてのAssignedガードを静かに無効化してしまう
この帰結が、人々をここに連れてくる失敗モードだ。あなたはコンポーネントをアップグレードし、2年間出荷し続けてきたのと同じpdfium.dllを出荷し、アプリケーションは起動しない。エラーは、あなたが一度も呼んだことのない機能のためのエクスポートを名指しする。呼び出し側でできることは何もない。呼び出し側のコードはそもそも実行されないからだ。失敗はバインディング中に発生していた——ドキュメントが開かれるずっと前だ
function CheckGetProcAddress(const Name: string): Pointer;
begin
Result := GetProcAddress(PDFiumLibrary, PChar(Name));
if Result = nil then
begin
// A missing required export means the deployed pdfium.dll is older
// than this build of the binding. Drop every pointer resolved so far
// so no caller can reach into the module we are about to free.
UnloadLibrary;
raise EPdfError.Create('Required PDFium export not found: ' + Name);
end;
end;
function TryGetProcAddress(const Name: string): Pointer;
begin
// Optional export. nil is a legitimate answer here; every caller is
// required to test Assigned() before dereferencing the variable.
Result := GetProcAddress(PDFiumLibrary, PChar(Name));
end;
必須か任意か:境界線が実際にあるところ
PDFium Componentが適用するルールは率直だ。エクスポートは、それが欠落しているとコンポーネントが自分の存在理由である仕事を実行できなくなる場合に必須であり、その欠落が1つの末端機能だけを取り除く場合に任意だ。FPDF_InitLibrary、FPDF_LoadDocument、FPDF_RenderPageBitmap、FPDF_ClosePageは必須であり、それらについて大きな音を立てて失敗するのは正しい:レンダリングできないビューアは劣化したビューアではなく、壊れたビューアだ
今日、寛容なローダーを通じて到達するものはすべて末端だ。FPDFBookmark_GetColorはM109以降に到着し、アウトラインエントリの任意の/C色配列を供給するだけなので、それより前のDLLは単にブックマークの色がないと報告する。V8ヘルパーのFPDF_GetRecommendedV8FlagsとFPDF_GetArrayBufferAllocatorSharedInstance、そしてXFA文字列ヘルパーのFPDF_BStr_Init、FPDF_BStr_Set、FPDF_BStr_Clearは、構造上、非V8ビルドには存在しないので、それらを必須として扱うと、通常のpdfium.dllを読み込み不能にしてしまう。そしてこの記事のきっかけになったペア:FPDFAttachment_SetDescriptionとFPDFAttachment_GetDescriptionは、2026-07-13に上流で追加され、それはこのプロジェクトがDLLs/Win32とDLLs/Win64の下で出荷する4つのPDFiumバイナリすべてのビルド日付より後だ。この最後のケースは、単発の問題ではなく問題の一般的な形だ:バインディング層は継続的に動く上流ヘッダーを追跡しているが、あなたのインストーラーにあるDLLは、誰かがそれを再ビルドするたびにしか離散的に動かない。Pascal側が、展開済みのバイナリが持っていないエクスポートを知っているウィンドウが常に存在し、新しいエクスポートが必須と任意のどちら側に落ちるかを事前に決めておくことだけが、そのウィンドウを生存可能にするものだ
FPDFDoc_GetAttachmentCount := CheckGetProcAddress('FPDFDoc_GetAttachmentCount');
FPDFDoc_AddAttachment := CheckGetProcAddress('FPDFDoc_AddAttachment');
FPDFAttachment_GetName := CheckGetProcAddress('FPDFAttachment_GetName');
FPDFAttachment_GetStringValue := CheckGetProcAddress('FPDFAttachment_GetStringValue');
// Attachment descriptions were added after the bundled DLL revision.
// Keep them optional so older deployments continue to load.
FPDFAttachment_SetDescription := TryGetProcAddress('FPDFAttachment_SetDescription');
FPDFAttachment_GetDescription := TryGetProcAddress('FPDFAttachment_GetDescription');
FPDFAttachment_SetFile := CheckGetProcAddress('FPDFAttachment_SetFile');
FPDFAttachment_GetFile := CheckGetProcAddress('FPDFAttachment_GetFile');
呼び出し側でケーパビリティゲートは何をすべきか
非対称であるべきであり、その非対称性こそが設計のすべてだ。実行できない読み取りには正直な空の答えがある。実行できない書き込みにはまったく正直な答えがないので、それは大きな音を立てて失敗しなければならない。PDFium Componentは、添付ファイルの説明プロパティを正確にその線に沿って分割しており、その分割こそが、欠落したエクスポートが黙ったデータ損失に変わることを止めているものだ。TPdf.GetAttachmentDescriptionはAssigned(FPDFAttachment_GetDescription)をテストし、空のWStringで抜ける。それは嘘ではない:そのエクスポートを持たないDLLでは、コンポーネントはその添付ファイルが/Descエントリを持っているかどうか本当に判断できないので、空の説明は、それを一度も持ったことがない添付ファイルと同じ読み方になる。添付ファイルAPIの残りは、DelphiでPDF添付ファイルを扱う記事で扱っており、手つかずで動作し続ける
TPdf.SetAttachmentDescriptionは逆の道を取る。同じAssignedテストに対してCheckを呼び出し、「Attachment descriptions are not supported by the loaded PDFium DLL」というテキストとともにEPdfErrorを発生させる。ここで黙って戻ることは最悪の選択肢だろう:呼び出し側は説明を設定し、エラーを得ず、ファイルを保存し、説明が単純に存在しないPDFを出荷することになる。下流のコンシューマーがそれがどこへ行ったかを尋ねるまで、誰も気づかない
function TPdf.GetAttachmentDescription(Index: Integer): WString;
begin
CheckActive;
Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
Result := '';
// Read side degrades: an old DLL cannot report /Desc, and '' is
// indistinguishable from an attachment that carries no description.
if not Assigned(FPDFAttachment_GetDescription) then
Exit;
// ... two-pass buffer sizing against FPDFAttachment_GetDescription ...
end;
procedure TPdf.SetAttachmentDescription(Index: Integer; const Value: WString);
begin
CheckActive;
Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
// Write side refuses: silently dropping the value would produce a file
// the caller believes carries a description and does not.
Check(Assigned(FPDFAttachment_SetDescription),
'Attachment descriptions are not supported by the loaded PDFium DLL');
// ... FPDFDoc_GetAttachment, then FPDFAttachment_SetDescription ...
end;
機能を提供する前にケーパビリティを調べる
例外を捕まえることは、あなたの展開が何をできるかを発見する貧弱な方法なので、PDFium Componentは同じテストを名前付きの関数として公開している。AttachmentDescriptionFeaturesAvailableはLoadLibraryを呼び出し、ペアの両半分が解決したかどうかを返す。これはV8FeaturesAvailable、XfaBStrHelpersAvailable、XfaFeaturesAvailableと並んでおり、それぞれの任意グループについて同一のパターンに従っている。プローブに名前を付けることは、見た目以上に重要だ:AttachmentDescriptionFeaturesAvailableという名前のブール値は、この機能が展開されたバイナリに条件付きであることを次の保守者に伝えるが、プロパティセッターの奥に埋め込まれた裸のAssignedテストではそれが決して伝わらない。それはまた、UI層にバインドできるものも与える。つまり説明編集ボックスは、保存時に入力を受け付けて拒否するのではなく、あらかじめ無効化される
procedure TAttachmentFrame.SyncCapabilities;
begin
// Ask once, at form setup, instead of discovering the limit on save.
DescriptionEdit.Enabled := AttachmentDescriptionFeaturesAvailable;
if not DescriptionEdit.Enabled then
DescriptionEdit.TextHint := 'Requires a newer pdfium.dll';
end;
procedure TAttachmentFrame.SaveDescription(Pdf: TPdf; Index: Integer);
begin
if not AttachmentDescriptionFeaturesAvailable then
Exit;
Pdf.AttachmentDescription[Index] := DescriptionEdit.Text;
end;
なぜバインディングのカバレッジはツールで証明されなければならないのか
数字はもはや人間が信頼できる域を超えているからだ。PDFium Componentは、21の公開PDFiumヘッダーを2026-07-29の上流ベースラインに照らして監査し、470個のエクスポートされたC ABI関数を見つけた。バインディングはすでにそのうち468個をカバーしていた。誰もヘッダーを読んでその2つの隙間を見つけたわけではない。スクリプトが1秒でそれを見つけ、次の上流バンプでも再び見つけるだろう。tools/audit_pdfium_public_api.pyは意図的に小さい:公開ディレクトリのすべてのヘッダーにわたってFPDF_EXPORT ... FPDF_CALLCONV name(を正規表現でマッチさせ、PDFium.pas内のすべてのCheckGetProcAddress('Name')とTryGetProcAddress('Name')を正規表現でマッチさせ、2つの集合差を出力する:バインディングを持たないエクスポートにはmissing、上流にもはや存在しないエクスポートを持つバインディングにはstaleだ。どちらかの集合が空でなければ非ゼロで終了するので、これは追加の儀式なしにビルドステップに組み込める。現在の結果は470件中470件がバインド済み、missing 0、stale 0だ
staleの方向は、missingの方向と同じくらい役に立つ。上流が削除したエクスポートは、将来のすべての読み込みをハードに失敗させ続けるCheckGetProcAddress行を残し、その種の腐敗は誰かがDLLを更新する日まで見えないままだ。手動レビューは、あなたが考えていた関数を見つける。あなたが考えていなかった関数は見つけない。監査が両方のローダーをカバレッジとして意図的にカウントしていることにも注意してほしい。それはAPIドリフトにとって正しい判断であり、必須/任意の分割が、誰かがその行を追加した副産物ではなく、文書化された決定でなければならない理由だ
任意バインディングが正直でなくなる場所
率直に述べておく価値のある境界が2つある。このパターンは過剰適用しやすいからだ。1つ目は、nil関数ポインタが安全なのは、それに触れるあらゆる経路が文字通り最初にAssignedをテストする場合だけだということだ。何百ものcdecl関数変数を宣言しているユニットでは、1つのガードされていない呼び出しが、スタックトレースの中では何も意味しないアドレスでのアクセス違反になる。C境界をまたぐ呼び出し規約とライフタイムを支配するのと同じ規律がここにも当てはまり、それはPDFiumバインディングをABIとメモリ安全性の欠陥に対して強化する記事の主題だ
2つ目の境界はスコープだ。任意バインディングは、すべてを寛容にする一般的なライセンスではない。もしFPDF_RenderPageBitmapが任意だったら、コンポーネントは喜んで読み込まれ、その後すべてのページで失敗し、1つの明確な起動エラーを、明白な原因のない実行時エラーの散乱に変えてしまうだろう。必須がデフォルトとして正しい。任意は、機能が本当に末端であり、その欠落が読み取り側に擁護できる劣化した挙動を持ち、書き込み側がその理由を名指しするメッセージとともに拒否できる場合に手を伸ばす例外だ
ここで説明したローダー設計、ケーパビリティプローブ、監査ツールは、Delphi・C++Builder向けPDFium Componentの一部として出荷される。製品ページには、同梱されているPDFiumバイナリと、それらが公開する完全なAPIサーフェスが列挙されている