Artykuł techniczny

Kompilacja skrośna komponentów Delphi dla C++Builder: unikanie nierozwiązanych symboli zewnętrznych

Utrzymując komponent VCL napisany w Delphi, ale wykorzystywany zarówno przez użytkowników Delphi, jak i C++Builder, szybko dostrzegasz, że oba systemy budowania traktują zależności bardzo różnie. Moduł, który kompiluje się bez zarzutu w Delphi, może wywołać katastrofalne błędy linkera w C++Builder. Ta rozbieżność jest częstą pułapką dla autorów komponentów, szczególnie przy dodawaniu nowych modułów wewnętrznych do istniejącej biblioteki

Oto szczegółowa analiza tego, dlaczego tak się dzieje, na rzeczywistych przykładach z komponentu HotXLS, oraz jak uczynić proces kompilacji skrośnej kuloodpornym za pomocą jawnych włączeń, dyrektywy {$HPPEMIT} i linkowania pragmą

Pułapka: kompilacja niejawna kontra jawne włączenie

Załóżmy, że tworzysz nowy moduł Delphi, lxXlsSummary.pas, do obsługi metadanych dokumentu, i korzystasz z niego przez uses w swoim głównym module parsującym, lxRead.pas. Naciskasz kompilację w IDE Delphi, budowanie kończy się na zielono, i wysyłasz aktualizację

Następnego dnia twoi użytkownicy C++Builder zgłaszają błąd w fazie linkowania: Unresolved external 'XlsReadSummaryInformation' referenced from lxRead.obj

Sposób Delphi (dcc32)

Gdy kompilator Delphi przetwarza pakiet (.dpk), patrzy na moduły jawnie wymienione w klauzuli contains. Jeśli jeden z tych modułów korzysta przez uses z zewnętrznego modułu (jak lxXlsSummary.pas), którego nie ma na liście contains, kompilator Delphi wykonuje niejawne linkowanie statyczne. Po prostu znajduje plik .pas w ścieżce wyszukiwania, kompiluje go do .dcu i wtapia go w wynikowy .bpl. Budowanie kończy się sukcesem, całkowicie maskując pominięcie

Sposób C++Builder (MSBuild / .cbproj)

System budowania C++Builder jest znacznie bardziej rygorystyczny. Generuje pliki obiektowe C++ (.obj) i nagłówki (.hpp) tylko dla modułów Delphi jawnie wymienionych w grupie elementów <DelphiCompile> pliku .cbproj. Ponieważ lxXlsSummary.pas nigdy nie został jawnie zarejestrowany w pliku projektu, żaden lxXlsSummary.obj nie jest tworzony. Gdy linker próbuje rozwiązać wywołania wykonywane przez lxRead.obj, symbole są brakujące, co prowadzi do błędu nierozwiązanego symbolu zewnętrznego

Rozwiązywanie symboli zewnętrznych za pomocą pragma link i HPPEMIT

Jeśli chcesz zapewnić, że moduł jest poprawnie zlinkowany w C++ bez zmuszania użytkownika do ręcznego dodawania pliku .obj do jego projektu, możesz użyć dyrektywy Delphi {$HPPEMIT}. Mówi ona kompilatorowi Delphi, aby wstrzyknął konkretną dyrektywę C++ #pragma link do wygenerowanego pliku .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.

Gdy C++Builder włącza lxXlsSummary.hpp, kompilator napotyka #pragma link i automatycznie mówi linkerowi (ILINK32/ILINK64), aby rozwiązał symbole z lxXlsSummary.obj

Złota zasada utrzymania komponentów

Aby całkowicie uniknąć psucia kompilacji C++Builder, musisz przyjąć rygorystyczną politykę rejestracji. Ilekroć nowy moduł Pascal jest dodawany do twojej biblioteki, musi zostać jawnie zarejestrowany jednocześnie we wszystkich trzech typach plików projektu

1. Zaktualizuj projekt C++Builder (.cbproj / .bpk)

Otwórz plik .cbproj w edytorze tekstu i dodaj nowy moduł do listy kompilacji, upewniając się, że podajesz unikalną kolejność budowania. Jeśli używasz starszych wersji C++Builder z plikami .bpk, upewnij się, że dodany jest znacznik <file containerid="PascalCompiler" designclass="" filename="lxXlsSummary.pas" formname="" localcommand="" unitname="lxXlsSummary"></file>

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

2. Zaktualizuj pakiet Delphi (.dpk)

Dodaj moduł do jawnej klauzuli contains. Zapewnia to, że kompilator Delphi nie musi polegać na niejawnym linkowaniu, które i tak jest ogólnie uważane za złą praktykę

package HotXLS;

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

requires
  rtl,
  vcl;

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

end.

Walidacja ciągłej integracji

Ostateczną obroną przed tą pułapką jest walidacja CI/CD. Nigdy nie polegaj wyłącznie na udanym budowaniu w Delphi przed wysłaniem komponentu dwujęzycznego. Twoje skrypty budowania muszą wywoływać MSBuild lub narzędzia wiersza poleceń bcc32c na projektach C++Builder (np. build-Win32-Lib-CB.cmd) i przeprowadzać pełne linkowanie wersji próbnej C++ oraz pełnych wersji demonstracyjnych. Dopiero gdy linker C++ zakończy się sukcesem, możesz mieć pewność, że wszystkie moduły Delphi są poprawnie zarejestrowane i udostępniają swoje symbole środowisku uruchomieniowemu C++

Uwaga: zgodność kompilatorów międzyplatformowych jest ściśle utrzymywana w edycjach Delphi i C++Builder komponentu HotXLS VCL Component