HotXLS מתקמפל תחת Free Pascal ו-Lazarus על Windows, וההעברה נסבה סביב ארבע החלטות שאין להן קשר לתחביר של Object Pascal: לשמור את הליבה במצב DELPHIUNICODE, להכריז על ממשקי ה-structured storage של OLE כממשקי CORBA עם ספירת רפרנסים מנוהלת ידנית, להחליף את קובצי האובייקט של AES ל-Win32 במימוש פסקל, ולתקן לולאת inflate שיכלה לקבל ZIP קטוע כאילו הושלם
כל מי שהעביר ספריית Delphi בוגרת מכיר את הצורה של העבודה הזאת. הקומפיילר מקבל כמעט הכול במעבר הראשון. אחריו מגיע זנב ארוך של הבדלי התנהגות שמתקמפלים נקי ומייצרים תוצאות שגויות, ומנוע גיליונות חשוף אליהם בצורה יוצאת דופן כי הוא נוגע בקידוד טקסט, ב-structured storage של COM, בדחיסה ובקריפטוגרפיה בנתיב קוד אחד
למה הליבה מתעקשת על DELPHIUNICODE ולא על DELPHI פשוט?
כי מנוע הנוסחאות תלוי בכך ש-String ו-Char נושאים סמנטיקה של UTF-16, והחלופה ה-ANSI מאבדת תווים לפני שמשהו מגיע לקובץ. יש פיתוי לבנות את הליבה במצב DELPHI של FPC, כי זהו מתג התאימות שרוב ההעברות פונות אליו, והקוד מתקמפל. ואז workbook עם שמות גיליון בסינית או תוויות בקירילית עובר סבב שלם דרך נתיב החישוב, והתווים כבר אינם כשהכותב מקבל אותם — בלי שום שגיאה בשום מקום
המצב אינו אחיד לאורך הספרייה, וזה מכוון ולא בלגן. מפענח הבייטים של ה-PNG וה-overrides של ה-LCL באמת זקוקים לחתימות ANSI, כי הם עוסקים בבייטים ובמה שה-widgetset מעביר אליהם. היחידות האלה מפעילות מתג נפרד LX_FPC_ANSI. שני מצבים בספרייה אחת נשמע כמו code smell עד שמבחינים שהחלופה היא מפענח בייטים שמתייחס לקלט שלו כאל טקסט
יש פרט נלווה שתופס אנשים אחר כך. DELPHIUNICODE לא הופך את TFormatSettings.DecimalSeparator ל-WideChar בזמן הריצה של FPC. קלט שנושא מפריד עשרוני ב-Unicode חייב לעבור נרמול למפריד 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 של מחלקה ל-Windows בתור vtable של ממשק COM, גם כשההכרזה נראית זהה לזו ש-Delphi מקבל. הפריסות שונות באופן שמייצר קריאה למשבצת הלא נכונה, מה שמתבטא כקריסה במקום שאינו קשור לאתר הקריאה. structured storage חשוב כאן כי הפורמט הבינארי הקלאסי של workbook הוא קובץ compound של OLE, וקריאה או כתיבה של כזה משמעותה לממש ILockBytes שה-API של האחסון ב-Windows יקרא אליו חזרה
הסידור העובד הוא ממשק CORBA עם המשבצות של COM מוכרזות במפורש ו-AddRef ו-Release מנוהלים ידנית. זה אומר לוותר על ספירת רפרנסים אוטומטית לסוגים האלה ולקחת אחריות על מחזור החיים, מה ששווה בשביל מספר קטן של ממשקים שחיים בתוך יחידה אחת. המלכודת הספציפית בתוך העבודה הזאת היא QueryInterface: הוא חייב להחזיר מצביע ממשק, לא מצביע אובייקט. שניהם מתקמפלים. אחד מהם מוסר ל-Windows כתובת שהמילה המכונה הראשונה שלה אינה vtable
ההכרזות הייחודיות ל-FPC חיות ב-lxOleInterfaces.inc, לצד lxAESBackend.inc ו-lxZlibBackend.inc בתיקיית המקור של FPC, כך שההחלטות הספציפיות לקומפיילר יושבות במקום אחד במקום להיות פזורות דרך המנוע. הפורמט עצמו ואיך הספרייה מנווטת בו מתוארים בקריאת קובצי compound של OLE2 בפסקל
פרט טיפוסים נוסף שייך לאותה משפחה. LargeInt חייב להיפתר ל-Int64 בענף של FPC, וסיווג ה-Comp אצל הקומפיילר שונה מספיק בין שתי שרשרות הכלים שפתרון overloads יכול לבחור מועמד אחר. בודקים התנהגות של offsets גדולים עם stream של קובץ ולא עם stream של HGLOBAL: ה-stream של זיכרון גלובלי ב-Windows עושה wrap-around על חיפושים מעבר ל-4 GiB מעצמו, כך שבדיקה שעוברת שם לא מוכיחה דבר על האריתמטיקה שלך
מה מסתיר מימוש AES עקבי עם עצמו
קובצי האובייקט של AES ל-Win32 שבניית ה-Delphi מקשרת הם OMF, וה-linker של Free Pascal אינו מסוגל לצרוך אותם, ולכן הענף של FPC משתמש במימוש AES בפסקל במקום. Delphi ממשיכה לקשר את קובצי האובייקט שתמיד קישרה, מה ששומר את הבינארי המשוחרר ללא שינוי עבור הלקוחות הקיימים
דרישת האימות היא החלק ששווה לקחת לכל פרויקט. להצפין נתונים ולפענח אותם שוב עם אותו מימוש לא מוכיח כלום: אלגוריתם סימטרי עם key schedule שגוי, סדר בלוקים שגוי או chaining שגוי הוא עקבי עם עצמו בצורה מושלמת ויעשה round-trip לפלט שלו עצמו בכל פעם. רק known-answer vectors תופסים את זה, בבדיקת הרחבת המפתח, סדר הבלוקים וה-chaining של ה-CBC מול ערכים מפורסמים. משחררים מימוש שגוי שעקבי עם עצמו והסימפטום יופיע בפעם הראשונה שלקוח פותח את הקובץ ב-Excel
לדחיסה היה פגם מאופי אחר. backend של inflate בפסקל יכול עדיין להחזיק פלט ממתין אחרי שצרך את כל הקלט הדחוס שלו, ולכן הקורא חייב להמשיך לקרוא עד שה-stream מדווח על סופו. להתייחס לקלט שאזל כאל סוף-stream חותך את הבלוק האחרון. גרוע מכך, זה הופך ארכיון פגום לאחד שמתקבל בשקט, מה שהוא בדיוק מצב הכישלון שההקשחה באימות רשומת ה-end-of-central-directory של ZIP קיימת כדי למנוע. הכלל הוא שאין התקדמות בתוספת לא-סיום הוא שגיאת קטיעה, לעולם לא EOF
שתי מלכודות מערכת בנייה שעלו שעות אמיתיות
נתיבי החיפוש של ה-LCL חייבים לקדם את נתיבי ה-wildcard של החבילות של FPC, אחרת יחידת ה-Menus של Free Vision מצליפה את יחידת ה-LCL באותו שם ואתה מקבל אי-התאמת checksum של PPU שלא אומרת דבר על אף אחת מהן. התקנת Lazarus שהועברה אחרי ההתקנה יכולה גם להשאיר נתיבים מיושנים ב-fpc.cfg, ולכן נקודות הכניסה של הבנייה מציינות נתיבי יחידות ובינאריים במפורש במקום לרשת את מה שהסביבה מציעה
המלכודת השנייה אין לה שום קשר לפסקל. קובץ אצווה .cmd שנכתב עם סופי שורה מסוג LF עובד עד שהקובץ חורג מגודל חוצץ הקריאה של המפענח, ואז call :label נכשל בטענה שתווית האצווה לא קיימת, והכישלון מופיע בתוכנית שמקרית יושבת מעבר לגבול. כל כלי שכותב סקריפט אצווה מחדש חייב לכתוב CRLF בחזרה. ו-lazbuild --build-all מנקה את תיקיית הפלט של יחידות החבילה לפני הקומפילציה, כך שקובץ אפשרויות שחונה בתיקייה הזאת נמחק לפני שניתן לקרוא אותו: שומרים אותו בחוץ, וזוכרים שנתיב ה-@ נפתר ביחס לתיקיית החבילה כי lazbuild מפעיל את הקומפיילר משם
// ייצוא גריד ב-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 מדווח על משתנים מקומיים לא מאותחלים ש-Delphi לא, והרצת הבנייה של FPC הפכה את ההבדל הזה לשני פגמים אמיתיים ביחידת החישוב. פונקציה אחת קראה משתנה מונה שמעולם לא הוקצה לו ערך לפני השימוש, ואחרת השתמשה בשתי קואורדינטות בענף אחד לפני שהקוד שחישב אותן רץ בענף אחר. תחת Delphi שתיהן התנהגו לפי מה שהמחסנית במקרה החזיקה, וזו ההגדרה של באג שמשוחזר על מכונה אחת ולא על אחרת
המסקנה המעשית היא שהקומפיילר השני שווה להשאיר במעגל גם עבור מוצר שמשוחרר בעיקר על הראשון. סריקה תקופתית של מחלקות האזהרות של FPC היא מעבר ניתוח סטטי זול על בסיס קוד של Delphi, והיא מוצאת קטגוריית פגמים שאף חבילת בדיקות לא מגיעה אליה באופן אמין. משמעת מטריצת הגרסאות הרחבה יותר שזה יושב בתוכה מתוארת במטריצת הבנייה הבין-קומפיילרית
התמיכה ב-Free Pascal וב-Lazarus עבור Windows מגיעה עם רכיב הגיליונות של HotXLS ל-Delphi כחבילת Lazarus לצד החבילות של Delphi ושל C++Builder, בנויה מאותו עץ מקור ולא מ-fork. זו הנקודה של כל התרגיל: מנוע אחד, ארבע שרשרות כלים, וההחלטות הספציפיות לקומפיילר מבודדות בקובצי include שאפשר לקרוא אותם בישיבה אחת