مقاله فنی

کامپایل متقابل کامپوننت‌های Delphi برای C++Builder: جلوگیری از Unresolved Externals

وقتی از یک کامپوننت VCL که با Delphi نوشته شده اما کاربران Delphi و C++Builder هر دو آن را مصرف می‌کنند نگه‌داری می‌کنید، خیلی زود متوجه می‌شوید این دو سیستم build وابستگی‌ها را کاملاً متفاوت مدیریت می‌کنند. واحدی که در Delphi بی‌نقص کامپایل می‌شود، ممکن است در C++Builder یک خطای linker فاجعه‌بار ایجاد کند. این تفاوت از دام‌های پرتکرار برای نویسندگان کامپوننت است، به‌ویژه وقتی unit داخلی جدیدی به یک کتابخانه موجود اضافه می‌شود

در اینجا با استفاده از نمونه‌ای واقعی از HotXLS دقیقاً توضیح می‌دهیم چرا این اتفاق می‌افتد و چگونه می‌توان با explicit include، {$HPPEMIT} و pragma link فرایند کامپایل متقابل را مقاوم کرد

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

فرض کنید یک unit جدید در Delphi به نام lxXlsSummary.pas برای رسیدگی به metadata سند می‌سازید و آن را در unit اصلی parsing خود یعنی lxRead.pas در بخش uses به کار می‌گیرید. در Delphi IDE روی compile می‌زنید، build سبز می‌شود و نسخه را منتشر می‌کنید

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

روش Delphi در dcc32

وقتی کامپایلر Delphi یک package با پسوند .dpk را پردازش می‌کند، به unitهایی نگاه می‌کند که به شکل صریح در clause contains آمده‌اند. اگر یکی از این unitها از طریق uses به unit بیرونی دیگری مثل lxXlsSummary.pas وابسته باشد که در contains ثبت نشده، کامپایلر Delphi یک implicit static linking انجام می‌دهد. فایل .pas را در search path پیدا می‌کند، آن را به .dcu کامپایل می‌کند، و داخل .bpl نهایی می‌پزد. build موفق می‌شود و این حذف‌شدگی را کاملاً پنهان می‌کند

روش C++Builder در MSBuild و .cbproj

سیستم build در C++Builder بسیار سخت‌گیرتر است. فقط برای unitهای Delphi که به شکل صریح در گروه <DelphiCompile> فایل .cbproj آمده‌اند فایل object با پسوند .obj و header با پسوند .hpp می‌سازد. چون lxXlsSummary.pas هرگز در فایل پروژه ثبت صریح نشده، فایلی به نام lxXlsSummary.obj ساخته نمی‌شود. وقتی linker می‌خواهد فراخوانی‌هایی را که از lxRead.obj آمده resolve کند، symbolها را پیدا نمی‌کند و در نتیجه خطای unresolved external رخ می‌دهد

مقایسه خط لوله build برای کامپوننت HotXLS در Delphi: dcc32 در Delphi یک unit فهرست‌نشده را ضمنی داخل package کامپایل و build سبز می‌دهد، در حالی که MSBuild در C++Builder هرگز فایل object آن را تولید نمی‌کند و ILINK32 با unresolved external متوقف می‌شود
همان یونیت فهرست‌نشده دو مسیر واگرا می‌رود: dcc32 حذف را از طریق لینک استاتیک ضمنی می‌پوشاند، در حالی که لینکر سخت‌گیرتر C++Builder آن را آشکار می‌کند

حل externals با Pragma Link و HPPEMIT

اگر می‌خواهید مطمئن شوید یک unit در C++ به درستی لینک می‌شود بدون این‌که کاربر را مجبور کنید فایل .obj را دستی به پروژه‌اش اضافه کند، می‌توانید از دستور {$HPPEMIT} در Delphi استفاده کنید. این دستور به کامپایلر Delphi می‌گوید یک #pragma link مشخص را به داخل فایل .hpp تولیدشده تزریق کند

unit lxXlsSummary;

interface

{$IFDEF WINDOWS}
  // یک pragma link را داخل فایل هدر C++ تولیدشده تزریق کن
  // این کار linker سی‌پلاس‌پلاس را وادار می‌کند فایل .obj متناظر را شامل کند
  {$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 اینجا
end;

end.

وقتی C++Builder فایل lxXlsSummary.hpp را include می‌کند، کامپایلر با #pragma link روبه‌رو می‌شود و به صورت خودکار به linkerهای ILINK32 و ILINK64 می‌گوید symbolها را از lxXlsSummary.obj resolve کنند

جریان چهارستونی سازوکار لینک pragma با نام HPPEMIT در Delphi: یک directive اعلام‌شده باعث می‌شود dcc32 ترتیب لینکر را به lxXlsSummary.hpp تزریق کند، C++Builder هنگام include آن را مصرف می‌کند و ILINK32 مقدار XlsReadSummaryInformation را خودکار حل می‌کند
ترتیب لینکر یک بار در منبع Pascal نوشته می‌شود، درون هدر تولیدشده سفر می‌کند، و به هر بیلد C++ آینده بدون ویرایش دستی پروژه می‌رسد

قاعده طلایی در نگه‌داری کامپوننت

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

1. به‌روزرسانی پروژه C++Builder یعنی .cbproj یا .bpk

فایل .cbproj را در ویرایشگر متن باز کنید و unit جدید را به فهرست compile اضافه کنید و برای آن یک BuildOrder یکتا تعیین کنید. اگر از نسخه‌های قدیمی‌تر C++Builder با فایل .bpk استفاده می‌کنید، مطمئن شوید برچسب <file containerid="PascalCompiler" designclass="" filename="lxXlsSummary.pas" formname="" localcommand="" unitname="lxXlsSummary"></file> هم افزوده شده است

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

2. به‌روزرسانی package در Delphi یعنی .dpk

unit را به clause صریح contains اضافه کنید. این کار تضمین می‌کند کامپایلر Delphi مجبور نباشد به implicit linking تکیه کند، روشی که در هر صورت practice خوبی محسوب نمی‌شود

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 است. پیش از انتشار یک کامپوننت دو زبانه، هرگز فقط به موفقیت build در Delphi تکیه نکنید. اسکریپت‌های build شما باید روی پروژه‌های C++Builder هم MSBuild یا ابزار خط فرمان bcc32c را اجرا کنند، برای نمونه build-Win32-Lib-CB.cmd، و لینک کامل نسخه trial و نسخه full از demoهای C++ را هم بیازمایند. فقط وقتی linker در C++ موفق شود می‌توانید مطمئن باشید همه unitهای Delphi درست ثبت شده‌اند و symbolهای خود را به runtime در C++ آشکار می‌کنند

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