Когато поддържате VCL компонент, написан на Delphi, но използван както от потребители на Delphi, така и на C++Builder, бързо осъзнавате, че двете системи за изграждане третират зависимостите по много различен начин. Единица (unit), която се компилира перфектно в Delphi, може да причини катастрофални грешки на линкера в C++Builder. Това несъответствие е често срещан капан за авторите на компоненти, особено когато добавят нови вътрешни единици към съществуваща библиотека
Ето подробна разбивка на това защо се случва това, използвайки реални примери от компонента HotXLS, и как да защитите процеса на кръстосано компилиране, използвайки изрични включвания, {$HPPEMIT} и pragma свързване
Капанът: Неявно компилиране срещу изрично включване
Да предположим, че създавате нова Delphi единица, lxXlsSummary.pas, за обработка на метаданните на документа, и я добавяте в uses на основната си единица за парсиране, lxRead.pas. Натискате компилиране в Delphi IDE, изграждането е успешно и вие пускате актуализацията
На следващия ден вашите C++Builder потребители съобщават за грешка по време на фазата на свързване (linking): Unresolved external 'XlsReadSummaryInformation' referenced from lxRead.obj
Начинът на Delphi (dcc32)
Когато компилаторът на Delphi обработва пакет (.dpk), той преглежда единиците, изрично изброени в клаузата contains. Ако една от тези единици използва външна единица чрез uses (като lxXlsSummary.pas), която не е в списъка contains, компилаторът на Delphi извършва неявно статично свързване. Той просто намира .pas файла в пътя за търсене, компилира го в .dcu и го вгражда в резултатния .bpl. Изграждането е успешно, напълно маскирайки пропуска
Начинът на C++Builder (MSBuild / .cbproj)
Системата за изграждане на C++Builder е много по-строга. Тя генерира C++ обектни файлове (.obj) и заглавни файлове (.hpp) само за Delphi единиците, изрично изброени в групата елементи <DelphiCompile> на .cbproj файла. Тъй като lxXlsSummary.pas никога не е бил изрично регистриран във файла на проекта, не се създава lxXlsSummary.obj. Когато линкерът се опита да разреши извикванията, направени от lxRead.obj, символите липсват, което води до грешка за неразрешена външна зависимост
Разрешаване на външни зависимости с Pragma Link и HPPEMIT
Ако искате да сте сигурни, че дадена единица е правилно свързана в C++ без да принуждавате потребителя ръчно да добавя .obj файла към своя проект, можете да използвате директивата {$HPPEMIT} на Delphi. Това казва на компилатора на 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 файла в текстов редактор и добавете новата единица към списъка за компилиране, като се уверите, че предоставяте уникален ред на изграждане. Ако използвате по-стари версии на C++Builder с .bpk файлове, уверете се, че е добавен тагът <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 преди да пуснете двуезичен компонент. Вашите скриптове за изграждане трябва да извикват MSBuild или инструментите от командния ред bcc32c върху проектите на C++Builder (напр. build-Win32-Lib-CB.cmd) и да стартират пълно свързване (link) на C++ пробните и пълните демо версии. Само когато линкерът на C++ успее, можете да бъдете сигурни, че всички Delphi единици са правилно регистрирани и излагат своите символи на C++ средата за изпълнение
Забележка: Кръстосаната съвместимост на компилаторите се поддържа стриктно между изданията за Delphi и C++Builder на компонента HotXLS VCL