HotXLSはWindows上のFree PascalとLazarusでビルドできます。移植の成否を分けたのは、Object Pascalの構文とは何の関係もない4つの決定です。コアをDELPHIUNICODEモードで維持すること、OLE構造化ストレージのインターフェースを、参照カウントを手動管理するCORBAインターフェースとして宣言すること、Win32用AESオブジェクトファイルをPascal実装に置き換えること、そして切り詰められたZIPを完全なものとして受け入れてしまうinflateループを修正すること
成熟したDelphiライブラリを移植したことがあれば、この手の作業の輪郭は分かるはずです。最初のパスでは、コンパイラはほぼすべてを受け付けます。その後に続くのは、クリーンにコンパイルが通るのに間違った結果を生む挙動の違いの長い尾です。そしてスプレッドシートエンジンは、1本のコードパスでテキストエンコーディング、COM構造化ストレージ、圧縮、暗号に触れるため、これらの影響を受け方が異常に大きいのです
コアはなぜ素のDELPHIではなくDELPHIUNICODEにこだわるのか
数式エンジンがStringとCharにUTF-16のセマンティクスを担わせているためで、ANSIの代替では、何もファイルに届く前に文字が失われます。コアをFPCのDELPHIモードでビルドしたくなるのは分かります。ほとんどの移植が最初に手を伸ばす互換スイッチですし、コードは通るのです。しかし中国語のシート名やキリル文字ラベルを持つワークブックが計算パスを往復すると、ライターに届く時点では文字が消えています。エラーはどこにも出ません
モードはライブラリ全体で統一されていませんが、これは意図的であって、だらしないわけではありません。PNGのバイトデコーダとLCLオーバーライドは、バイトと、ウィジェットセットから渡されるものを扱うため、ANSIシグネチャが本当に必要です。これらのユニットは別個のLX_FPC_ANSIスイッチを有効にします。1ライブラリに2モードというのは、代替案が入力をテキストとして扱うバイトデコーダだと気づくまで、コードの臭いに聞こえるものです
後で人を引っかける付随的な細部もあります。FPCランタイムでは、DELPHIUNICODEはTFormatSettings.DecimalSeparatorをWideCharにしてくれません。Unicodeの小数点セパレータを含む入力は、まずUnicode文字列の内部でASCIIセパレータへ正規化し、セパレータが期待値と一致しない入力は、パーサが認識できなかった文字の位置で黙って切り詰めるのではなく、拒否しなければなりません
program ExportReport;
{$MODE DELPHI}
uses
Interfaces, // 先頭に置くこと:LCLウィジェットセットを初期化する
SysUtils, lxHandle; // 続いてUTF-8変換レイヤー
var
Book: TXLSWorkbook;
begin
Book := TXLSWorkbook.Create(nil);
try
Book.LoadFromFile('input.xls');
Book.Sheets[0].AsString[1, 1] := 'Quarterly summary';
Book.SaveToFile('output.xls');
finally
Book.Free;
end;
end.
Interfacesユニットは省略できず、先頭に置く必要があります。LCLウィジェットセットとUTF-8変換レイヤーを初期化するのはこのユニットで、フォント、ファイルパス、テキストのいずれかがRTLとLCLの境界を越えた瞬間から、HotXLSは両方に依存します。これを省いたコンソールプログラムはコンパイルは通りますが、ASCII以外のパスですべて誤動作します。ここから、コンパイルの成功があまり多くを語らない理由も分かります。実際のフォント名、実際のパスを持つ実際のドキュメントが完全な往復を果たして初めて、移植が動いていると証明できたのです
クラスのVMTはCOMのvtableではない
Free Pascalは、クラスのVMTをCOMインターフェースのvtableとしてWindowsに渡すことを許しません。宣言がDelphiが受け付けるものと同じに見える場合でもです。レイアウトの違いにより、間違ったスロットへの呼び出しが発生し、それは呼び出し箇所とは無関係の場所でのクラッシュとして現れます。構造化ストレージがここで問題になるのは、クラシックなバイナリワークブック形式がOLE複合ファイルであり、読み書きするにはWindowsストレージAPIがコールバックしてくるILockBytesの実装が要るからです
うまくいく構成は、COMスロットを明示的に宣言し、AddRefとReleaseを手動管理するCORBAインターフェースです。これらの型については自動参照カウントを諦め、寿命の責任を負うことになります。1つのユニット内に収まる少数のインターフェースなら、割に合う取引です。この作業内部の特定の罠がQueryInterfaceです。インターフェースポインタを返す必要があり、オブジェクトポインタを返してはいけません。どちらもコンパイルは通ります。片方は、先頭のマシンワードがvtableではないアドレスをWindowsに渡します
FPC固有の宣言はlxOleInterfaces.incに置かれています。FPCソースディレクトリのlxAESBackend.inc、lxZlibBackend.incの隣です。コンパイラ固有の選択がエンジン中に散らばるのではなく、1か所に座っているわけです。形式そのものと、ライブラリがそれをどう辿るかについては、PascalでOLE2複合ファイルを読むで解説しています
同じ仲間に属する型の細部がもう1つあります。FPCブランチではLargeIntはInt64に解決されるようにし、Compのコンパイラ上の分類は2つのツールチェーンで十分に異なるため、オーバーロード解決が別の候補を選び得ます。大きなオフセットの挙動は、HGLOBALストリームではなくファイルストリームでテストしてください。Windowsのグローバルメモリストリームは4 GiBを越えるシークで自分でラップするため、そこで通るテストは自分の算術については何も証明しません
自己整合的なAES実装が隠すもの
DelphiビルドがリンクするWin32用AESオブジェクトファイルはOMF形式で、Free Pascalのリンカは消費できません。そのためFPCブランチはPascal実装のAESを使います。Delphiは従来どおりのオブジェクトファイルをリンクし続けるので、既存顧客にとってリリース済みバイナリは変わりません
検証要件は、どんなプロジェクトにも持ち込む価値のある部分です。同じ実装で暗号化して復号しても、何も証明されません。鍵スケジュールが間違っていても、ブロック順序が違っても、チェイニングが違っても、対称アルゴリズムは完璧に自己整合的で、自分の出力を毎回正しく往復させます。これを捕まえられるのは既知回答ベクトルだけです。鍵拡張、ブロック順序、CBCチェイニングを公開値と照合します。自己整合的な間違った実装を出荷すると、症状が現れるのは、顧客がExcelでそのファイルを開いた最初の瞬間です
圧縮には性格の違う欠陥がありました。Pascalのinflateバックエンドは、圧縮入力をすべて消費し終わった後でも出力を抱えていることがあります。呼び出し側は、ストリームが終端を報告するまで呼び続けなければなりません。入力の枯渇をストリーム終端と扱うと、最後のブロックが切り詰められます。さらに悪いことに、破損したアーカイブが黙って受理される状態になり、これはまさにZIPエンドオブセントラルディレクトリレコードの検証の堅牢化が防ぐために存在する失敗形態です。ルールはこうです。進捗ゼロかつ未完了は切り詰めエラーであって、EOFでは決してない
実際に何時間も奪う2つのビルドシステムの罠
LCLのサーチパスはFPCパッケージのワイルドカードパスより先に置く必要があります。そうしないとFree VisionのMenusユニットが同名のLCLユニットを覆い隠し、どちらのことでもないPPUチェックサム不一致が出ます。インストール後に移動されたLazarus環境は、fpc.cfgに古いパスを残していることもあります。そのためビルドのエントリポイントは、環境が提供するものを継承せず、ユニットとバイナリのパスを明示的に指定します
2つ目の罠はPascalとは無関係です。LF改行で書かれた.cmdバッチファイルは、ファイルがインタプリタの読み取りバッファのサイズを越えて大きくなるまでは動きます。そこを越えると、call :labelがバッチラベルが存在しないと主張して失敗し、失敗は境界を越えてたまたま座っていたプログラムのところに現れます。バッチスクリプトを書き換えるツールはすべて、CRLFで書き戻さなければなりません。さらにlazbuild --build-allはコンパイル前にパッケージのユニット出力ディレクトリをクリアするため、そこに置かれたオプションファイルは読まれる前に消されます。外に置くこと。そして@パスはパッケージディレクトリ基準で解決されることも覚えておいてください。lazbuildはそこからコンパイラを呼ぶのです
// Lazarusグリッドエクスポート:TGridToXLSはLazarusパッケージに同梱で、
// 同じDBグリッドエクスポートコードがLCLアプリケーションでもそのまま動く
var
Exporter: TGridToXLS;
begin
Exporter := TGridToXLS.Create(nil);
try
Exporter.DBGrid := GridOrders;
Exporter.WorksheetName := 'Orders';
Exporter.ExportHeader := True;
Exporter.SetColumnsWidth := True;
Exporter.ExportDBGrid;
Exporter.SaveAs('orders.xls');
finally
Exporter.Free;
end;
end;
コンパイラ警告にはどんな価値があるか
Free Pascalは、Delphiが報告しないローカル変数の未初期化を報告します。FPCビルドを回したことで、この違いが計算ユニットの実際の欠陥2件になりました。1つの関数は、使用前に一度も代入されていないカウント変数を読んでおり、別の関数は、ある分岐で2つの座標を、それらを計算するコードが別の分岐で走る前に使っていました。Delphiの下では両者とも、スタックがたまたま保持していた内容どおりに振る舞います。1台では再現し、別の1台では再現しないバグの定義です
実用的な結論はこうです。主に1つ目のコンパイラで出荷する製品であっても、2つ目のコンパイラをループに留めておく価値があります。FPCの警告クラスを定期的に眺めることは、Delphiコードベースに対する安価な静的解析パスであり、どんなテストスイートも確実には届かないカテゴリの欠陥を見つけます。これが置かれている、より広いバージョンマトリクスの規律については、クロスコンパイラビルドマトリクスで説明しています
Windows向けのFree PascalとLazarusサポートは、Delphiパッケージ、C++Builderパッケージと並ぶLazarusパッケージとして、フォークではなく同じソースツリーからビルドされ、HotXLS Delphiスプレッドシートコンポーネントに同梱されています。これがこの取り組みの要点です。1つのエンジン、4つのツールチェーン、そしてコンパイラ固有の決定はインクルードファイルに隔離され、一度に読み通せる場所に置かれています