مقاله فنی

Free Pascal Win32: name decoration سمبل‌های C در HotPDF

Free Pascal روی Win32 به هر ایمپورت cdecl; external خودکار یک آندرلاین ابتدایی اضافه می‌کند، در حالی که public name رشته‌ای را که نوشته‌اید حرف به حرف اکسپورت می‌کند. HotPDF باید هر دو قرارداد را در همان درخت سورس راضی نگه دارد، چون بیلد Delphi از قبل اعلان‌های ایمپورتی را عرضه می‌کند که آندرلاین را با دست نوشته‌اند. غلط‌گرفتن این عدم‌تقارن link errorهایی تولید می‌کند که نام سمبلی را می‌گویند که هیچ‌کس ننوشته

گسترش یک کتابخانه Delphi به Free Pascal معمولاً یک مسئله portability توصیف می‌شود، و روی Win64 هم اغلب همین است. Win32 فرق دارد. ABI سی‌ودوبیتی ویندوز x86 سی سال قرارداد انباشته به همراه دارد درباره اینکه سمبل‌های C چطور هجی می‌شوند، چه کسی stack را پاک می‌کند، و کدام helperهای خصوصی کامپایلر یک translation unit حق دارد فرضشان کند، و هرکدام از این‌ها جایی است که دو کامپایلر پاسکالی که روی زبان توافق دارند می‌توانند روی فایل آبجکت اختلاف داشته باشند

چرا همان سمبل روی Win64 resolve می‌شود و روی Win32 fail می‌شود؟

چون پیشوند آندرلاین یک قرارداد ۳۲بیتی است که Free Pascal روی ایمپورت‌ها اعمال می‌کند نه روی اکسپورت‌ها. اعلان function deflate(...): Integer; cdecl; external; کنید و FPC روی Win32 دنبال _deflate در فایل آبجکت می‌گردد و روی Win64 دنبال deflate. این رفتار درست است و با آنچه یک کامپایلر C تولید می‌کند جور است. تله آن سمت پل است: روتینی که public name 'deflate' علامت خورده روی هر دو تارگت دقیقاً deflate را اکسپورت می‌کند، بدون هیچ پیشوندی

حالا جزئیات تاریخی را اضافه کنید که ماجرا را ملموس می‌کند. بیلد Delphi از قبل بعضی از این نقاط ورود را با آندرلاینِ نوشته‌شده در دل نام اعلان می‌کند، چون فایل‌های آبجکت خودش همین را دارند. همان اعلان را به FPC روی Win32 بدهید و کامپایلر وظیفه‌شناسانه دوباره پیشوند می‌گذارد، پس لینکر دنبال __deflate می‌گردد، سمبلی که هیچ‌کس اکسپورت نمی‌کند. fix شهودی، یعنی همه‌جا یک آندرلاین اضافه کردن، ایمپورت‌هایی را که از قبل درست هجی شده بودند می‌شکند

چیزی که کار می‌کند یک جفت ثابت پیشوند است نه یکی. HPDFFPCZLib و HPDFFPCCodecStubs برای ایمپورت‌های C ساده یک پیشوند و برای ایمپورت‌هایی که از قبل پیشوند سمت Delphi دارند پیشوند دیگری به کار می‌برند، و روی Win64 هر دو ثابت خالی‌اند تا نام‌های لینک موجود دست‌نخورده بمانند. دو ثابت به‌جای یکی تمامِ fix است، و فقط وقتی بدیهی می‌شود که قاعده ایمپورت را از قاعده اکسپورت جدا کرده باشید

اعلان‌های سمبل C یکسان که Free Pascal و Delphi روی Win64 و Win32 resolve می‌کنند: ایمپورت‌های cdecl فقط روی تارگت ۳۲بیتی آندرلاین می‌گیرند، اعلان Delphi که از قبل با آندرلاین هجی شده به __deflate تبدیل می‌شود و لینک نمی‌شود، در حالی که اکسپورت‌های public name روی هر دو معماری literal می‌مانند
یک ثابت پیشوند نمی‌تواند به هر دو قاعده خدمت کند: ایمپورت‌های cdecl ساده و ایمپورت‌هایی که از قبل آندرلاین دلفی دارند زیر FPC روی Win32 متفاوت decorate می‌شوند، پس HotPDF دو تا نگه می‌دارد و روی Win64 هر دو را خالی می‌گذارد
// دو پیشوند، نه یکی: ایمپورت‌های C ساده و ایمپورت‌هایی که از قبل
// پیشوند دست‌نویس دلفی دارند زیر FPC/Win32 متفاوت decorate می‌شوند
const
{$IF DEFINED(FPC) and DEFINED(CPU32)}
  CPrefix     = '_';   // FPC خودش برای cdecl external اضافه‌اش می‌کند
  DelphiCName = '';    // در سورس از قبل با آندرلاین هجی شده
{$ELSE}
  CPrefix     = '';
  DelphiCName = '';
{$IFEND}

// سمت اکسپورت: 'public name' روی هر تارگتی literal است
procedure hpdf_codec_free(P: Pointer); cdecl;
  public name 'hpdf_codec_free';

WIN32 معماری را می‌گوید، نه ABI را

این همان اشتباه کامپایل شرطی با طولانی‌ترین دنباله دیباگ است، و ارزش دارد صریح گفته شود: WIN32 و WIN64 معماری تارگت را توصیف می‌کنند و هیچ نمی‌گویند درباره اینکه کدام helperهای runtime خصوصی کامپایلر وجود دارند. Free Pascal هر دو نماد را روی تارگت‌های ویندوزی متناظر تعریف می‌کند، دقیقاً مثل Delphi. پس یک guard به شکل {$IFDEF WIN32} دور کدی که یک helper runtime دلفی را صدا می‌زند زیر FPC کامپایل می‌شود و در زمان لینک می‌افتد

مجزاً سه خانواده کد در این تله می‌افتند. trampolineهای عددی ۶۴بیتی دلفی که از طریق helperهای System.@_ll به آن‌ها می‌رسیم، روتین‌های پشتیبانی اسمبلی Win32 در سبک MSVC، و slotهای ایمپورتی که همراهشان می‌آیند، همه برای خدمت به آبجکت‌های C از پیش کامپایل‌شده‌ای هستند که بیلد Delphi لینک می‌کند. Free Pascal آن آبجکت‌ها را لینک نمی‌کند، پس به هیچ‌کدام از آن ماشین‌ری نیاز ندارد، و هر ارجاعی به آن‌ها باید غیب شود. نکته ظریف این است که اعلان و پیاده‌سازی باید با هم حذف شوند. فقط یکی را حذف کنید و کامپایلر چیزی بی‌فایده درباره شناسه‌ای گزارش می‌کند که نمی‌تواند به هیچ‌چیزش تطبیق دهد

قاعده‌ای که از این بیرون می‌آید کوتاه است. روی کامپایلر guard بگذارید وقتی سؤال درباره ABI یا پشتیبانی runtime است، روی معماری guard بگذارید وقتی سؤال درباره پهنای اشاره‌گر یا تعداد رجیستر است، و هرگز اجازه ندهید یکی جای دیگری بایستد

guardکردن همزمان اعلان‌ها و پیاده‌سازی‌ها

یک بلاک شرطی در بخش interface به‌راحتی بی‌خبر قورت داده می‌شود، و پیام خطای حاصله به هر جایی اشاره می‌کند جز علت. یک اعلان متد به interface یک کلاس اضافه کنید و جای طبیعی‌اش کنار متدهای مرتبط است، که تا لحظه‌ای که آن همسایه‌ها اتفاقاً درون یک بلاک {$IFDEF} موجود بنشینند کاملاً مشکلی ندارد. directiveهای شرطی تورفتگی نمی‌گیرند، پس بلاکی که چهل خط بالاتر باز شده حین خواندن اعلان‌های اطراف عملاً نامرئی است

چیزی که بعد می‌شود یک کامپایل است که روی یک toolchain موفق می‌شود و روی دیگری آبشاری تولید می‌کند. اگر guard اطراف یک چک نسخه Delphi باشد که Free Pascal برآورده‌اش نمی‌کند، اعلان برای FPC غیب می‌شود در حالی که پیاده‌سازی بدون شرط باقی می‌ماند، و کامپایلر لیست بلندی از گلایه درباره شناسه‌های متدی که انتظار داشت و پیدا نکرد گزارش می‌کند. هیچ‌کدام از پیام‌ها به بلاک شرطی علت‌ساز اشاره نمی‌کند

دو عادت تمام این رده شکست را پیشگیری می‌کند. پیش از درج در بخش interface، به دنبال نزدیک‌ترین شرطِ باز به بالا نگاه کنید نه اینکه به گروه‌بندی بصری اعتماد کنید. و یک تست‌سوییت سبز Delphi را فقط مدرکی درباره Delphi بدانید: بیلد کتابخانه Free Pascal یک گیت جداست، و تنها راه فهمیدن اینکه پاس می‌شود اجرای build-Win32-Lib-FPC.cmd و build-Win64-Lib-FPC.cmd به‌عنوان بخشی از همان تغییر است

در کد حساب ۳۲بیتی چه چیزی می‌شکند

یک محدودیت زبانی دقیقاً در کدی ظاهر می‌شود که کمترین تمایل به تغییر را دارد: Free Pascal سی‌ودوبیتی یک UInt64 را به‌عنوان متغیر کنترل حلقه for قبول نمی‌کند. در یونیت‌های منحنی بیضوی که X25519 و X448 را حمل می‌کنند، حلقه‌هایی که آرایه‌های limb را می‌گردند صرفاً به این دلیل با شمارنده‌های ۶۴بیتی نوشته شده بودند که بقیه فایل همگی ۶۴بیتی‌اند

fix باید جراحی باشد، چون در حساب میدانی پهنای یک متغیر بخشی از استدلال درستی است. شاخص‌های حلقه Integer می‌شوند، چون آرایه limb مشتی عنصر دارد و هیچ شاخصی به محدوده ۳۲بیتی نزدیک نمی‌شود. هر چیزی که در حساب مشارکت دارد — خود limbها، carry propagation و ماسک‌ها — UInt64 می‌ماند، چون باریک‌کردن هرکدام از این‌ها بی‌سروصدا نتیجه را به پیمانه اول میدان تغییر می‌دهد

// FPC سی‌ودوبیتی متغیر حلقه UInt64 را رد می‌کند. فقط شاخص را باریک کنید؛
// limbها، ماسک‌ها و carryها پهنایشان بماند وگرنه حساب میدان عوض می‌شود
var
  I: Integer;                 // قبلاً UInt64 بود
  Carry, Mask: UInt64;
begin
  Carry := 0;
  for I := 0 to High(Limbs) do
  begin
    Limbs[I] := Limbs[I] + Carry;
    Carry := Limbs[I] shr 51;
    Limbs[I] := Limbs[I] and Mask;
  end;
end;

راستی‌آزمایی برای چنین تغییری نمی‌تواند یک تست رفت‌وبرگشتی باشد. رمزگذاری و رمزگشایی با همان پیاده‌سازی خراب با خودش کامل موافقت می‌کند، به همین دلیل است که بردارهای known-answer اینجا قابل مذاکره نیستند: بردارهای تست منتشرشده X25519 و X448 را اجرا کنید و بایت‌های خروجی دقیق را مقایسه کنید. تنها چکی که پیاده‌سازی درست را از پیاده‌سازی غلطِ خودسازگار جدا می‌کند همین است، و به primitives متقارنی که در مرزهای کدک deflate و AES در Free Pascal بحث شد به همان اندازه هم اعمال می‌شود

دو نقطه شکست یک بیلد Win32 فری پاسکال در HotPDF: یک guard به شکل {$IFDEF WIN32} دور helperهای runtime دلفی که کامپایل می‌شود ولی در لینک می‌افتد مگر اعلان و پیاده‌سازی با هم حذف شوند، و متغیر حلقه UInt64 در گردش limbهای X25519 و X448 که به Integer باریک می‌شود در حالی که limbها، carryها و ماسک‌ها پهنایشان حفظ می‌شود
وقتی سؤال ABI یا پشتیبانی runtime است روی کامپایلر guard بگذارید و وقتی پهنای اشاره‌گر است روی معماری، بعد تغییرات حسابی را با بردارهای known-answer منتشرشده اثبات کنید نه با تست‌های رفت‌وبرگشتی

یک بیلد Win32 فری پاسکال چقدر می‌ارزد

سود عملی این است که یک برنامه Lazarus با تارگت ویندوز ۳۲بیتی همان موتور سندی را می‌گیرد که همتای دلفی‌اش، بدون یک قرارداد باینری جداگانه که نگهداری شود. این بیشتر برای استقرارهایی مهم است که کمتر درباره‌شان حرف می‌زنیم: کنترل‌کننده‌های صنعتی، ترمینال‌های POS و نرم‌افزارهای خط کسب‌وکار عمر بلند که در آن‌ها runtime سی‌ودوبیتی یک انتخاب لگسی نیست بلکه یک محدودیت سخت‌افزاری است

ماجرای Win64 اول آمد و در پشتیبانی Free Pascal و Lazarus روی Win64 توصیف شده. Win32 اجرای دوباره آن نیست. Win64 یک calling convention دارد، name decoration ندارد و helper عددی خصوصی دلفی‌ای هم ندارد که دورش بزنید، پس تقریباً همه‌چیز در این مقاله مختص تارگت ۳۲بیتی است. یونیت‌های حسابی که تغییر متغیر حلقه لازم داشتند همان‌هایند که در حساب Montgomery روی منحنی‌های NIST توصیف شده‌اند، جایی که انضباط پهنا عمیق‌تر توضیح داده می‌شود

درس کلی این است که کار portability بین‌کامپایلری در درجه اول درباره قابلیت‌های زبان نیست. هر دو کامپایلر اینجا همان Object Pascal را قبول می‌کنند. آنچه فرق می‌کند فایل آبجکت است: سمبل‌ها چطور هجی می‌شوند، کدام روتین‌های helper فرض می‌شود runtime فراهم کند، و کدام آبجکت‌های از پیش کامپایل‌شده در لینک هستند. HotPDF بسته‌های Free Pascal و Lazarus را کنار بسته‌های Delphi و C++Builder در HotPDF Delphi PDF component عرضه می‌کند، پس همان درخت سورس به همه toolchainها خوراک می‌دهد به‌جای انشعاب به‌ازای هر کامپایلر