Technical Article

کامپایل متقاطع کامپوننت‌های دلفی برای C++Builder: جلوگیری از خطای Unresolved Externals

هنگام نگهداری یک کامپوننت VCL که در دلفی نوشته شده اما توسط کاربران دلفی و C++Builder استفاده می‌شود، به سرعت متوجه خواهید شد که سیستم‌های بیلد این دو محیط با وابستگی‌ها بسیار متفاوت رفتار می‌کنند. یونیتی که به صورت کامل در دلفی کامپایل می‌شود، ممکن است باعث خطاهای مخرب لینکر در C++Builder شود. این اختلاف یک تله رایج برای نویسندگان کامپوننت است، به‌ویژه هنگام افزودن یونیت‌های داخلی جدید به یک کتابخانه موجود.

در اینجا تفکیک دقیقی از دلیل وقوع این اتفاق (با استفاده از مثال‌های دنیای واقعی از کامپوننت HotXLS) و چگونگی ایمن‌سازی فرآیند کامپایل متقاطع با استفاده از فراخوانی‌های صریح، {$HPPEMIT} و pragma link آورده شده است.

تله: کامپایل ضمنی در برابر گنجاندن صریح

فرض کنید یک یونیت دلفی جدید به نام lxXlsSummary.pas برای مدیریت متادیتای سند ایجاد می‌کنید، و آن را در یونیت اصلی تجزیه‌کننده خود یعنی lxRead.pas با دستور uses فراخوانی می‌کنید. دکمه کامپایل را در IDE دلفی می‌زنید، فرآیند بیلد با موفقیت (سبز) انجام می‌شود و شما به‌روزرسانی را منتشر می‌کنید.

روز بعد، کاربران C++Builder شما یک خطا در مرحله پیوند گزارش می‌دهند: Unresolved external 'XlsReadSummaryInformation' referenced from lxRead.obj.

روش دلفی (dcc32)

هنگامی که کامپایلر دلفی یک پکیج (dpk.) را پردازش می‌کند، به یونیت‌هایی که به طور صریح در بند contains فهرست شده‌اند نگاه می‌کند. اگر یکی از آن یونیت‌ها از یونیت خارجی دیگری (مانند lxXlsSummary.pas) استفاده کند که در لیست contains نیست، کامپایلر دلفی پیوند استاتیک ضمنی (implicit static linking) را انجام می‌دهد. این کامپایلر به سادگی فایل pas. را در مسیر جستجو پیدا می‌کند، آن را به dcu. کامپایل می‌کند و درون bpl. نهایی قرار می‌دهد. بیلد موفقیت‌آمیز است و این فراموشی را کاملاً پنهان می‌کند.

روش C++Builder (سیستم MSBuild / فایل cbproj.)

سیستم بیلد C++Builder بسیار سخت‌گیرتر است. این سیستم تنها برای یونیت‌های دلفی که به طور صریح در گروه آیتم <DelphiCompile> در فایل cbproj. فهرست شده‌اند، فایل‌های شیء C++ (obj.) و هدرها (hpp.) را تولید می‌کند. از آنجایی که lxXlsSummary.pas هرگز به طور صریح در فایل پروژه ثبت نشده بود، فایل lxXlsSummary.obj ایجاد نمی‌شود. هنگامی که لینکر تلاش می‌کند فراخوانی‌های انجام شده توسط lxRead.obj را حل و فصل کند، نمادها (symbols) مفقود هستند و منجر به خطای Unresolved external می‌شود.

حل مشکل اکسترنال‌ها با Pragma Link و HPPEMIT

اگر می‌خواهید اطمینان حاصل کنید که یک یونیت بدون نیاز به اضافه کردن دستی فایل obj. به پروژه توسط کاربر، به درستی در C++ پیوند داده می‌شود، می‌توانید از دایرکتیو {$HPPEMIT} در دلفی استفاده کنید. این دایرکتیو به کامپایلر دلفی می‌گوید که یک دایرکتیو خاص #pragma link از زبان C++ را به فایل 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، باید یک خط‌مشی ثبت صریح را اتخاذ کنید. هر زمان که یک یونیت پاسکال جدید به کتابخانه شما اضافه می‌شود، باید هم‌زمان در هر سه نوع فایل پروژه به طور صریح ثبت شود.

۱. به‌روزرسانی پروژه C++Builder (فایل‌های cbproj. / bpk.)

فایل cbproj. را در یک ویرایشگر متنی باز کنید و یونیت جدید را به لیست کامپایل اضافه کنید، و مطمئن شوید که یک ترتیب ساخت (build order) منحصر‌به‌فرد ارائه می‌دهید. در صورت استفاده از نسخه‌های قدیمی‌تر C++Builder با فایل‌های bpk.، اطمینان حاصل کنید که تگ مربوطه یعنی <file containerid="PascalCompiler" ...> اضافه شده باشد.

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

۲. به‌روزرسانی پکیج دلفی (dpk.)

یونیت را به بند contains اضافه کنید. این کار تضمین می‌کند که کامپایلر دلفی مجبور نیست به پیوند ضمنی متکی باشد، که به هر حال یک روش نادرست در نظر گرفته می‌شود.

package HotXLS;

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

requires
  rtl,
  vcl;

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

end.

اعتبارسنجی یکپارچه‌سازی مداوم (CI)

نهایی‌ترین دفاع در برابر این تله، اعتبارسنجی از طریق CI/CD است. هرگز پیش از انتشار یک کامپوننت دو زبانه، تنها به موفقیت یک بیلد دلفی اتکا نکنید. اسکریپت‌های بیلد شما باید ابزارهای خط فرمان MSBuild یا bcc32c را روی پروژه‌های C++Builder فراخوانی کنند (مثلاً build-Win32-Lib-CB.cmd) و یک پیوند کامل از دموهای آزمایشی و کامل C++ را اجرا کنند. تنها زمانی که لینکر C++ موفق شود، می‌توانید مطمئن باشید که تمام یونیت‌های دلفی به درستی ثبت شده‌اند و نمادهای خود را در اختیار زمان اجرای C++ قرار می‌دهند.

توجه: سازگاری متقاطع کامپایلر بین نسخه‌های دلفی و C++Builder در کامپوننت HotXLS VCL به شدت حفظ می‌شود.