技術記事

HotPDFとFree Pascal・Lazarus:Win64サポートの限界

サポートチケットへの短い答えは、はい、ただし制限付きです。HotPDF 2.730.0はFree Pascal 3.2.2とLazarus 4.6上のWin64でビルドでき、コアの生成、読み込み、保存の各パスは動作します。続かないのは、静的リンクされたネイティブコーデックオブジェクトやDelphiの無名メソッドに依存するすべてです

質問はたいてい同じ形で届きます。クロスプラットフォームツールのためにLazarusへ標準化したチームや、Free Pascalコードベースを引き継いだチームが、すでにDelphi向けにライセンス済みの同じPDFコンポーネントを使いたいというものです。成熟したDelphiライブラリの移植は、構文の問題であることはめったにありません。興味深いのは、移植がライブラリが密かに一つのツールチェーンと結びついていた箇所を露呈させる点で、この場合その結びつきは二つの非常に具体的な場所にあります。同梱コーデックのオブジェクトファイルABIと、バージョンシンボルの裏に隠れたコンパイラ機能です

Delphi版HotPDFビルドとFree Pascal 3.2.2+Lazarus 4.6のWin64ビルドを比較する能力マトリクス。どのドキュメントパスが共有され、どのコーデック、圧縮、並列描画、無名メソッドAPIが例外送出スタブに行き着くかを示す
コアの生成、読み込み、保存の各パスは両ビルドで同一で、差は静的リンクコーデックと無名メソッドAPIに完全に集中している

HPDFDocがコンパイルされるまでにFree Pascal 3.2.2が必要とするもの

HotPDFがFree PascalでコンパイルされるのはDelphiモードのみで、しかもLazarusのLCLユニットディレクトリが検索パス上にある場合に限られます。どちらも交渉の余地がありません。HotPDF.inc{$IFDEF FPC}ブロック内で{$MODE DELPHI}{$H+}によってコンパイラを切り替え、FPC_FULLVERSIONが30202未満のときは{$FATAL}で古い環境を拒否します。3.0.x環境は壊れたユニットを生む代わりに大声で失敗します。LazarusランタイムパッケージHotPDFLaz.lpkが残りをコード化します。必須パッケージとしてのLCLと、カスタムオプションとしての-Mdelphiです

コンソール出力しか望まない人にとってLCL要件は意外ですが、これは構造的なものです。HPDFFPCCompatはFree Pascalに等価物のないDelphi VCL型を供給し、TMetafileTMetafileCanvasをLCLのビットマップとキャンバスクラスへ対応付け、TRichEditTMemoへ別名付けします。HPDFDocTPNGObjectGraphics.TPortableNetworkGraphicへ別名付けします。これらは機能の同等化ではなくコンパイル時のshimと捉えるべきです。ビットマップで裏付けられたメタファイルクラスはユニットをコンパイル可能に保つだけで、メタファイルの各パスをDelphi上と同じ挙動にはしません。GUIなしのスモークテストでさえInterfacesを引き込み、ビルドスクリプトはlcl\units\x86_64-win64とlazutils出力ディレクトリに対して-Fuを渡します

D2009+がバージョンゲートを兼ねられない理由

Free Pascalビルドをモダンコンパイラとして扱い、最新のDelphi機能シンボルを単純に定義したくなるものです。HotPDFはそうしません。その理由は率直に述べる価値があります。D2009+はUnicode文字列だけを意味するのではなく、公開APIを無名メソッドで表現するユニットへのゲートでもあります。Free Pascal 3.2.2はDelphiの無名メソッドもそれらのAPIもサポートしないため、シンボルを借りてくるとコンパイル不能なコードを引き込むことになります。そこでHPDFDocのuses節は二つの独立した条件テールを持ち、その間の重複は偶然ではなく意図的なものです

uses
  // ...
  HPDFJavaScript,
  HPDFFormCalcGraph
{$IFDEF FPC}
  , HPDFFPCCodecStubs,
  HPDFCMS,
  HPDFWinCertSigner
{$ENDIF}
{$IFDEF D2009+}
  , HPDFXFARuntime,
  HPDFCMS,
  HPDFWinCertSigner,
  HPDFSignVerify,
  HPDFSignatureBatch
{$ENDIF};

ネイティブコーデックがリンカで止まる理由

一つの特定ツールチェーンが生成したWin64 COFFオブジェクトであり、Win64上のFree Pascalのリンカはどちらもこれを消費できないからです。内部リンカでも外部GNU ld経路でもなく。これはオブジェクトファイルABIの問題であってPascalの問題ではないため、条件付きソースの修正ではどうにもなりません。ライブラリは取れる唯一の正直な道を選びました。静的コーデックオブジェクトを引き込むすべての{$L}ディレクティブを{$IFNDEF FPC}で包み、Free Pascalビルドでは単純に省略し、HPDFFPCCodecStubsが各欠落シンボルを、値を返す代わりに例外を送出するスタブとして供給します

// HPDFFPCCodecStubs.pas
function HPDFFPCNativeCodecUnavailable: PtrUInt;
begin
  raise ENotSupportedException.Create(
    'This native codec is not available in the Free Pascal build');
end;

function HPDFFPCStub_deflate: PtrUInt; cdecl;
  public name 'deflate';
begin
  Result := HPDFFPCNativeCodecUnavailable;
end;

このスタブテーブルは長く、読めば本日時点でどの機能がDelphi専用かが正確に分かります。zlib-ngとzopfliのdeflateエントリポイント、libjpegの圧縮と展開、OpenJPEGのJPEG 2000コーデック、libtiffと圧縮ごとの初期化子、JBIG2のエンコードとデコード、Little-CMSの色変換エントリポイント、そしてAESプリミティブです。リストそのものよりスタブという設計選択の方が重要です。リンク時の欠落シンボルは、触ってもいないユニットから未定義参照の壁として現れます。一方ENotSupportedExceptionを送出するスタブは、動くビルド、理由を名指すメッセージ、呼び出し箇所を指すスタックトレースを与えます。これはまた、Delphiビルドなら正しいバイト列を生む場所で、Free Pascalビルドが黙って誤ったバイト列を生むことが決してないことも意味します。二次的な効果にも注意してください。信頼できない画像コーデックを隔離プロセスで実行するのはDelphiビルドでのみ生じる判断です。Free Pascalビルドには、そもそもサンドボックス化すべきプロセス内ネイティブデコーダーが存在しないからです

DelphiではHotPDFの静的コーデックオブジェクトがリンクされネイティブ実行される。Free PascalのWin64ビルドはリンクディレクティブを省略し、各欠落外部シンボルを、呼び出し箇所で名前付き例外を送出するスタブへ迂回させる
リンクディレクティブを省略し全外部シンボルをスタブ化することで、未定義参照の壁を、動作し自らの限界を名指すビルドに変える

圧縮:最初に変える一行はcmNone

他を移植する前に、CompressioncmNoneへ設定してください。THPDFCompressionMethodが提供する値はcmNonecmFlateDecodeの二つだけで、後者はFree Pascalビルドでスタブ化されたdeflateエントリポイントへ一直線につながります。まず圧縮を切った状態でコアのオブジェクトモデルを検証し、それから他に何が必要かを判断します。同梱スモークテストが採る順序もそれです。非圧縮の1ページ文書を生成し、再読み込みし、ページ数が1に戻ることを確認します。非圧縮出力は大きくなりますが、それでも完全に有効なPDFです

program HotPDFLazarusSmoke;

{$mode delphi}
{$H+}

uses
  Interfaces, SysUtils, HPDFDoc;

var
  Pdf, Reloaded: THotPDF;
  OutputFile: string;
  PageCount: Integer;
begin
  OutputFile := IncludeTrailingPathDelimiter(GetTempDir) +
    'HotPDF-FPC-Smoke.pdf';
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := OutputFile;
    Pdf.Compression := cmNone;   // cmFlateDecodeはスタブ化されたシンボルへ届く
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(72, 72, 0, 'HotPDF Free Pascal smoke test');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;

  Reloaded := THotPDF.Create(nil);
  try
    PageCount := Reloaded.LoadFromFile(OutputFile);
    if PageCount <> 1 then
      raise Exception.CreateFmt('Expected one page, got %d', [PageCount]);
  finally
    Reloaded.Free;
  end;
end.

並列ページ描画はどうなるのか

コンパイルは通り、正しいビットマップは返りますが、並列ではなくなります。THotPDF.RenderLoadedPagesParallelTHotPDF.RenderLoadedPagesParallelOrderedはインラインprocedureクロージャ付きのTThread.CreateAnonymousThreadの上に構築されており、Free Pascal 3.2.2はこれを表現できません。そこでFree Pascal分岐は決定論的な逐次フォールバックを実行します。ページインデックスを順に辿り、各ページにRenderLoadedPageToBitmapを呼び出し、成功数を数えます。APIの形状、戻り値、出力配列は不変で、これにより単一コードベースが両方の方法でビルドできます

同一のHotPDF並列描画呼び出しが、Delphiでは重なり合うワーカースレッドで走り、Free Pascalではページインデックスを逐次辿る。パイプラインinfoレコードはフォールバックを隠さずワーカー数1を報告する
Free Pascal分岐はAPI形状と出力配列を保ったままワーカー数1を報告するため、infoレコードを既に読んでいるコードは真実を見る
var
  Bitmaps: THPDFBitmapArray;
  Info: THPDFParallelRenderPipelineInfo;
  Rendered: Integer;
begin
  Rendered := Pdf.RenderLoadedPagesParallel([0, 1, 2, 3], 150, 4,
    Bitmaps, Info);
  // Delphi: Info.WorkerCountはメモリ予算が許した値になる
  // Free Pascal: Info.WorkerCountは常に1、ページはインデックス順
  if Info.WorkerCount = 1 then
    LogSerialFallback(Rendered, Info.RequestedWorkerCount);

フォールバックは黙っていません。ここが設計上の肝です。THPDFParallelRenderPipelineInfoを正直に埋めます。リクエストに基づくPageCount、要求した値をそのまま返すRequestedWorkerCount、1に設定されるWorkerCount、そして実際に戻ってきた数と一致する完了数と配信数です。プログレスバーやメモリ予算のサイズ決めのために既にInfoを検査しているコードは動作し続け、想定ではなく真実を読みます。スループット計画が並列描画パイプラインとその背圧モデルに依存しているなら、その計画はDelphiの計画です。Free Pascalでは、ページをビットマップへ描画するシングルスレッドコストにページ数を掛けて予算計上してください

実際に出荷すべきビルドはどちらか

選ぶ基準は好みではなく能力です。ワークフローが文書の組み立て、テキストとベクター描画、フォーム入力、読み込みと保存で構成されるなら、Win64のFree Pascalビルドで足ります。何かを有効化する前に、圧縮を切った状態で検証すべきです。JPEG、JPEG 2000、TIFF、JBIG2画像、ICC色変換、圧縮出力、多数のコアに依存するスループットが絡むなら、当面はDelphiまたはC++Builderに留まってください。境界線はオブジェクトファイルABIと欠落言語機能が引いており、どちらもソース上に見え、どちらも誤った結果ではなく名前付きエラーで失敗します

Free PascalとLazarusのパッケージはDelphiおよびC++Builderユニットと同じディストリビューションに同梱されるため、一つのライセンスが両方をカバーし、コミットする前に自分の文書でLazarus経路を試せます。現在のコンパイラサポートマトリクスと完全なAPIリファレンスはHotPDF Delphi PDF Component製品ページにあります