Технічна стаття

Крос-компіляція компонентів Delphi для C++Builder: як уникнути помилок Unresolved External

Під час супроводу VCL-компонента, написаного на Delphi, який використовується розробниками як на Delphi, так і на C++Builder, ви швидко розумієте, що ці дві системи збирання працюють із залежностями зовсім по-різному. Модуль, який ідеально компілюється в Delphi, може викликати катастрофічні помилки компонувальника в C++Builder. Така розбіжність є частою пасткою для авторів компонентів, особливо під час додавання нових внутрішніх модулів до наявної бібліотеки

Нижче наведено детальний розбір причин цієї проблеми на реальних прикладах із компонента HotXLS, а також поради щодо того, як зробити процес крос-компіляції надійним за допомогою явних включень, {$HPPEMIT} та директив pragma link

Сумісність компонентів визначається не лише спільним вихідним кодом: C++Builder потребує власних згенерованих двійкових артефактів і правильних шляхів пошуку для кожної цільової платформи

Пастка: неявна компіляція проти явного підключення

Припустимо, ви створюєте новий модуль Delphi lxXlsSummary.pas для обробки метаданих документа і додаєте його в uses вашого головного модуля парсингу lxRead.pas. Ви натискаєте кнопку компіляції в Delphi IDE, збирання проходить успішно, і ви випускаєте оновлення

Наступного дня ваші користувачі C++Builder повідомляють про помилку на етапі компонування: 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, відповідних символів бракує, що призводить до помилки Unresolved external

Вирішення проблеми Unresolved external за допомогою 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 у текстовому редакторі та додайте новий модуль до списку компіляції, вказавши унікальний порядок збирання. Якщо ви використовуєте старі версії 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.

Валідація за допомогою Continuous Integration

Найкращим захистом від цієї пастки є валідація в CI/CD. Ніколи не покладайтеся виключно на успішне збирання в Delphi перед випуском двомовного компонента. Ваші скрипти збирання повинні викликати MSBuild або інструменти командного рядка bcc32c для проєктів C++Builder (наприклад, build-Win32-Lib-CB.cmd) і виконувати повне компонування ознайомлювальних та повних демоверсій C++. Тільки у разі успішного завершення роботи компонувальника C++ ви можете бути впевнені, що всі модулі Delphi зареєстровані правильно і відкривають свої символи для середовища виконання C++

Примітка: Кросплатформна сумісність компіляторів суворо підтримується в редакціях для Delphi та C++Builder компонента HotXLS VCL