技術記事

Pascal PDF ライブラリのサイレントスタブ障害の診断

Delphi ライブラリがビジュアルフレームワークなしのビルド構成を手に入れるとき、バグが住み着くのは代替クラスです。プラットフォームでもコンパイラでもなく、代役です。PDFlibPas には、VCL なしのビルド向けにビットマップ、キャンバス、フォント、メタファイル、プリンターの同等物を提供するグラフィックスレイヤーがあり、Free Pascal への移植は、代役が持ち得るあらゆる障害モードを表面化させました。これらは診断コストできれいに並び、その順序は直観の示すものと正反対です

例外を投げる代役は発見が安価です。例外がメソッド名を告げるからです。空のデータを返す代役は高くつきます。障害が原因から数層離れたところに現れるからです。成功を返す代役は最悪です。戻りコードは有効で、エラーコードはゼロで、例外は投げられず、何かが誤っていた唯一の証拠は出てきたバイトの中にあるからです

Pascal PDF ライブラリにおけるサイレントスタブ障害の 3 形状。診断コスト順に、例外を投げるスタブから空出力の上の成功まで。
例外を投げる代役の診断は安価、空データは高価、そして空の成果物の上の成功返却が最も発見しづらくなります

形状 3:空の XObject の上の有効な画像識別子

ベクターメタファイル変換機能は、non-VCL 構成では空の procedure 本体でした。その上のものはすべて動き続けます。EMF 取り込みエントリポイントとキャンバス取得エントリポイントは最後まで走り、正当な画像識別子を返し、呼び出し側はそれをページへ配置しました。ファイルに着地したのは、コンテンツ長ゼロのフォーム XObject でした。ページは白く描画されました

問題を報告するものは何もなく、それにはこの機能向けのライブラリ自前のデモプログラムも含まれます。白紙ページを描いて、気づきもしませんでした。確認すべき失敗戻り値は存在しませんでした。呼び出しの連鎖は本当にすべて成功したからです。誤っていた唯一のものは、生成されたストリームのサイズでした。この種の欠陥の診断とは、別の問いを立てることです。「呼び出しは失敗したか」ではなく「成果物はもっともらしいか」です。長さゼロのフォーム XObject、ゼロピクセルの画像、コンテンツバイト数ゼロのページ。これらを捕まえるアサーションです

修正は 2 つの半分からなり、後半は忘れがちです。まず、空の実装に例外を投げさせ、障害に少なくとも伝達経路を与えます。次に、その例外を画像ファクトリで null 結果へ変換し、画像識別子を消費する 2 か所に null チェックを追加します。さもないと「きれいな失敗」は、ページツリーが無を参照しようとして、そのままアクセス違反へ変わります。投げるスタブが改善になるのは、呼び出し側が、これまで受け取れなかった失敗を受け取れる準備をしていたときだけです

形状 2:空データ、クラッシュから 3 層

メタファイルキャンバスの代役は、物理寸法を埋めていませんでした。その値はページジオメトリ計算の除数に入るため、計算はゼロを生み、バウンディングボックス計算はゼロ除算になりました。素の例外ハンドラがそれを飲み込み、画像ファクトリは null 結果を返し、アクセス違反は最後に、null が使われたページツリーで起こりました。原因と症状の間に 3 層あり、途中の例外ハンドラが証拠を消しています

PDFlibPas の空のメタファイルキャンバス代役の障害連鎖。ゼロ除算の 3 層先でアクセス違反に至る。
空の寸法がジオメトリ計算をゼロへ割り、素のハンドラが例外を消し、null 識別子がページツリーをクラッシュさせます

同じユニットには、このパターンがさらに 2 つありました。フォントクラスには空の Assign とコンストラクタ本体がありました。見た目以上に重要です。キャンバスのフォントプロパティは読み取り専用であり、そこへ代入することがフォントを届ける唯一の方法だからです。空の実装はフォント選択を黙って無効にし、テキストは既定の何かで出てきます。そして 1 インチあたりピクセル数の値ゼロにより、フォントメトリクスからキャンバスをサイズする呼び出し側はすべて、0x0 のキャンバスを生み出しました。白紙ページと成功返却に至る経路です

// 代役ユニットで探すべき形状:例外も投げず何もしないメソッド。
// どちらもコンパイルが通り、どちらも出力なしの「成功」を生む
procedure TMetafileCanvasStandIn.Create(...);
begin
  // inherited 呼び出しなし、フィールド初期化なし
end;

function TBitmapStandIn.LoadFromStream(Stream: TStream): Boolean;
begin
  Result := True;    // そしてビットマップは空のまま
end;

最初の 1 文字しか保持しないワイド構造体

これは代役の問題ではまったくありません。しかし症状が原因から同じだけ離れているため、同じ目録に属します。プリンター列挙構造体は、12 個すべての文字列メンバーが 1 バイト文字へのポインタとして宣言されていました。一方、それを埋める関数は列挙 API のワイド文字バリアントです

ポインタサイズは同一であるため、構造体レイアウトは正しく、クラッシュも何も起こりません。代わりに起こるのは、UTF-16 文字列を 1 バイト文字列として読むと最初のゼロバイトで停止することです。ASCII のプリンター名であれば、それは 2 文字目の上位バイトです。すべてのプリンター名は正確に 1 文字になって返ってきました。下流では、名前検証が失敗し、プリンター作成が失敗し、マシン上のすべての実在プリンターで印刷が失敗しました。そしてどの症状も構造体宣言を指していません

ワイド Win32 構造体が PWideChar ではなく PAnsiChar メンバーで宣言された結果、UTF-16 プリンター名が 1 文字へ切り詰められる様子。
1 バイトメンバーは UTF-16 の名前を最初のゼロバイトまでしか読みません。そのためすべてのプリンター名は正確に 1 文字で返ります
// 誤り:サイズは正しく、要素型が誤り。コンパイルエラーもクラッシュも
// なく、すべての文字列が 1 文字へ切り詰められる
type
  TPrinterInfo2Wrong = record
    pServerName: PAnsiChar;
    pPrinterName: PAnsiChar;
    // ... 他 10 個
  end;

// 正しい:*W 構造体は全体がワイドメンバー
type
  TPrinterInfo2W = record
    pServerName: PWideChar;
    pPrinterName: PWideChar;
    // ... 他 10 個
  end;

ここから出てくる規則は機械的で、考えずに適用する価値があります。名前が W で終わる Win32 構造体については、すべての文字列メンバーがワイドバリアントであることをフィールドごとに確認する。ANSI とワイドの世界を混ぜても、コンパイラの診断もクラッシュも生まれず、黙った切り詰めだけが生まれます。同じことが逆に ANSI バリアントにも当てはまります

素の例外ハンドラこそが本当の敵

これらの調査はすべて、同じ構成物によって減速されました。すべてを捕捉して false 戻り値へ変換するハンドラです。画像デコーダの周りにこれを書くのは妥当です。壊れた画像が文書ジョブを落とすべきではないからです。しかし同時に、必要な唯一の情報を削除する装置でもあります

実務上の対応は、ハンドラを一時的に大声にすることです。デバッグ条件の下で、素のハンドラの内側から例外クラス、メッセージ、バックトレースをダンプすれば、説明のつかない null 返却は、位置情報を持つ名前付き例外へ変わります。上の 3 ケースのうち 2 つでは、この 1 歩だけで調査が終わりました。例外がゼロ除算か、名前がすべてを語る代役メソッド内のアクセス違反だったからです

代役経路を採用するときのチェックリスト

4 つの項目を、効果の順に。代替クラスを呼び出す前に、使おうとしているメソッドを読み、それぞれに実体のある本体があることを確認してください。空の本体は実装の細部ではなく、欠けている機能です。中立な値を返す代役よりも例外を投げる代役を選び、それと対になる null チェックを、ファクトリが正当に無を返し得る場所へ置きます。機能の検証は戻りコードではなく成果物の検査によって行ってください。ここの障害モードの全体が、空の成果物の上のきれいな戻りコードだからです。文書が実際に何を含むかのバイトレベルの内訳を見るのが最速であり、ファイルサイズ監査の記事がそのツールを扱っています。そして、機能に実行可能な代替実装がないときは、影響を受けるサンプルを機能する経路へ振り分け、コメントで理由を書いてください。黙って白紙出力を生むデモを放置するのではなく

より広い論点は、1 つのライブラリをはるかに超えて当てはまります。条件付きの第二実装、モックレイヤー、ヘッドレスモード、プラットフォームシムを持つコードベースはどれも、形状 3 に晒されています。これがこれほどよく隠れる理由は、チームが普段頼るすべての品質ゲート、戻りコード、エラーコード、例外、終了ステータスがステータスチャネルであり、形状 3 はそのすべてをきれいに保つからです。裏切るのは出力だけです。これは、信頼できない入力を扱うときにステータスではなく成果物を確認する理由でもあり、信頼できない PDF 解析の記事に述べています。また、1 つのエンジンを信用するのではなく複数エンジンのレンダリング出力を比較する理由でもあり、マルチエンジンレンダリングに述べています

PDFlibPas は Delphi、C++Builder、Free Pascal 向けのネイティブ Object Pascal PDF ライブラリであり、non-VCL 構成こそがヘッドレスとクロスツールチェーンのビルドを可能にしています。現在の構成対応状況は、losLab PDF Developer Library の製品ページに一覧があります