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