وقتی از یک کامپوننت 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 رخ میدهد
حل externals با Pragma Link و HPPEMIT
اگر میخواهید مطمئن شوید یک unit در C++ به درستی لینک میشود بدون اینکه کاربر را مجبور کنید فایل .obj را دستی به پروژهاش اضافه کند، میتوانید از دستور {$HPPEMIT} در Delphi استفاده کنید. این دستور به کامپایلر Delphi میگوید یک #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 را include میکند، کامپایلر با #pragma link روبهرو میشود و به صورت خودکار به linkerهای ILINK32 و ILINK64 میگوید symbolها را از lxXlsSummary.obj resolve کنند
قاعده طلایی در نگهداری کامپوننت
برای اینکه 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 VCL Component به شکل سختگیرانه حفظ میشود