Technical Article

针对 C++Builder 交叉编译 Delphi 组件:避免未解析的外部符号

在维护用 Delphi 编写但由 Delphi 和 C++Builder 用户共同使用的 VCL 组件时,您很快就会发现这两种构建系统处理依赖关系的方式截然不同。一个在 Delphi 中完美编译的单元,可能会在 C++Builder 中导致灾难性的链接器错误。这种差异是组件作者经常遇到的陷阱,尤其是在向现有库添加新的内部单元时。

下面详细分析了发生这种情况的原因(使用来自 HotXLS 组件的真实示例),以及如何使用显式包含(explicit includes)、{$HPPEMIT} 和 pragma 链接来巩固您的交叉编译过程。

陷阱:隐式编译与显式包含

假设您创建了一个新的 Delphi 单元 lxXlsSummary.pas 来处理文档元数据,并且在您的主解析单元 lxRead.pas 中使用了它(uses)。您在 Delphi IDE 中点击编译,构建成功(变绿),然后您发布了更新。

第二天,您的 C++Builder 用户报告了链接阶段的一个错误:Unresolved external 'XlsReadSummaryInformation' referenced from lxRead.obj

Delphi 的方式(dcc32)

当 Delphi 编译器处理包(.dpk)时,它会查看 contains 子句中显式列出的单元。如果其中一个单元使用了(uses)不在 contains 列表中的外部单元(如 lxXlsSummary.pas),Delphi 编译器会执行隐式静态链接。它只需在搜索路径中找到 .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" designclass="" filename="lxXlsSummary.pas" formname="" localcommand="" unitname="lxXlsSummary"></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 Component 的 Delphi 和 C++Builder 版本严格保持跨平台编译器兼容性。