מאמר טכני

Free Pascal Win32: עיטור סמלי C ב-HotPDF

Free Pascal על Win32 מוסיף קו תחתון מוביל לכל ייבוא cdecl; external באופן אוטומטי, בזמן ש-public name מייצא את המחרוזת שכתבת, תו לתו. HotPDF צריך לענות על שתי המוסכמות באותו עץ מקור, כי בניית ה-Delphi כבר משלחת הצהרות ייבוא שמאייתות את הקו התחתון ביד. טעות באסימטריה הזו מניבה שגיאות קישור שמזכירות סמל שאף אחד לא כתב

הרחבת ספריית Delphi ל-Free Pascal מתוארת בדרך כלל כבעיית ניידות, וב-Win64 היא באמת בעיקר זה. Win32 שונה. ה-ABI של Windows על x86 בן 32 הסיביות נושא שלושים שנה של מוסכמות שהצטברו על איך סמלי C מאויתים, מי מנקה את המחסנית, ואילו פונקציות עזר פרטיות למהדר יחידת תרגום רשאית להניח, וכל אחת מאלה היא מקום שבו שני מהדרי פסקל שמסכימים על השפה עדיין יכולים לא להסכים על קובץ האובייקט

מדוע אותו סמל נפתר ב-Win64 ונכשל ב-Win32?

כי קידומת הקו התחתון היא מוסכמה של 32 סיביות ש-Free Pascal מחיל על ייבוא אבל לא על ייצוא. מצהירים function deflate(...): Integer; cdecl; external; ו-FPC מחפש _deflate בקובץ האובייקט ב-Win32, ואת deflate ב-Win64. זו התנהגות נכונה שתואמת את מה שמהדר C פולט. המלכודת היא בצד השני של הגשר: רוטינה שמסומנת public name 'deflate' מייצאת בדיוק את deflate בשני היעדים, בלי שום קידומת

עכשיו מוסיפים את הפרט ההיסטורי שהופך את זה לקונקרטי. בניית ה-Delphi כבר מצהירה על חלק מנקודות הכניסה האלה עם הקו התחתון כתוב בתוך השם, כי זה מה שקובצי האובייקט שלה מכילים. מזינים את אותה הצהרה ל-FPC ב-Win32 והמהדר מקפיד להוסיף לה קידומת שוב, ואז המקשר מצייד את __deflate, סמל ששום דבר לא מייצא. התיקון האינטואיטיבי, הוספת קו תחתון אחד בכל מקום, שובר את הייבוא שכבר אוית נכון

מה שעובד הוא זוג קבועי קידומת ולא קבוע אחד. HPDFFPCZLib ו-HPDFFPCCodecStubs משתמשים בקידומת אחת לייבוא C פשוט ובאחרת לייבוא שכבר נושא קידומת מצד Delphi, וב-Win64 שני הקבועים ריקים כך ששמות הקישור הקיימים שורדים ללא מגע. שני קבועים במקום אחד הם כל התיקון, וזה ברור רק אחרי שמפרידים בין כלל הייבוא לכלל הייצוא

אותן הצהרות סמלי C שמפוענחות על ידי Free Pascal ו-Delphi ב-Win64 וב-Win32: ייבוא cdecl מקבל קו תחתון רק ביעד של 32 סיביות, הצהרת Delphi שכבר אויתה עם קו תחתון הופכת ל-__deflate ונכשלת בקישור, בזמן שייצוא public name נשאר מילולי בשתי הארכיטקטורות
קבוע קידומת אחד לא יכול לשרת את שני הכללים: ייבוא cdecl פשוט וייבוא שכבר נושא את הקו התחתון של Delphi מעוטרים אחרת תחת FPC ב-Win32, ולכן HotPDF מחזיק שניים ומשאיר את שניהם ריקים ב-Win64
// שני קידומות, לא אחד: ייבוא C פשוט וייבוא שכבר נושא
// קידומת Delphi בכתב יד מעוטרים אחרת תחת FPC/Win32
const
{$IF DEFINED(FPC) and DEFINED(CPU32)}
  CPrefix     = '_';   // FPC מוסיף את זה בעצמו ל-cdecl external
  DelphiCName = '';    // כבר מאוית עם הקו התחתון במקור
{$ELSE}
  CPrefix     = '';
  DelphiCName = '';
{$IFEND}

// צד הייצוא: 'public name' הוא מילולי בכל יעד
procedure hpdf_codec_free(P: Pointer); cdecl;
  public name 'hpdf_codec_free';

WIN32 מספר לך מה הארכיטקטורה, לא מה ה-ABI

זו טעות ההידור-מותנה עם זנב הניפוי הארוך ביותר, ושווה לומר אותה במילים מפורשות: WIN32 ו-WIN64 מתארים את ארכיטקטורת היעד ולא אומרים דבר על אילו פונקציות עזר של זמן ריצה פרטיות למהדר קיימות. Free Pascal מגדיר את שני הסמלים על יעדי ה-Windows המתאימים, בדיוק כמו Delphi. מגן שנכתב כ-{$IFDEF WIN32} סביב קוד שקורא לפונקציית עזר של זמן ריצה של Delphi לכן מתקמפל תחת FPC ונכשל בזמן קישור

קונקרטית, שלוש משפחות קוד נופלות במלכודת. ה-trampolines של Delphi למספרים שלמים בני 64 סיביות שמגיעים אליהם דרך פונקציות העזר של System.@_ll, רוטינות התמיכה באסמבלי Win32 של MSVC, ומשבצות הייבוא שמלוות אותן כולן קיימות לשרת קובצי C שהודרו מראש ושבניית ה-Delphi מקשרת. Free Pascal לא מקשר את האובייקטים האלה, ולכן אינו צריך אף חלק מהמנגנון הזה, וכל הפניה אליו חייבת להיעלם. העדינות היא שגם ההצהרה וגם המימוש חייבים להישלל יחד. שוללים רק את אחד מהם והמהדר מדווח משהו לא עוזר על מזהה שאינו מצליח להתאים לשום דבר

הכלל שנשאר קצר. מגנים לפי המהדר כשהשאלה היא על ABI או תמיכת זמן ריצה, מגנים לפי הארכיטקטורה כשהשאלה היא על רוחב מצביע או מספר אוגרים, ולעולם לא נותנים לאחד לעמוד במקום השני

הגנה על הצהרות ומימושים יחד

בלוק מותנה במקטע ממשק קל ליפול לתוכו מבלי לשים לב, והודעת השגיאה שמתקבלת מצביעה לכל מקום חוץ מהגורם. מוסיפים הצהרת מתודה לממשק מחלקה והמקום הטבעי לשים אותה הוא ליד המתודות הקשורות, וזה בסדר עד הרגע שבו השכנים האלה מקרים לשבת בתוך בלוק {$IFDEF} קיים. הנחיות מותנות לא מוזחות, כך שבלוק שנפתח ארבעים שורות למעלה כמעט בלתי נראה בזמן קריאת ההצהרות הסובבות

מה שקורה אחר כך הוא הידור שמצליח ב-toolchain אחד ומייצר מפל באחר. אם המגן הסובב הוא בדיקת גרסת Delphi ש-Free Pascal לא עומד בה, ההצהרה נעלמת עבור FPC בזמן שהמימוש הבלתי-מותנה נשאר, והמהדר מדווח רשימה ארוכה של תלונות על מזהי מתודות שציפה להם ולא מצא. אף אחת מההודעות לא מזכירה את הבלוק המותנה שגרם לזה

שתי הרגלים מונעות את מעמד הכישלון כולו. לפני הכנסה למקטע ממשק, מביטים כלפי מעלה אחר המותנה הפתוח הקרוב ביותר במקום לסמוך על הקיבוץ הוויזואלי. ומתייחסים לסוויטת בדיקות Delphi ירוקה כראיה על Delphi בלבד: בניית הספרייה של Free Pascal היא שער נפרד, והדרך היחידה לדעת שהיא עוברת היא להריץ את build-Win32-Lib-FPC.cmd ואת build-Win64-Lib-FPC.cmd כחלק מאותו שינוי

מה נשבר בקוד האריתמטיקה של 32 סיביות

הגבלת שפה אחת מופיעה בדיוק בקוד הפחות מוכן להשתנות: Free Pascal ל-32 סיביות לא יקבל UInt64 כמשתנה בקרה של לולאת for. ביחידות העקומה האליפטית שנושאות את X25519 ו-X448, הלולאות שהולכות על מערכי limbs נכתבו עם מונים של 64 סיביות פשוט כי כל שאר הקובץ הוא של 64 סיביות

התיקון חייב להיות כירורגי, כי באריתמטיקת שדה רוחב של משתנה הוא חלק מהטיעון לנכונות. אינדקסי הלולאה הופכים ל-Integer, מאחר שלמערך limbs יש חופן אלמנטים ואף אינדקס לא מתקרב לטווח של 32 סיביות. כל מה שמשתתף באריתמטיקה, ה-limbs עצמם, העברת ה-carry והמסכות, נשאר UInt64, כי צמצום אחד מהם משנה בשקט את התוצאה מודולו הראשוני של השדה

// FPC ל-32 סיביות דוחה משתנה לולאה מסוג UInt64. מצמקים רק את האינדקס;
// ה-limbs, המסכות וה-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;

האימות לשינוי כזה לא יכול להיות בדיקת round-trip. הצפנה ופענוח עם אותו מימוש שבור מסכימים עם עצמם בצורה מושלמת, ולכן וקטורי known-answer כאן אינם נתונים למשא-ומתן: מריצים את וקטורי הבדיקה המפורסמים של X25519 ו-X448 ומשווים את בייטי הפלט המדויקים. זו הבדיקה היחידה שמבדילה מימוש נכון ממימוש שגוי ועקבי עם עצמו, והיא חלה באותה מידה על הפרימיטיבים הסימטריים שנדונים בגבולות הקודק של deflate ו-AES ב-Free Pascal

שתי נקודות השבירה של בניית Free Pascal Win32 של HotPDF: מגן {$IFDEF WIN32} סביב פונקציות עזר של זמן ריצה של Delphi שמתקמפל אך נכשל בקישור אלא אם הצהרה ומימוש נשללים יחד, ומשתנה הלולאה UInt64 בהליכות ה-limb של X25519 ו-X448 שמצומק ל-Integer בזמן שה-limbs, ה-carry והמסכות שומרים על רוחבם
מגנים לפי המהדר כשהשאלה היא ABI או תמיכת זמן ריצה ולפי הארכיטקטורה כשהיא רוחב מצביע, ואז מוכיחים שינויי אריתמטיקה מול וקטורי known-answer מפורסמים ולא בדיקות round-trip

מה שווה בניית Free Pascal של Win32

הרווח המעשי הוא שיישום Lazarus שמכוון ל-Windows של 32 סיביות מקבל את אותו מנוע מסמכים כמו המקביל שלו ב-Delphi, בלי חוזה בינארי נפרד לתחזק. זה חשוב ביותר בדיוק לפריסות שאנשים מעטים מדברים עליהן: בקרים תעשייתיים, מסופי נקודת מכירה ותוכנת line-of-business ארוכת חיים שבהן זמן הריצה של 32 סיביות אינו בחירה של ירושה אלא אילוץ חומרה

סיפור ה-Win64 בא קודם ומתואר בתמיכת Free Pascal ו-Lazarus ב-Win64. Win32 אינו הרצה חוזרת שלו. ל-Win64 יש calling convention אחת, בלי name decoration ובלי פונקציות עזר שלמות פרטיות ל-Delphi לעקוף, כך שכמעט כל מה במאמר הזה ספציפי ליעד של 32 סיביות. יחידות האריתמטיקה שהזדקקו לשינוי משתנה הלולאה הן היחידות שמתוארות באריתמטיקת Montgomery על העקומות של NIST, שם משמעת הרוחב מוסברת בעומק רב יותר

הלקח הכללי הוא שעבודת ניידות בין מהדרים אינה בעיקר עניין של תכונות שפה. שני המהדרים מקבלים כאן את אותו Object Pascal. מה ששונה הוא קובץ האובייקט: איך סמלים מאויתים, אילו רוטינות עזר מניחים שזמן הריצה מספק, ואילו אובייקטים שהודרו מראש יושבים בקישור. HotPDF משלח את החבילות של Free Pascal ו-Lazarus לצד אלה של Delphi ו-C++Builder ברכיב ה-PDF של HotPDF ל-Delphi, כך שאותו עץ מקור מזין כל toolchain ולא מתפצל לפי מהדר