مقاله فنی

HotXLS روی Free Pascal: یونیکد، اسلات‌های COM و zlib

HotXLS زیر Free Pascal و Lazarus روی ویندوز بیلد می‌شود، و پورت روی چهار تصمیم سوار بود که هیچ ربطی به سینتکس Object Pascal ندارند: نگه‌داشتن هسته در حالت DELPHIUNICODE، اعلان اینترفیس‌های OLE structured-storage به‌شکل اینترفیس‌های CORBA با شمارش مرجع مدیریت‌شده با دست، جایگزینی آبجکت‌فایل‌های AES وین32 با یک پیاده‌سازی پاسکالی، و درست‌کردن یک حلقه inflate که می‌توانست یک ZIP بریده‌شده را کامل قبول کند

هر کسی که یک کتابخانه بالغ دلفی را پورت کرده شکل این کار را می‌شناسد. کامپایلر در گذر اول تقریباً همه چیز را قبول می‌کند. آنچه بعد می‌آید یک دنباله بلند از تفاوت‌های رفتاری است که تمیز کامپایل می‌شوند و نتیجه غلط می‌دهند، و یک موتور صفحه‌گسترده به‌طور غیرمعمولی در معرضشان است چون در یک مسیر کد واحد به encoding متن، structured storage COM، فشرده‌سازی و رمزنگاری دست می‌زند

چرا هسته روی DELPHIUNICODE اصرار می‌کند نه DELPHI ساده؟

چون موتور فرمول به این وابسته است که String و Char معناشناسی UTF-16 حمل کنند، و گزینه ANSI کاراکترها را قبل از آنکه چیزی به فایل برسد گم می‌کند. وسوسه‌انگیز است که هسته را در حالت DELPHI مال FPC بیلد کنید، چون همان سوییچ سازگاری است که بیشتر پورت‌ها سراغش می‌روند، و کد کامپایل می‌شود. بعد workbookی با نام شیت‌های چینی یا برچسب‌های سیریلیک از مسیر محاسبه رد می‌شود و تا وقتی writer به کاراکترها می‌رسد آنها رفته‌اند، بدون هیچ خطایی در هیچ‌جا

این حالت در سراسر کتابخانه یکنواخت نیست، و این عمدی است نه بی‌نظمی. دیکدر بایتی PNG و overrideهای LCL واقعاً به امضاهای ANSI نیاز دارند، چون با بایت‌ها و با چیزی سر و کار دارند که widgetset به آنها می‌دهد. آن یونیت‌ها یک سوییچ جدای LX_FPC_ANSI را فعال می‌کنند. دو حالت در یک کتابخانه شبیه بوی بد کد است تا وقتی متوجه شوید گزینه دیگر دیکدری بایتی است که ورودی‌اش را متن فرض می‌کند

یک جزئیات همراه هم هست که بعداً آدم‌ها را می‌گیرد. DELPHIUNICODE در ران‌تایم FPC چیزی TFormatSettings.DecimalSeparator را WideChar نمی‌کند. ورودی‌ای که جداساز اعشاری یونیکد حمل می‌کند باید اول درون رشته یونیکد به یک جداساز ASCII نرمالایز شود، و هر ورودی‌ای که جداسازش با مورد انتظار نمی‌خواند باید رد شود نه اینکه بی‌سروصدا در کاراکتری که parser نشناخته بریده شود

program ExportReport;
{$MODE DELPHI}
uses
  Interfaces,          // باید اول بیاید: widgetset لایه LCL را مقداردهی اولیه می‌کند
  SysUtils, lxHandle;  // و لایه تبدیل UTF-8

var
  Book: TXLSWorkbook;
begin
  Book := TXLSWorkbook.Create(nil);
  try
    Book.LoadFromFile('input.xls');
    Book.Sheets[0].AsString[1, 1] := 'Quarterly summary';
    Book.SaveToFile('output.xls');
  finally
    Book.Free;
  end;
end.

یونیت Interfaces اختیاری نیست و باید اول بیاید. همان چیزی است که widgetset لایه LCL و لایه تبدیل UTF-8 را مقداردهی اولیه می‌کند، و HotXLS به هر دو وابسته است به محض اینکه فونت‌ها، مسیرهای فایل یا متن از مرز RTL و LCL رد شوند. یک برنامه کنسولی که آن را جا بیندازد کامپایل می‌شود و روی هر مسیر غیرASCII بدرفتاری می‌کند. همین دلیلش هم هست که یک کامپایل موفق اینجا این‌قدر کم اثبات می‌کند: پورت فقط وقتی عملاً کار کرد که سندهای واقعی با نام فونت‌های واقعی و مسیرهای واقعی یک رفت‌وبرگشت کامل انجام دادند

VMT کلاس همان vtable COM نیست

Free Pascal اجازه نمی‌دهد VMT کلاس را به‌عنوان vtable اینترفیس COM به ویندوز بدهید، حتی وقتی اعلان دقیقاً شبیه چیزی باشد که دلفی قبول می‌کند. چیدمان‌ها جوری فرق می‌کنند که فراخوانی به اسلات اشتباه می‌افتد، که خودش را به‌شکل کرشی در جایی بی‌ربط به محل فراخوانی نشان می‌دهد. structured storage اینجا مهم است چون فرمت باینری کلاسیک workbook یک فایل OLE compound است، و خواندن یا نوشتنش یعنی پیاده‌سازی ILockBytes که API ذخیره‌سازی ویندوز داخلش callback می‌زند

چیدمان کارا یک اینترفیس CORBA با اسلات‌های COM اعلان‌شده صریح و AddRef و Release مدیریت‌شده با دست است. یعنی گذشتن از شمارش مرجع خودکار برای این تایپ‌ها و پذیرفتن مسئولیت طول عمر، که برای مشتی اینترفیس که در یک یونیت زندگی می‌کنند معامله منصفانه‌ای است. تله خاص درون این کار QueryInterface است: باید اشاره‌گر اینترفیس برگرداند نه اشاره‌گر آبجکت. هر دو کامپایل می‌شوند. یکی از آنها آدرسی به ویندوز می‌دهد که اولین کلمه ماشینی‌اش vtable نیست

نمودار مقایسه VMT کلاس Free Pascal با vtable اینترفیس COM که HotXLS باید به API structured storage ویندوز ارائه کند: ترتیب اسلات‌های متفاوت برای اعلان پاسکالی یکسان، به‌علاوه تله QueryInterface که در آن برگرداندن اشاره‌گر آبجکت به‌جای اشاره‌گر اینترفیس یک فراخوانی ILockBytes را به اسلات کلاس می‌فرستد و دور از محل فراخوانی کرش می‌کند
Free Pascal حاضر نیست VMT کلاس را به‌عنوان vtable COM سرو کند، پس HotXLS اینترفیس‌های CORBA با اسلات‌های COM صریح و AddRef و Release مدیریت‌شده با دست اعلان می‌کند، و QueryInterface اشاره‌گر اینترفیسی را برمی‌گرداند که ویندوز می‌تواند dereference کند

اعلان‌های مخصوص FPC در lxOleInterfaces.inc زندگی می‌کنند، کنار lxAESBackend.inc و lxZlibBackend.inc در شاخه سورس FPC، پس انتخاب‌های وابسته به کامپایلر یک‌جا می‌نشینند به‌جای آنکه در موتور پخش باشند. خود فرمت و اینکه کتابخانه چطور در آن راه می‌رود در خواندن فایل‌های OLE2 compound در پاسکال توضیح داده شده

یک جزئیات تایپی دیگر هم به همین خانواده تعلق دارد. LargeInt باید در شاخه FPC به Int64 resolve شود، و طبقه‌بندی Comp بین دو زنجیره ابزار به‌اندازه‌ای فرق می‌کند که resolve شدن overload می‌تواند کاندیدای متفاوتی بردارد. رفتار آفست بزرگ را با یک file stream تست کنید نه HGLOBAL stream: stream حافظه جهانی ویندوز روی سیک‌های past 4 GiB خودش wrap می‌شود، پس تستی که آنجا پاس می‌شود درباره محاسبات خودتان هیچ چیز اثبات نمی‌کند

چه چیزی پشت یک پیاده‌سازی AES خودسازگار پنهان است

آبجکت‌فایل‌های AES وین32 که بیلد دلفی لینک می‌کند OMF هستند، و لینکر Free Pascal نمی‌تواند آنها را بخورد، پس شاخه FPC به‌جایش از یک پیاده‌سازی AES پاسکالی استفاده می‌کند. دلفی به لینک‌کردن همان آبجکت‌فایل‌های همیشگی ادامه می‌دهد، که باینری منتشرشده را برای مشتریان موجود دست‌نخورده نگه می‌دارد

الزام وریفیکیشن همان بخشی است که ارزش دارد به هر پروژه‌ای برده شود. رمزکردن داده و رمزگشایی دوباره‌اش با همان پیاده‌سازی هیچ چیز اثبات نمی‌کند: یک الگوریتم متقارن با key schedule غلط، ترتیب بلوک غلط یا chaining غلط کاملاً خودسازگار است و هر بار خروجی خودش را رفت‌وبرگشت می‌دهد. فقط وکتورهای known-answer می‌گیرندش، با چک‌کردن گسترش کلید، ترتیب بلوک و chaining یک CBC در برابر مقادیر منتشرشده. یک پیاده‌سازی غلط اما خودسازگار را شپیود کنید و علامتش اولین باری که مشتری فایل را در Excel باز می‌کند ظاهر می‌شود

فشرده‌سازی نقصی با شخصیت متفاوت داشت. یک بک‌اند inflate پاسکالی بعد از خوردن همه ورودی فشرده‌اش ممکن است هنوز خروجی معوق داشته باشد، پس caller باید فراخوانی را ادامه دهد تا stream پایانش را اعلام کند. فرض‌کردن ورودی ته‌کشیده به‌عنوان انتهای stream آخرین بلوک را می‌برد. بدتر، آرشیو آسیب‌دیده را به آرشیوی تبدیل می‌کند که بی‌سروصدا قبول می‌شود، که دقیقاً همان حالت شکستی است که سخت‌سازی در اعتبارسنجی رکورد ZIP end-of-central-directory برای جلوگیری از آن وجود دارد. قانون این است که هیچ پیشرفت به‌علاوه تمام‌نشده یک خطای برش است، هرگز EOF نه

دو تله سیستم بیلد که ساعت‌های واقعی می‌بلعند

مسیرهای جست‌وجوی LCL باید جلوی مسیرهای وایلدکارد پکیج FPC بیایند، وگرنه یونیت Menus فری ویژن یونیت هم‌نام LCL را می‌پوشاند و شما یک عدم‌تطابق checksum PPU می‌گیرید که درباره هیچ‌کدام چیزی نمی‌گوید. نصب Lazarus که بعد از نصب جابه‌جا شده باشد هم می‌تواند مسیرهای کهنه در fpc.cfg باقی بگذارد، پس نقاط ورود بیلد مسیرهای یونیت و باینری را صریح مشخص می‌کنند به‌جای اینکه هرچه محیط می‌دهد به ارث ببرند

تله دوم هیچ ربطی به پاسکال ندارد. یک فایل batch با .cmd نوشته‌شده با line endingهای LF کار می‌کند تا وقتی فایل از اندازه بافر خواندن مفسر بزرگ‌تر شود، که آن موقع call :label با ادعای اینکه لیبل batch وجود ندارد شکست می‌خورد، و شکست در هر برنامه‌ای که اتفاقاً آن‌طرف مرز نشسته ظاهر می‌شود. هر ابزاری که اسکریپت batch را بازنویسی می‌کند باید CRLF را برگرداند. و lazbuild --build-all شاخه خروجی یونیت پکیج را قبل از کامپایل خالی می‌کند، پس یک فایل گزینه‌ای که در آن شاخه پارک شده قبل از خوانده‌شدن حذف می‌شود: بیرون نگهش دارید، و یادتان باشد مسیر @ نسبت به شاخه پکیج resolve می‌شود چون lazbuild کامپایلر را از آنجا صدا می‌زند

نقشه دو لایه خطر پشت یک کامپایل تمیز HotXLS روی Free Pascal: حالت DELPHIUNICODE که String و Char را در UTF-16 نگه می‌دارد، راه‌فرار LX_FPC_ANSI برای دیکدر بایتی PNG و overrideهای LCL، و تله‌های بیلد از سایه Menus فری ویژن، مسیرهای کهنه fpc.cfg، فایل‌های batch فقط-LF و پاک‌سازی خروجی lazbuild
کامپایل اول کم اثبات می‌کند: نقشه حالت تصمیم می‌گیرد کدام کاراکترها تا writer زنده می‌مانند، و تله‌های سیستم بیلد به‌شکل عدم‌تطابق checksum، لیبل‌های گمشده خیالی و فایل‌های گزینه حذف‌شده قبل از خوانده‌شدن خودشان را نشان می‌دهند
// اکسپورت گرید Lazarus: TGridToXLS همراه پکیج Lazarus می‌آید، پس
// همان کد اکسپورت DB-grid در یک اپلیکیشن LCL کار می‌کند
var
  Exporter: TGridToXLS;
begin
  Exporter := TGridToXLS.Create(nil);
  try
    Exporter.DBGrid := GridOrders;
    Exporter.WorksheetName := 'Orders';
    Exporter.ExportHeader := True;
    Exporter.SetColumnsWidth := True;
    Exporter.ExportDBGrid;
    Exporter.SaveAs('orders.xls');
  finally
    Exporter.Free;
  end;
end;

یک هشدار کامپایلر چه ارزشی دارد

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

نتیجه عملی این است که کامپایلر دوم ارزش دارد در حلقه بماند حتی برای محصولی که عمدتاً روی اولی شپیود می‌شود. اسکن دوره‌ای کلاس‌های هشدار FPC یک گذر آنالیز استاتیک ارزان روی یک کد‌بیس دلفی است، و رده‌ای از نقص را پیدا می‌کند که هیچ test suite به‌طور قابل‌اعتماد به آن نمی‌رسد. انضباط وسیع‌تر ماتریس نسخه که این درونش نشسته در ماتریس بیلد چندکامپایلری توضیح داده شده

پشتیبانی Free Pascal و Lazarus برای ویندوز همراه HotXLS Delphi spreadsheet component به‌شکل یک پکیج Lazarus کنار پکیج‌های دلفی و C++Builder عرضه می‌شود، بیلدشده از همان درخت سورس نه یک فورک. این همان نکته تمرین است: یک موتور، چهار زنجیره ابزار، و تصمیم‌های وابسته به کامپایلر ایزوله‌شده در فایل‌های include که در یک نشست خوانده می‌شوند