Technical Article

針對 C++Builder 交叉編譯 Delphi 元件:避免未解決的外部符號

當維護一個用 Delphi 撰寫但同時供 Delphi 與 C++Builder 使用者消費的 VCL 元件時,您很快就會意識到這兩個建置系統對待相依性的方式截然不同。一個在 Delphi 中編譯完美的單元 (unit),可能會在 C++Builder 中導致災難性的連結器錯誤。這種差異對於元件作者來說是個常見的陷阱,尤其是在將新的內部單元加入現有函式庫時。

以下是發生這種情況的詳細原因分析 (使用 HotXLS 元件的實際範例),以及如何使用明確包含 (explicit includes)、{$HPPEMIT} 與 pragma 連結來確保您的交叉編譯過程萬無一失。

陷阱:隱式編譯與明確包含

假設您建立了一個新的 Delphi 單元 lxXlsSummary.pas 來處理文件詮釋資料,並在主要解析單元 lxRead.pasuses 子句中引用了它。您在 Delphi IDE 中點擊編譯,建置通過,然後發布了更新。

第二天,您的 C++Builder 使用者回報在連結階段出現錯誤:Unresolved external 'XlsReadSummaryInformation' referenced from lxRead.obj

Delphi 方式 (dcc32)

當 Delphi 編譯器處理套件 (.dpk) 時,它會查看 contains 子句中明確列出的單元。如果其中一個單元 uses 了一個外部單元 (例如 lxXlsSummary.pas) 但該單元不在 contains 清單中,Delphi 編譯器會執行隱式靜態連結 (implicit static linking)。它只會在搜尋路徑中找到 .pas 檔案,將其編譯為 .dcu,然後將其封裝到產生的 .bpl 中。建置成功了,這完全掩蓋了遺漏的問題。

C++Builder 方式 (MSBuild / .cbproj)

C++Builder 的建置系統嚴格得多。它只會為明確列在 .cbproj 檔案中 <DelphiCompile> 項目群組內的 Delphi 單元產生 C++ 目的檔 (.obj) 與標頭檔 (.hpp)。因為 lxXlsSummary.pas 從未在專案檔中明確註冊,所以不會建立 lxXlsSummary.obj。當連結器嘗試解決 lxRead.obj 所發出的呼叫時,由於符號缺失,就會導致未解決的外部錯誤。

使用 Pragma Link 和 HPPEMIT 解決外部符號

如果您想確保某個單元在 C++ 中正確連結,而不強迫使用者手動將 .obj 檔案加入他們的專案中,您可以使用 Delphi 的 {$HPPEMIT} 指令。這會告訴 Delphi 編譯器將特定的 C++ #pragma link 指令注入產生的 .hpp 檔案中。

unit lxXlsSummary;

interface

{$IFDEF WINDOWS}
  // Inject a pragma link into the generated C++ header file
  // This forces the C++ linker to include the corresponding .obj file
  {$HPPEMIT '#pragma link "lxXlsSummary.obj"'}
{$ENDIF}

uses
  SysUtils, Classes;

type
  TXlsSummaryInfo = class(TObject)
  public
    Title: string;
    Author: string;
    CreateTime: TDateTime;
  end;

function XlsReadSummaryInformation(const FileName: string): TXlsSummaryInfo;

implementation

function XlsReadSummaryInformation(const FileName: string): TXlsSummaryInfo;
begin
  Result := TXlsSummaryInfo.Create;
  // Metadata extraction logic here
end;

end.

當 C++Builder 包含 lxXlsSummary.hpp 時,編譯器會遇到 #pragma link,並自動告訴連結器 (ILINK32/ILINK64) 解析來自 lxXlsSummary.obj 的符號。

元件維護的黃金法則

為了避免完全破壞 C++Builder 建置,您必須採用嚴格的註冊策略。每當將新的 Pascal 單元加入您的函式庫時,它必須同時在所有三種專案檔類型中明確註冊。

1. 更新 C++Builder 專案 (.cbproj / .bpk)

在文字編輯器中開啟 .cbproj 檔案,將新的單元加入編譯清單,並確保您提供了獨特的建置順序 (Build Order)。如果是使用帶有 .bpk 檔案的舊版 C++Builder,請確保加入對應的 <file containerid="PascalCompiler" ...></file> 標籤。

<DelphiCompile Include="lxXlsSummary.pas">
  <BuildOrder>101</BuildOrder>
</DelphiCompile>

2. 更新 Delphi 套件 (.dpk)

將單元加入明確的 contains 子句。這確保了 Delphi 編譯器不必依賴隱式連結,隱式連結通常被認為是不良的實踐。

package HotXLS;

{$R *.res}
{$ALIGN 8}
{$ASSERTIONS ON}
{$BOOLEVAL OFF}

requires
  rtl,
  vcl;

contains
  lxRead in 'lxRead.pas',
  lxXlsSummary in 'lxXlsSummary.pas';

end.

持續整合驗證

對抗這個陷阱的終極防禦手段是 CI/CD 驗證。在發布雙語言元件之前,絕對不要僅依賴成功的 Delphi 建置。您的建置指令碼必須在 C++Builder 專案上叫用 MSBuild 或 bcc32c 命令列工具 (例如 build-Win32-Lib-CB.cmd),並對 C++ 試用版和完整範例執行完整的連結。只有當 C++ 連結器成功時,您才能確定所有 Delphi 單元都已正確註冊,並將其符號公開給 C++ 執行階段。

備註:HotXLS VCL 元件 在 Delphi 和 C++Builder 版本之間嚴格維護跨平台編譯器相容性。