Technický článek

Křížová kompilace komponent Delphi pro C++Builder: Jak se vyhnout nevyřešeným externím odkazům

Při údržbě VCL komponenty napsané v Delphi, kterou však využívají uživatelé Delphi i C++Builderu, rychle zjistíte, že tyto dva build systémy přistupují k závislostem velmi odlišně. Jednotka (unit), která se v Delphi zkompiluje bezchybně, může v C++Builderu způsobit katastrofální chyby linkeru. Tento rozdíl je častou pastí pro autory komponent, zejména při přidávání nových interních jednotek do existující knihovny

Zde je podrobný rozbor toho, proč k tomu dochází (s využitím reálných příkladů z komponenty HotXLS), a jak zajistit neprůstřelnost procesu křížové kompilace pomocí explicitních includů, {$HPPEMIT} a pragma linkování

Past: Implicitní kompilace vs. Explicitní zahrnutí

Předpokládejme, že vytvoříte novou jednotku Delphi, lxXlsSummary.pas, pro zpracování metadat dokumentu a použijete ji (pomocí uses) ve vaší hlavní parsovací jednotce lxRead.pas. V Delphi IDE kliknete na kompilaci, build zezelená a vy vydáte aktualizaci

Následující den vaši uživatelé C++Builderu nahlásí chybu během fáze linkování: Unresolved external 'XlsReadSummaryInformation' referenced from lxRead.obj

Způsob Delphi (dcc32)

Když kompilátor Delphi zpracovává balíček (.dpk), podívá se na jednotky explicitně uvedené v klauzuli contains. Pokud jedna z těchto jednotek používá (uses) externí jednotku (jako lxXlsSummary.pas), která není v seznamu contains, kompilátor Delphi provede implicitní statické linkování. Jednoduše najde soubor .pas ve vyhledávací cestě, zkompiluje jej do .dcu a zapeče jej do výsledného .bpl. Sestavení proběhne úspěšně a toto opomenutí je zcela zamaskováno

Způsob C++Builderu (MSBuild / .cbproj)

Build systém C++Builderu je mnohem striktnější. Generuje pouze objektové soubory C++ (.obj) a hlavičkové soubory (.hpp) pro jednotky Delphi explicitně uvedené ve skupině položek <DelphiCompile> v souboru .cbproj. Protože soubor lxXlsSummary.pas nebyl nikdy explicitně registrován v souboru projektu, žádný lxXlsSummary.obj není vytvořen. Když se linker pokusí vyřešit volání provedená z lxRead.obj, symboly chybí, což vede k chybě nevyřešeného externího odkazu (unresolved external)

Řešení externích odkazů pomocí Pragma Link a HPPEMIT

Pokud chcete zajistit, aby byla jednotka v C++ správně slinkována, aniž byste nutili uživatele ručně přidávat soubor .obj do jejich projektu, můžete použít direktivu Delphi {$HPPEMIT}. Ta říká kompilátoru Delphi, aby do vygenerovaného hlavičkového souboru .hpp pro C++ vložil specifickou direktivu #pragma link

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.

Když C++Builder vloží lxXlsSummary.hpp, kompilátor narazí na #pragma link a automaticky řekne linkeru (ILINK32/ILINK64), aby vyřešil symboly z lxXlsSummary.obj

Zlaté pravidlo pro údržbu komponent

Abyste se vyhnuli úplnému rozbití sestavení v C++Builderu, musíte přijmout přísná pravidla pro registraci. Kdykoli je do vaší knihovny přidána nová jednotka Pascalu, musí být explicitně a současně registrována ve všech třech typech projektových souborů

1. Aktualizace projektu C++Builder (.cbproj / .bpk)

Otevřete soubor .cbproj v textovém editoru a přidejte novou jednotku do seznamu pro kompilaci, přičemž zajistěte poskytnutí unikátního pořadí sestavení (build order). Pokud používáte starší verze C++Builderu se soubory .bpk, ujistěte se, že je přidána značka <file containerid="PascalCompiler" designclass="" filename="lxXlsSummary.pas" formname="" localcommand="" unitname="lxXlsSummary"></file>

<DelphiCompile Include="lxXlsSummary.pas">
  <BuildOrder>101</BuildOrder>
</DelphiCompile>

2. Aktualizace balíčku Delphi (.dpk)

Přidejte jednotku do explicitní klauzule contains. Tím se zajistí, že kompilátor Delphi nebude muset spoléhat na implicitní linkování, což je tak jako tak obecně považováno za špatnou praxi

package HotXLS;

{$R *.res}
{$ALIGN 8}
{$ASSERTIONS ON}
{$BOOLEVAL OFF}

requires
  rtl,
  vcl;

contains
  lxRead in 'lxRead.pas',
  lxXlsSummary in 'lxXlsSummary.pas';

end.

Ověřování pomocí Continuous Integration

Nejlepší obranou proti této pasti je validace pomocí CI/CD. Nikdy nespoléhejte pouze na úspěšné sestavení v Delphi před vydáním komponenty pro oba jazyky. Vaše skripty pro sestavení musí vyvolat MSBuild nebo nástroje příkazového řádku bcc32c na projektech C++Builderu (např. build-Win32-Lib-CB.cmd) a provést kompletní linkování zkušebních a plných C++ demoverzí. Pouze pokud kompilátor a linker C++ uspějí, můžete si být jisti, že jsou všechny jednotky Delphi správně registrovány a vystavují své symboly běhovému prostředí C++

Poznámka: Kompatibilita křížových platforem je striktně udržována napříč edicemi Delphi a C++Builderu u komponenty HotXLS VCL Component