PDFlibPas 3.538.0では、外部JBIG2エンコーダーをFree PascalとLazarusのプログラムへ静的リンクできます。プロジェクトでDelphiとC++Builderがすでに使っている同じPDFlibJBIG2EncCユニットを追加すると、エンコーダーは実行ファイルの中に入り、同梱が必要な追加ファイルはなくなります。これは、この機能について以前「Free Pascalから外部エンコーダーへ到達するにはDLLしかない」と結論づけていた見方を覆します
なぜDLLが唯一の選択肢に見えていたのか
DLLしかないように見えたのは、3つのリンク経路がそれぞれ無関係な理由で失敗し、どのコンパイラースイッチもそこへ届かなかったからです。内部リンカーは連想COMDATセクションをそのまま拒否します。バンドルされたbinutilsを通す外部リンクは、Free Pascalが64ビットWindowsターゲットで無条件に渡すセクションのガベージコレクション中にクラッシュします。新しいbinutilsはFree Pascalのリンクスクリプトをまったく処理できません。C++側を別のツールチェーンで再ビルドすると、1つの拒否が別の拒否に置き換わります。テンプレートとinlineのインスタンス化は仕様上weak externalシンボルを出力するため、Free PascalはそれらをUnsupported COFF symbol type 105として報告します。これらの証拠はどれも間違っておらず、JBIG2エンコーダーバックエンドとFree Pascalリンカーの以前の説明では、現在でも再現する行き止まりを1つずつたどっています。間違っていたのは、修正を置ける場所についての前提でした。試みはすべてコンパイラーかリンカーを通り、どちらもオブジェクトファイルにすでに入っているものを変えられません。問題はずっとオブジェクトファイルにありました。ObjConvはCOFFを読み、COFFを書き出します。そしてFree Pascalがつまずくすべての構造には、受け入れられる機械的な置き換えがあります
原因を決して名前に出さないエラー
Free Pascalの内部リンカーはpick-any COMDATを途中までしか実装しておらず、その中途半端な実装がここで最も診断しにくい部分です。形式が意図する通り、重複する定義は折りたたみます。しかしセクションを使用済みとしてマークするとき、TExeOutput.RemoveUnreferencedSectionsはexesymbolを経由して勝った定義へリダイレクトする一方、TCoffexeoutput.DoRelocationFixupはobjreloc.symbol.objsectionを直接読みます。使用中のセクションが、自分のオブジェクト内で定義されているものの折りたたみで負けたコピーにあるシンボルを参照すると、2つのパスが別のセクションを見てしまい、リンクはInternal error 200603061で停止します
これを両側の2つの制限と比べてみてください。Unsupported COFF symbol type 105はweak externalを意味します。Associative or exact match COMDAT sections are not yet supportedは連想COMDATを意味し、問題のシンボルまで名前に出します。内部エラー200603061は何も言いません。シンボル名も、セクション名も、ファイル名も、フェーズもありません。しかもこれは特殊なケースではなく通常のケースです。MSVCはすべての文字列リテラルとすべてのinlineまたはtemplateのインスタンス化をpick-any COMDATへ入れます。このエンコーダー一式の186オブジェクトでは、リンカーが2656回の折りたたみを実行しました。/Gy-で通常の関数を関数単位のCOMDATセクションから外しても、文字列リテラルとtemplateのインスタンス化は元の場所に残ります
CRTシンボルのstubを埋めると最後の追加が原因に見える理由
リンカーはすべてのシンボルが解決してからでないとfixupフェーズへ進まないからです。まだ何かが不足している間はUndefined symbolで早期終了し、COMDAT問題が表面化する機会はありません。最後のCランタイムstubを埋めると、リンカーは次のフェーズへ進み、まっすぐ内部エラー200603061に入ります。そのため現場の症状は系統的に誤解を招きます。参照されたCシンボルのPascal本体を1つずつ追加すると、いつも最後に追加したものがビルドを壊したように見え、あるいは100個前後のstubを超えたところにしきい値があるように見えます。どちらも真実ではありません。最後にどのシンボルを入れたかも、合計いくつ入れたかも関係ありません。失敗は最初のオブジェクトから潜んでおり、解決が成功して到達可能になっただけです。無関係なものを直した後にリンカーの訴えが変わったら、回帰を起こしたのではなくフェーズを進めた可能性を考えてください
修正はコンパイラーフラグではなく1回のObjConvパス
修正全体は、コンパイル済みの各オブジェクトに対して実行する1つの後処理コマンドです。ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_。この作業で3つのオプションが追加されました。-xwはIMAGE_SYM_CLASS_WEAK_EXTERNALシンボルを通常のexternalへ解決します。-xnは_fltusedのようなIMAGE_SYM_CLASS_NULLシンボルを正規化します。Free PascalがこれをUnsupported COFF symbol type 0として報告するためです。-xcが本丸です。すべてのCOMDATセクションを通常のセクションへ降格し、そこで定義されるシンボルをstaticにします。連想.pdataと.xdataのunwind sectionも一緒に通常化されます。COMDATセクションがなければfoldingも、あるパスがリダイレクトして別のパスが見失う勝ちコピーもなくなるため、決定そのものを取り除くことで失敗を消します。正当にマージできたコピーまでそれぞれ残るため、コストはありますが小さいものです
-np:__imp_:pdflibimp_という接頭辞の名前変更は別の衝突を解決します。MSVCはindirection cellを__imp_*という名前で呼び、Free Pascalはその接頭辞を独自のimport機構用に予約しているため、これらの名前をそのまま定義すると同じ内部エラー200603061が発生します。cellの名前を変えることで、Pascal側はそれらを通常の変数として公開し、実行時に埋められます。オブジェクト自体は/GS- /Gs999999 /Gy- /Zl /GR- /EHs-c-というstatic-linkフラグを付け、image codecも無効にしてコンパイルします。これにより、使用されないファイルI/Oとcodecの経路に必要なリンク専用stubが大幅に減ります。オブジェクトはLib\thirdparty\Win64fに置かれ、DelphiとC++Builderの経路は独自のWin64xセットを変更せずにリンクします。1つのツールチェーンに限定した移植性修正として、これは正しい結果です
Pascal側でなおexportすべきもの
Free PascalはCオブジェクトのimportをシンボル名で解決するため、その名前を明示する必要があります。したがってCのエントリーポイントを代替するPascalルーチンにはすべて明示的なpublic name句が付きます。Delphiはルーチン名をシンボル名として使うため句は不要です。このため、両コンパイラーに1つのユニットを使い、句だけを{$IFDEF FPC}の下に置けます。罠は、external 'msvcrt.dll'宣言では何も満たせないことです。これはimportを作るだけで、リンクされたオブジェクトがbindできるdefinitionは作りません。forwarding bodyが実在しなければなりません
// 外部宣言はimportだけを作る。リンクされたオブジェクトは
// これにbindできない
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
external 'msvcrt.dll' name 'memcmp';
// 正確なCシンボル名で公開したPascal本体に、オブジェクト一式が
// 実際にbindする
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
public name 'memcmp';
begin
Result := crt_memcmp(Buf1, Buf2, Count);
end;
可変長引数のエントリーポイントはこのパターンを壊します。Pascal wrapperは自分のvarargsを別のvarargs calleeへ転送できないからです。解決策はwrapperであることをやめることです。C名でnaked routineをexportし、callerが用意した引数レジスターとstackをそのまま使って本来の実装へtail-jumpします。JPEG 2000層ではすでにsnprintfとvsnprintfをこの方法で処理しています。plain nameはUCRTだけがexportするため、underscore付きのmsvcrt表記へjumpします。同じ内部エラーに由来する関連制約もあります。名前を変更したimport cellはGetModuleHandleAとGetProcAddressを使い、initializationセクションからstatic initializerではなく埋めます。initializerでimportされたroutineのアドレスを取ると、コンパイラーが処理できないfixupを出力して、再び200603061で失敗するためです
function crt_sprintf(Buffer: PAnsiChar; Fmt: PAnsiChar): Integer; cdecl; varargs;
external 'msvcrt.dll' name 'sprintf';
var
SPrintfTarget: Pointer = @crt_sprintf;
// varargsはPascal wrapperから転送できないため、exportされた
// シンボルはcallerが組み立てたframeをそのまま使ってtail-jumpする
procedure jbig2_sprintf; assembler; nostackframe;
public name 'sprintf';
asm
jmp qword ptr [rip + SPrintfTarget]
end;
Free Pascalプロジェクトで変わること
uses句のユニット名以外は何も変わらず、配布すべきファイルもなくなります。バックエンドは自身のinitializationセクションからRegisterJBIG2EncoderBackendを通じて登録され、呼び出し側は以前と同じように使えます。options bit PDF_JBIG2_OPTION_EXTERNAL_ENCODERを通す方法で、値は4です。または拡張エントリーポイントのUseExternalEncoder引数を使います。ただし要求は保証ではなく希望のままです。ユニットを含めずにビルドした場合は、native Pascal MMR encoderへ静かにフォールバックし、エラーではなく大きなファイルを生成します
uses
Classes, SysUtils,
PDFlibrary,
PDFlibJBIG2EncC; // Delphi、C++Builder、および3.538.0以降のFree Pascal
var
Pdf: TPDFlib;
Scan: TStream;
ImageId: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.NewDocument;
Pdf.NewPage;
Scan := TFileStream.Create('scan-page-1.tif', fmOpenRead);
try
// Interpolate、SymbolExtract、UseExternalEncoder、SkipBlackDots、
// BlackDotSize、LossyLevel
ImageId := Pdf.AddImageJBIG2FromStreamEx(Scan, 0, 1, 1, 0, 0, 0);
finally
Scan.Free;
end;
if ImageId = 0 then
raise Exception.Create('JBIG2 encoding failed');
Pdf.SaveToFile('archive.pdf');
finally
Pdf.Free;
end;
end;
2つの制限は明確に述べておく価値があります。Win64のオブジェクトセットしか存在しないため、他のすべてのFree Pascalターゲットでは外部encodeエントリーポイントが失敗を返し、native Pascal encoderが処理を引き受けます。もう1つ、この機能全体を通す回帰条件はサイズチェックではなくrender比較です。同じ入力に対して両方のエンコーダーはlosslessなので、出力をrenderしてbyte単位で比較します。Lazarus suiteはこのテストを含め26件中26件に合格します。圧縮されたstreamサイズを比べても何も証明できません。反転したページは正しいページとほぼ同じサイズに圧縮されるからです
より広い教訓はJBIG2を越えて一般化できます。DLL、ActiveX、dylibの統合面が担うように境界が本当に動的なら、DLLは正しい形です。一方、COFF readerの回避策にすぎないなら、毎回のinstallerにファイルを追加し、毎回のdeploymentに検索パスを追加し、static linkingにはないversion-skewの失敗モードを追加するため、間違った形です。上流も重要です。二値画像がどのように作られるかのほうが、最終サイズについてエンコーダー以上に決めるからです。Delphiでの領域ベースのモノクロレンダリングでは、そのパイプラインの半分を扱っています。ツールチェーンの対応範囲、コンパイラーごとのオブジェクトセット、対応ターゲットはlosLab PDF Developer Library製品ページに掲載されています