מאמר טכני

מטריצת build בין מהדרי Delphi: HotXLS מאז XE5

HotXLS שולחת בסיס קוד אחד של Object Pascal לכל מהדורת Delphi ו-C++Builder מ-XE5 והלאה, ו-build-All-Lib-TRIAL.cmd הוא הסקריפט שמוכיח זאת: 43 legs של build, המכסים 12 גרסאות Delphi ב-Win32 וב-Win64 ועוד 10 builds של חבילות C++Builder ב-Win32 ו-9 ב-Win64. מ-v2.363 עד v2.374 הסקריפט מעולם לא הורץ עד סופו, וה-leg של XE5 היה שבור לאורך כל הזמן הזה

שום דבר בכשל לא היה עדין אחרי שראו אותו. חמישה מבנים שונים שהמהדר הנוכחי מקבל בלי הערה הם שגיאות קשות ב-RAD Studio XE5, שאותה מטריצת build מסמנת כ-12.0. גרסה 2.375.0 תיקנה את כל החמישה והמטריצה חזרה לירוק ב-43 מתוך 43. להלן כל דחייה, מדוע המהדר הישן צודק במידה מסוימת לגבי השתיים שהוא דוחה מטעמי טיפוס והחלק המביך יותר: סקריפט probe שנכתב כדי לאבחן את הבלגן דיווח pass שקרי בהרצה הראשונה שלו

מדוע ה-leg של XE5 התנוון בלי שאיש שם לב

ה-leg של XE5 התנוון מפני שפיתוח יומיומי הריץ רק את קבוצת ארבעת הסקריפטים של 37.0, ו-build מקומי ירוק אינו אומר דבר על מהדר שלא הפעלתם. המטריצה המלאה היא סקריפט נפרד ואיטי שה-installer של גרסת הניסיון קורא לו לפני ש-Inno Setup אוסף קבצים, לכן הוא מופעל בזמן packaging ולא בזמן commit. שתים-עשרה גרסאות נכנסו לפער הזה

כדאי לפרט את חשבון ה-legs מפני ששם חיה אשליית הכיסוי. DELPHI_TRIAL_VERSIONS מונה 12.0 עד 37.0 וכל אחת מ-12 הגרסאות האלה נבנית פעמיים, Win32 ו-Win64. CB_TRIAL_WIN32_VERSIONS מונה 10 גרסאות, ו-CB_TRIAL_WIN64_VERSIONS רק 9, מפני של-XE5 יש פרויקט package של C++Builder אך אין בו אובייקט startup של package ל-Win64, c0pkg64.o. שתים-עשרה ועוד שתים-עשרה ועוד עשר ועוד תשע הן 43. להריץ ארבע מהן ולקרוא לבסיס הקוד portable זו שגיאה קטגורית, וזו השגיאה המסוימת שאפשרה לזה לקרות

HotXLS נפגעה מאותה צורה של בעיה גם מהכיוון ההפוך. יחידה חדשה שנגישה דרך סעיף uses אך חסרה ברשימת הקבצים של .cbproj מתקמפלת היטב תחת Delphi, מפני ש-dcc מושכת יחידות שלא נרשמו במפורש לתוך ה-package ובמקרה הגרוע פולטת hint מסוג W1033. C++Builder מפיקה .obj רק עבור יחידות ששמן נמצא בתוך <DelphiCompile>, ולכן אותו קוד מת ב-stage של ilink עם unresolved external. Toolchain אחד מסתיר את מה שהשני תופס. זה כל הטיעון להריץ מטריצה ולא לסמוך על מהדר מייצג

Hard casts של טיפוסים שהמהדרים הישנים ב-Win32 דוחים

שתיים מתוך חמש הדחיות הן אותו באג בבגדים שונים: hard type cast שמוחל על ביטוי floating-point במקום על משתנה. ב-Win32 מהדרים ישנים מעריכים אריתמטיקה דרך stack של x87, לכן חיבור שמעורב בו Double נשמר בדיוק עודף של 80 ביט והטיפוס הסטטי שלו הופך ל-Extended בן 10 בתים. Casting של 10 בתים ל-TDateTime בן 8 בתים אינו typecast חוקי, והמהדר אומר זאת ב-E2089 Invalid typecast

הפרט המטריף הוא שצורת המשתנה תקינה. TDateTime(Serial) מתקמפלת בכל גרסה במטריצה מפני ש-Serial כבר בת 8 בתים וה-cast שומר על הגודל. מוסיפים משהו והביטוי מתרחב מתחתיכם. התיקון אינו cast רחב יותר או define מותנה, אלא להפסיק לעשות cast: assignment implicit מ-real ל-real ממיר נכון בכל מהדר ש-HotXLS תומכת בו, ואומר מה הקוד באמת מתכוון

// נדחה ב-XE5 (Win32): כל חיבור מחושב כ-Extended בן 10 בתים,
// וה-cast מצמצום 10 ל-8 מעלה E2089
if Dates1904 then
  Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
  Value := TDateTime(Serial + 1)
else
  Value := TDateTime(Serial);        // זה מתקבל: אין חיבור

// בטוח לפי גרסה: תן ל-assignment מ-real ל-real לבצע את ההמרה
if Dates1904 then
  Value := Serial + XLSDate1904Offset
else if Serial < 60 then
  Value := Serial + 1
else
  Value := Serial;

// אותה דחייה ב-packer של ערכי התא: cast קשיח של Double
// על integer. חלק במקום זאת - האופרטור כבר מחזיר real
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then   // E2089
  ;
if (Scaled = intVal) and (intVal / 100 = AValue) then           // portable
  ;

הענף Serial < 60 הוא אשליית שנת המעבר של 1900 ולא off-by-one: serial 60 הוא 1900-02-29 הלא-קיים של Excel, לכן serials שמתחתיו זקוקים ליום נוסף לפני ש-DecodeDate רואה אותם. עבודת portability לעולם לא צריכה לשנות בשקט לוגיקה כזו, וזו בדיוק הסיבה שהעריכה הבטוחה כאן מסירה את ה-cast ומשאירה את החשבון ללא שינוי

מה נשבר כאשר nil הוא ארגומנט פרוצדורלי

העברת nil חשוף במקום שבו מצופה טיפוס פרוצדורלי נכשלת בזמן overload resolution במהדרים הישנים. אתר הקריאה ב-HotXLS הוא ResolveIndexedColor, שהוא overloaded ומקבל callback מסוג TXLSTryResolveSystemColor שרוב ה-callers אינם צריכים. מהדרים חדשים פותרים את nil מול הפרמטר הפרוצדורלי ובוחרים את ה-overload הנכון. XE5 אינו עושה זאת, וה-diagnostic מצביע על קבוצת ה-overloads ולא על הארגומנט, וכך נשרפות עשרים דקות

התשובה הניידת היא לתת ל-null callback טיפוס. משתנה ברמת היחידה מהטיפוס הפרוצדורלי מאותחל לאפס על ידי השפה, לכן הוא כבר nil בלי initializer, והוא נושא את מידע הטיפוס שה-resolver הישן צריך. במקום שבו משתנה ברמת היחידה יהיה מוגזם, local מטופס שמוצב ל-nil עושה את אותו דבר

var
  // literal פרוצדורלי nil אינו נקשר ב-overload resolution של
  // המהדרים הישנים; משתנה מטופס ומאותחל לאפס כן
  NilSystemColorResolver: TXLSTryResolveSystemColor;

// ...

FWorkbook.ResolveIndexedColor(AIndexedColor, xicsBiffIcv, ARole,
  NilSystemColorResolver, Resolution);

// אותו תיקון עם local מטופס, ב-workbook של XLSX
function TXLSXWorkbook.ResolveIndexedColor(AIndex: Int64;
  ASpace: TXLSIndexedColorSpace;
  out AResolution: TXLSIndexedColorResolution): Boolean;
var
  NoResolver: TXLSTryResolveSystemColor;
begin
  NoResolver := nil;
  Result := ResolveIndexedColor(AIndex, ASpace, xicrGeneral, NoResolver,
    AResolution);
end;

שימו לב שזה הבדל אמיתי ברמת השפה ולא באג מהדר שכדאי לעקוף ב-defines. המשתנה שמאותחל לאפס נכון בכל גרסה במטריצה ועולה שורה אחת, לכן אין כאן conditional compilation כלל. השתמשו ב-{$IF CompilerVersion} רק כאשר הפלטפורמה באמת שונה בין גרסאות, וזה קורה בדיוק פעם אחת באצווה הזו

מתודות VCL מוגנות זזות בין גרסאות

TPicture.LoadFromStream היא public ב-VCL הנוכחית ו-protected בגרסאות הישנות ש-HotXLS תומכת בהן, לכן קריאה ישירה מתקמפלת עכשיו ונכשלת אז. HotXLS משתמשת בה כדי לאמת ש-payload של תמונת רקע של worksheet אכן מתפענח, בדיקת signature שרצה לפני שה-exporter ל-HTML מתחייב להטמיע את הבתים. תשובת ה-Pascal הקלאסית חלה: הגדירו descendant באותה יחידה רק כדי להרחיב visibility, ובצעו cast דרכו באתר הקריאה

type
  // TPicture.LoadFromStream היא protected בגרסאות ה-VCL הישנות שהספרייה
  // תומכת בהן; descendant באותה יחידה חושף אותה
  TXlsxPictureAccess = class(TPicture);

// ...

Stream.WriteBuffer(AData[1], Length(AData));
Stream.Position := 0;
TXlsxPictureAccess(Picture).LoadFromStream(Stream);
Result := (Picture.Graphic <> nil) and not Picture.Graphic.Empty and
  (Picture.Graphic.Width > 0) and (Picture.Graphic.Height > 0);

הטריק של accessor-class בטוח כאן מפני שה-descendant אינו מוסיף שדות ולעולם אינו נוצר; ה-cast רק משנה מה המהדר ירשה לכם לקרוא בשם. עדיין כדאי להשאיר הערה בהצהרה, מפני שקורא שבונה רק על IDE נוכחי יראה אחרת טיפוס חסר טעם. טיפול בתמונת רקע מופיע שוב בנתיב הרינדור של רשת VCL מותאמת, שבו אותו payload מפוענח מזין את הגיליון שעל המסך

טיפוס ה-token של GdiplusStartup השתנה פעמיים

הדחייה היחידה באצווה שבאמת דורשת conditional compilation היא טיפוס פרמטר ה-var של GdiplusStartup, שהשתנה בין דורות VCL באופן שלא משאיר איות יחיד שתקף בכל מקום. probing לפי גרסה קיבע את ההתנהגות בפועל: legs מ-12.0 עד 20.0 מקבלים רק Cardinal, legs של 21.0 ו-22.0 מקבלים רק THandle או ULONG_PTR, ו-23.0 ו-37.0 מקבלים את שניהם. בשמות release, זה Cardinal מ-XE5 עד 10.3 Rio ו-THandle מ-10.4 Sydney והלאה. מפני שטווחי הקבלה אינם חופפים עבור 12.0 עד 22.0, שום declaration בלתי מותנה אינו עובד: ה-guard נקשר ל-CompilerVersion >= 34, שהיא Sydney, והקריאה מוסמכת במלואה כ-Winapi.GDIPAPI.GdiplusStartup כדי שסדר פתרון היחידות לא יחליף declaration אחר בגרסה כלשהי באמצע הטווח

function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
  StartupInput: TGdiplusStartupInput;
  // טיפוס פרמטר ה-var של GdiplusStartup ב-GDIPAPI עוקב אחר
  // דור ה-VCL: Cardinal עד Rio, THandle מ-Sydney והלאה
  {$IF CompilerVersion >= 34}
  StartupToken: THandle;
  {$ELSE}
  StartupToken: Cardinal;
  {$IFEND}
  TiffEncoder: TGUID;
begin
  FillChar(StartupInput, SizeOf(StartupInput), 0);
  StartupInput.GdiplusVersion := 1;
  CheckStatus(Winapi.GDIPAPI.GdiplusStartup(StartupToken, @StartupInput,
    nil), 'startup');
  if GetEncoderClsid('image/tiff', TiffEncoder) < 0 then
    raise EInvalidGraphic.Create('GDI+ TIFF encoder is unavailable');
  // ... encode ...
end;

זהו ענף ה-TIFF של exporter תמונות העמוד, ולכן רדיוס הפגיעה של טעות כולל את כל משטח ייצוא ה-raster, כולל הנתיבים שמתוארים בייצוא טווח תאים כתמונה אחת. שימו לב גם למה שה-guard אינו טוען: ULONG_PTR ו-THandle הם באותו רוחב בשתי הפלטפורמות, לכן הבחירה היא לגבי איזה identifier ה-declaration מכנה ולא לגבי נכונות 32 סיביות מול 64 סיביות

מדוע הרצת ה-probe הראשונה דיווחה על כלום

ה-probe של הגרסה לא דיווחה דבר בהרצה הראשונה מפני שהשמות res=$(...) הושמו בתוך subshell, ושם הם אינם עוברים ל-parent. dcc32 יוצאת 0 בהצלחה, לכן קוד היציאה היה האות הנכון ללכידה, והסקריפט לכד אותו במשתנה שנעלם שורה לאחר מכן. כל leg חזר ריק והפלט נראה כמו probe שלא קימפלה דבר, וזה בדיוק מה שהוא היה

הכשל השני היה גרוע יותר, מפני שהוא הפיק תשובה שגויה במקום שום תשובה. ה-probe סיווגה leg לפי ספירת שורות שמתאימות ל-Error, ו-Delphi אינה מקדימה כל fatal במילה הזו. F1026 File not found הוא fatal ואינו תואם, לכן probe שלא הצליחה לפתור יחידה כלל קיבלה ציון pass נקי. XE5 אינה שולחת Winapi.GDIPOPS.dcu, ה-probe הראשונה פגעה בדיוק בזה וקיבלה ירוק כוזב. הכלל שיצא מכך צר ומפורש: שפטו probe של מהדר לפי artifact שנוצר או לפי שורת הסיכום של המהדר עצמו, לעולם לא לפי grep של הפלט עבור keyword. Grepping של stderr ל-Error הוא heuristic שנכשל בכיוון שאסור לכם להרשות, ומדווח בשקט על הצלחה

מה באמת עולה לתמיכה בעשור של מהדרים

החשבון הכנה הוא ששינויי הקוד כאן טריוויאליים ושינויי התהליך אינם. ארבע מתוך חמש הדחיות תוקנו בכתיבת Pascal רגיל יותר ולא בהוספת version machinery: מסירים cast, מחלקים במקום לבצע cast, נותנים ל-nil טיפוס ומצהירים על accessor class. רק GdiplusStartup הרוויחה {$IF}. בסיס קוד שנפרש מ-XE5 עד הגרסה הנוכחית אינו הופך לסבך של conditional defines אלא אם נותנים ל-hard casts ול-idioms של המהדר החדש ביותר להצטבר מלכתחילה

המחיר האמיתי הוא זמן build ומשמעת. 43 legs הוא סקריפט איטי, וזו בדיוק הסיבה שהוא נדד לזמן packaging ואז לעולם לא הורץ. דרך האמצע שאפשר להגן עליה היא לשמור לולאת ארבעת הסקריפטים המהירה לאיטרציה ולהריץ את המטריצה המלאה בלוח זמנים שאי אפשר לדלג עליו, מפני שמצב הכשל אינו build שבור שמבחינים בו, אלא IDE נתמך שהפסיק להיות נתמך בשקט לפני שתים-עשרה גרסאות

החובה הזו היא הצד השני של שליחת רכיב native בכלל. HotXLS קוראת וכותבת XLS, XLSX ו-ODS באמצעות Object Pascal בלבד, ללא התקנת Excel וללא תלות COM, וזה מה שמאפשר אוטומציית חוברות עבודה ללא Office על שרת נעול. אותה תכונה פירושה שהמהדר הוא כל חוזה הפלטפורמה, לכן כל גרסה במטריצה היא הבטחה שצריך לאמת מחדש ולא להניח

מטריצת ה-build בין המהדרים והקוד הבטוח לפי גרסה שמתואר כאן נשלחים כחלק מHotXLS Delphi Spreadsheet Component, שתומך ב-Delphi וב-C++Builder מ-XE5 ועד הגרסה הנוכחית עם binaries מוכנים לכל IDE נתמך