PDFlibPas נבנה תחת Free Pascal עבור Windows בגרסת 32 סיביות, והחלק הקשה מעולם לא היה הפסקל. החלק הקשה היו קובצי האובייקט: האובייקטים של AES ו-OpenJPEG שבניית ה-Delphi מקשרת הם OMF, המקשר הפנימי של Free Pascal דורש COFF, וההמרה בין השניים מייצרת שמות מקטעים וסמלי הגדרת מקטעים שגורמים למקשר להיכשל בשגיאות פנימיות במקום באבחון שימושי
כל מי שקישר פעם אובייקטים של C לספריית פסקל מכיר את השטח הזה. Win64 מתנהג יחסית בנימוס, עם פורמט אובייקט אחד, calling convention אחת ובלי name decoration. Win32 שומר כל שכבה של היסטוריה שהפלטפורמה צברה, וספרייה שמקשרת סטטית קוד C של צד שלישי פוגשת את כולן בבת אחת
תיקיית המהדר לא אומרת לך מהו היעד
מתחילים מנקודת הכניסה של הבנייה, כי טעות כאן גוזרת שעות לפני שקובץ אובייקט כלשהו מעורב בעניין. שם תיקיית ההתקנה של Free Pascal מזהה היכן יושב המהדר הראשי, לא מה הוא מפיק. מהדר מארח של 32 סיביות יכול להפעיל cross-compiler שיושב לצידו ולפלוט קוד של 64 סיביות כשמעבירים לו את מתגי היעד הנכונים, ולכן הסקת היעד מנתיב היא ניחוש שמצליח במקרה, עד שמישהו מארגן מחדש את ה-toolchain שלו
הגישה האמינה היא לשאול את המהדר. שולחים שאילתה על המעבד ומערכת ההפעלה של היעד בפועל דרך מתגי המידע של המהדר עצמו, ולוקחים בחשבון את שתי פריסות ההתקנה הנפוצות, תיקיית ההרצה השטוחה וזו המקוננת לפי גרסה, כי מתקינים ומנהלי toolchain שונים מייצרים צורות שונות. סקריפט בנייה שמקבע בקוד את אחת הפריסות עובד בדיוק על מכונה אחת
מדוע קובץ אובייקט מומר מפיל את המקשר הפנימי?
כי ההמרה שומרת על מוסכמת שמות המקטעים של OMF ומאלתרת סמלי הגדרת מקטעים שלא תואמים למה שהמקשר של COFF מצפה למצוא. המרת האובייקטים מ-OMF ל-COFF היא תנאי הכרחי ולא מספיק: הקבצים שיוצאים נושאים את שמות המקטעים הקלאסיים _TEXT, _DATA ו-_BSS, בתוספת שמות סמלי הגדרת מקטעים שנגזרו מהם, והזנת זה למקשר הפנימי של Free Pascal מניבה שגיאות מהדר פנימיות במקום הודעה על שמות מקטעים
שגיאה פנימית היא מצב הכישלון הגרוע ביותר לבעיית בנייה, כי היא לא אומרת דבר על מה השתבש בקלט. התיקון הוא מעבר נרמול אחרי ההמרה על קובץ ה-COFF: כותבים מחדש את שמות המקטעים לצורה המצופה ואת סמלי הגדרת המקטעים המתאימים, בלי לגעת באינדקס הסמלים, בבייטים של הקוד וב-relocations. האילוץ האחרון הזה הוא כל הקושי. כתיבה מחדש שממספרת סמלים מחדש או מזיזה offsets מניבה אובייקט שנקשר בהצלחה ואז קורס
יש שלב מקדים עבור אחת משתי ערכות האובייקטים. האובייקטים של OpenJPEG שנבנו עם מהדר ה-C++ הקלאסי ל-32 סיביות תלויים ברוטינות עזר פרטיות של Delphi למספרים שלמים בני 64 סיביות, ש-Free Pascal לא מספק, ולכן שום כמות של המרת פורמט לא הופכת אותם לשמישים. אותם בונים מחדש קודם עם המהדר מבוסס ה-Clang, שלא פולט את התלויות האלה, וממירים אחר כך
// אובייקטים עבור יעד ה-FPC יושבים בתיקייה משלהם. הם אינם
// מחליפים את ערכת האובייקטים של Delphi, כי שתי ה-toolchain בונות
// מאותו עץ מקור ולכל אחת דרושים קלטי קישור משלה
//
// Lib\thirdparty\Win32 אובייקטי OMF של Delphi, ללא שינוי
// Lib\thirdparty\Win32f אובייקטי COFF של FPC, מומרים ומנורמלים
//
// נקודות כניסה של הבנייה:
// build-Win32-Lib-FPC.cmd
// build-Win64-Lib-FPC.cmd
פונקציות עזר פרטיות למהדר אינן ניידות, וגם המוסכמות שלהן לא
זמן הריצה של Delphi מספק trampolines באסמבלי לפעולות על מספרים שלמים בני 64 סיביות על x86 של 32 סיביות, ואובייקטים של C שהודרו מראש עבור Delphi קוראים אליהם. ל-Free Pascal יש ארגון משלו, ולכן את ההפניות האלה צריך לספק אחרת ולא להפנות מחדש. הפרט שהופך הפניה מחדש לבלתי אפשרית הוא ה-calling convention: את הארגומנט בן ארבעת הבייטים של פונקציית העזר למדידת זמנים שקוד עיבוד התמונות משתמש בה מנקה ה-callee, בעוד שפונקציית העזר לחילוק של 64 סיביות מנקה שישה עשר בייטים ומחזירה את התוצאה בזוג האוגרים הקלאסי. שתי פונקציות עזר, שתי מוסכמות, ו-trampoline שנכתב לאחת משחית בשקט את המחסנית עבור השנייה
Name decoration מוסיף את החצי השני של הבעיה. ב-Win32, Free Pascal מוסיף קו תחתון אוטומטית לייבוא חיצוני של C ובמקביל מייצא הצהרות public name כלשונן, כך שצד הייבוא וצד הייצוא של אותו גשר פועלים לפי כללים שונים. הגשר לזמן הריצה של C ש-OpenJPEG צריך חייב אם כן לייצא את שמות הסמלים המדויקים של C, ונקודות הכניסה הווריאדיות צריכות קפיצה עקיפה של 32 סיביות ולא ישירה. אף אחד מהדברים האלה לא אקזוטי ברגע שאומרים אותם. וכולם נופלים כשגיאת קישור שמזכירה סמל שאף אחד לא כתב
מה גרם לקובץ הרצה של Win32 למות לפני main?
DLL של 64 סיביות על נתיב החיפוש, שנכנס לתמונה כי היחידה zlib של Free Pascal נקשרת דינמית ולא סטטית. הסימפטום היה יציאה מיידית עם קוד הסטטוס של תמונה לא תקפה, לפני שרץ כל קוד פסקל בתוכנית, מה ששולח אותך לבדוק את התוכנית שרק בנית כשהתקלה היא ב-loader שמפענח ייבוא מול ארכיטקטורה לא נכונה
הלקח הוא על הנחות יסוד ולא על zlib. יחידה שנושאת שם של ספריית דחיסה לא בהכרח מכילה אחת; היא עשויה להיות binding שמצפה לספרייה משותפת בזמן ריצה, ותלות דינמית שלא כיוונת אליה היא נטל פריסה גם כשהיא נפתרת במקרה. מעבר למימוש ה-stream הטהור בפסקל נותן לשני היעדים נתיב דחיסה שנכלל סטטית וללא שום תלות חיצונית, וזה בדיוק מה שספרייה שמוטמעת בתוכנה של מישהו אחר צריכה מלכתחילה
אותו אינסטינקט חל על ה-backend החיצוני של המקודד ל-JBIG2. ביעד של 32 סיביות המקודד החיצוני לא מקושר, כך שבקשות נופלות חזרה למקודד הפסקל המובנה, והבדיקה שמאמתת את זה צריכה לבדוק את מצב הרישום של היעד הנוכחי ולא להתייחס לקידוד מוצלח כהוכחה שה-backend החיצוני נוכח. fallback שעובד הוא בדיוק הדבר שמסתיר תלות חסרה, וזהו דפוס הכישלון שנבחן באבחון כשלים שקטים של stub. עבודת הקישור הסטטי ל-64 סיביות מכוסה בקישור סטטי של jbig2enc תחת FPC
אריתמטיקה של 32 סיביות על memory stream
קוד שמתמרן גדלי חוצץ עם אריתמטיקה ללא סימן ברוחב מצביע נכון ב-Win64 ונמצא תמונה גדולה אחת מגלישה ב-Win32. ה-stream בזיכרון שמזין את הקודק של JPEG 2000 גדל בהכפלה בשניים ומתקדם בחיבור, וביעד של 32 סיביות שתי הפעולות יכולות להתגלגל על קלטים שגדולים אבל לגיטימיים לחלוטין
לכן כל כתיבה, דילוג, seek והקצאה ראשונית בודקים לפני שמחשבים, ותקרת הקיבולת היא הערך הסימני המרבי ברוחב מצביע, שנבחר להתאים למה שרוטינת העברת הבלוקים וערכי ההחזר של ה-callback מסוגלים להביע. דרישת ההתנהגות כשבקשה נדחית קלה לפספס: סירוב לא רשאי לשנות את מיקום ה-stream או את אורכו. מוטציה חלקית ואז שגיאה משאירה את ה-stream במצב שהקורא לא יכול להסתמך עליו, והפעולה הבאה מסבכת את זה עוד
// בדקו לפני שמחשבים. ב-Win32 שתי הפעולות האלה מתגלגלות על קלטים
// שתמונת JPEG 2000 גדולה מייצרת כרגיל
if Needed > NativeUInt(High(NativeInt)) - FPosition then
Exit(False); // סירוב, בלי לגעת במיקום ובגודל
NewCapacity := FCapacity;
while (NewCapacity < FPosition + Needed) do
begin
if NewCapacity > NativeUInt(High(NativeInt)) shr 1 then
Exit(False); // הכפלה הייתה גולשת
NewCapacity := NewCapacity shl 1;
end;
שתי מלכודות של פלט בנייה ששורדות את הפורט
הפרדת קובצי ההרצה של בדיקות ודוגמאות לפי ארכיטקטורת יעד לתיקיות פלט פר-יעד נשמעת נכונה לחלוטין ומיד שוברת כל דבר שאיתר את נתוני הבדיקה שלו על ידי ספירת רמות תיקייה כלפי מעלה. התיקון הוא לחפש כלפי מעלה את תיקיית הנכסים במקום להניח עומק קבוע, עם הגבלה אחת מכוונת: הדוגמה של החתימה מקבלת fallback של תעודה רק מתיקיית הפרויקט שלה עצמה, לעולם לא מאב קדום שרירותי, כי תעודה באותו שם שנמצאת גבוה יותר בעץ היא הפתעה אבטחתית ולא נוחות
המלכודת השנייה שורדת כל פורט ושווה לשאת לכל פרויקט FPC. אחרי שדרוג מהדר, לא מספיק שהמהדר דוחה קובצי PPU ישנים, כי המקשר עדיין מעדיף קובצי אובייקט שנשארו בנתיב החיפוש של היחידות גם כשה-PPU שטען הגיע מהתיקייה הנכונה, והוספת נתיב פלט אובייקט מפורש לא גוברת על ההעדפה הזו. התשובה האמינה היחידה היא תיקיית יחידות זמנית טרייה לכל סבב בנייה. כל פחות מזה מניב בינארי שנקשר משתי גרסאות מהדר, שנכשל בדרכים שנראות כמו באגים בקוד המקור
תנאי פלטפורמה הם החתיכה האחרונה, ובחירת הציר הנכון חשובה יותר משנדמה. השאלה הנכונה היא בדרך כלל האם הקוד ספציפי ל-Windows ולא האם ספריית widget מסוימת נוכחת, כפי שהראתה עבודת המרת ה-metafile בייבוא וקטורי של EMF ותנאי פלטפורמה: החלפת התנאי ההוא מתנאי ספריית פקדים לתנאי פלטפורמה הפך כתיבה מחדש מיועדת לשינוי של directive אחד. תמיכת Free Pascal ו-Lazarus בשני יעדי ה-Windows מגיעה עם ספריית ה-PDF של PDFlibPas ל-Delphi, הנבנית מאותם מקורות כמו החבילות של Delphi ו-C++Builder