技术文章

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

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

下面用 HotXLS 组件中的真实案例详细拆解问题为什么会出现,以及如何借助显式包含、{$HPPEMIT} 和 pragma link 让交叉编译流程更加稳妥

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

假设您创建了一个新的 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 的组件之前,绝不能只因为 Delphi 构建成功就放心出货。您的构建脚本必须对 C++Builder 项目调用 MSBuild 或 bcc32c 命令行工具,例如 build-Win32-Lib-CB.cmd,并完成 C++ 试用版与完整演示程序的整套链接。只有当 C++ 链接器顺利通过时,您才能确认所有 Delphi 单元都已正确注册,并且它们的符号也已经暴露给 C++ 运行时

说明:HotXLS VCL Component 的 Delphi 和 C++Builder 版本会严格保持跨编译器兼容性