在维护用 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 版本会严格保持跨编译器兼容性