מאמר טכני

קישור סטטי של jbig2enc ל-Free Pascal ללא DLL

PDFlibPas 3.538.0 מקשרת את מקודד JBIG2 החיצוני באופן סטטי לתוכניות Free Pascal ו-Lazarus. בפרויקט מוסיפים את היחידה PDFlibJBIG2EncC, אותה יחידה שכבר משמשת את Delphi ואת C++Builder, ובסופו של דבר המקודד נמצא בתוך קובץ ההרצה בלי דבר נוסף שצריך להפיץ לצדו. בכך מתהפכת המסקנה הקודמת לגבי התכונה הזו, שלפיה Free Pascal יכול להגיע למקודד החיצוני רק דרך DLL

מדוע ה-DLL נראה כמו האפשרות היחידה

ה-DLL נראה כמו האפשרות היחידה מפני ששלושה מסלולי קישור נכשלו בשלוש דרכים שאינן קשורות זו לזו, ואף מתג מהדר לא הגיע לאף אחד מהם. המקשר הפנימי דוחה על הסף מקטעי COMDAT אסוציאטיביים. קישור חיצוני דרך binutils המצורפים קורס בתוך איסוף מקטעים שאינם בשימוש, שאותו Free Pascal מעביר ללא תנאי ביעד Windows של 64 סיביות. binutils חדש יותר אינו מסוגל לעבד כלל את סקריפט הקישור של Free Pascal. בנייה מחדש של צד ה-C++ עם שרשרת הכלים האחרת מחליפה סירוב אחד באחר, מפני שיצירת מופעים של תבניות ו-inline מייצרת מטבעה סמלים חיצוניים חלשים, ו-Free Pascal מדווח עליהם כ-Unsupported COFF symbol type 105. שום ראיה מכל זה לא הייתה שגויה, והתיאור הקודם של backends למקודד JBIG2 ושל מקשר Free Pascal עובר על כל מבוי סתום בצורה שעדיין ניתנת לשחזור כיום. מה שהיה שגוי הוא ההנחה היכן יכול להימצא התיקון. כל ניסיון עבר דרך מהדר או מקשר, ואף אחד מהם אינו יכול לשנות את מה שכבר נמצא בקובץ אובייקט. קובץ האובייקט היה הבעיה לאורך כל הדרך. ObjConv קורא COFF וכותב COFF, ולכל מבנה ש-Free Pascal נחנק ממנו יש שקילות מכנית שהוא כן מקבל

השגיאה שאינה מציינת את הסיבה

המקשר הפנימי של Free Pascal מממש COMDAT מסוג pick-any רק באופן חלקי, והיישום החלקי הזה הוא הדבר הקשה ביותר לאבחון כאן. הוא אכן מאחד הגדרות כפולות, כפי שהפורמט דורש. אבל TExeOutput.RemoveUnreferencedSections מפנה דרך exesymbol להגדרה המנצחת כשהוא מסמן מקטעים שנמצאים בשימוש, ואילו TCoffexeoutput.DoRelocationFixup קורא ישירות את objreloc.symbol.objsection. כאשר מקטע שנמצא בשימוש מפנה לסמל שהאובייקט שלו עצמו מגדיר בעותק שהפסיד באיחוד, שני המעברים מסתכלים על מקטעים שונים, והקישור נעצר עם Internal error 200603061

השוו זאת לשתי המגבלות שמשני צדדיו. Unsupported COFF symbol type 105 אומר external חלש. Associative or exact match COMDAT sections are not yet supported אומר COMDAT אסוציאטיבי ואף מציין את הסמל הבעייתי. Internal error 200603061 אינו אומר דבר: לא שם סמל, לא שם מקטע, לא שם קובץ ולא שלב. זה גם המקרה הרגיל ולא פינה נדירה, מפני ש-MSVC מכניס כל literal של מחרוזת וכל יצירת מופע של inline או תבנית ל-COMDAT מסוג pick-any, וב-186 האובייקטים של קבוצת המקודד הזו המקשר ביצע 2656 איחודים. בנייה עם /Gy- משאירה פונקציות רגילות מחוץ למקטעי COMDAT נפרדים לכל פונקציה, אך משאירה את ה-literals של המחרוזות ואת יצירות המופעים של התבניות בדיוק במקומם

מדוע ה-stub האחרון ל-CRT תמיד נראה כאילו הוא שבר את הבנייה

מפני שהמקשר מגיע למעבר התיקונים רק אחרי שכל הסמלים נפתרו. כל עוד משהו חסר, הריצה מסתיימת מוקדם עם Undefined symbol ובעיית ה-COMDAT אינה מקבלת הזדמנות להיחשף. ממלאים את ה-stub האחרון של סביבת הריצה C והמקשר מתקדם שלב אחד, ישר אל Internal error 200603061. לכן התסמין בשטח מטעה באופן שיטתי: כשמוסיפים בזה אחר זה גופי Pascal לסמלי ה-C שאליהם יש הפניות, תמיד נראה שהתוספת האחרונה שברה את הבנייה, או שנחצה סף כלשהו סביב מאה stubs. אף אחת מהאפשרויות אינה נכונה. איזה סמל נוסף אחרון וכמה סמלים נוספו בסך הכול אינם רלוונטיים, מפני שהכשל היה חבוי כבר מהאובייקט הראשון ורק נעשה נגיש לאחר שהפתרון הצליח. כאשר מקשר משנה את התלונה שלו אחרי שתיקנתם משהו שאינו קשור, שאלו אם קידמתם שלב במקום לגרום לנסיגה

התיקון הוא מעבר ObjConv אחד, לא מתג מהדר

התיקון כולו הוא פקודת עיבוד-אחרי יחידה שמריצים על כל אובייקט מקומפל: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. שלוש מהאפשרויות האלה נוספו עבור העבודה הזו. -xw פותר סמלים מסוג IMAGE_SYM_CLASS_WEAK_EXTERNAL ל-external רגילים. -xn מנרמל סמלים מסוג IMAGE_SYM_CLASS_NULL, כגון _fltused, שעליהם Free Pascal מדווח כ-Unsupported COFF symbol type 0. -xc עושה את העבודה העיקרית: הוא מוריד כל מקטע COMDAT למקטע רגיל והופך את הסמלים שהוא מגדיר לסטטיים. כך הכשל נעלם מפני שההחלטה נעלמת: ללא מקטעי COMDAT אין איחוד, אין עותק מנצח שמעבר אחד יכול להפנות אליו ומעבר אחר לפספס, וגם מקטעי ה-unwind האסוציאטיביים .pdata ו-.xdata נעלמים יחד איתו. למחיר יש ממשות, אך הוא קטן: עותקים שהיו יכולים להתאחד באופן חוקי שורדים כעת כל אחד בפני עצמו

שינוי הקידומת -np:__imp_:pdflibimp_ פותר התנגשות נפרדת. MSVC קורא ל-API מיובאים של Win32 דרך תאי עקיפה בשם __imp_*, Free Pascal שומר את הקידומת הזו למנגנון הייבוא שלו, והגדרה ישירה של אחד מהשמות האלה מפעילה שוב את Internal error 200603061. שינוי שמות התאים מאפשר לצד Pascal לפרסם אותם כמשתנים רגילים ולמלא אותם בזמן ריצה. האובייקטים עצמם מקומפלים עם דגל הקישור הסטטי /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c-, ועם מקודדי התמונות כבויים, כך שנתיבי קלט/פלט קבצים ומקודדים שאינם בשימוש דורשים הרבה פחות stubs לצורכי קישור בלבד. הם נוחתים ב-Lib\thirdparty\Win64f, בעוד שמסלול Delphi ו-C++Builder ממשיך לקשר את קבוצת Win64x שלו ללא שינוי, וזו התוצאה הנכונה לתיקון ניידות שמוגבל לשרשרת כלים אחת

מה צד Pascal עדיין חייב לייצא

Free Pascal פותר את הייבוא של אובייקט C לפי שם הסמל, ולכן צריך לכתוב במפורש את השם הזה: לכל שגרת Pascal שמחליפה נקודת כניסה של C יש סעיף public name. Delphi משתמש בשם השגרה כשם הסמל ואינו זקוק לסעיף כזה, ולכן יחידה אחת משרתת את שני המהדרים כשהסעיפים נמצאים תחת {$IFDEF FPC}. המלכודת היא שהצהרת external 'msvcrt.dll' אינה מספקת דבר: היא יוצרת import, ולא הגדרה שאובייקט מקושר יכול להיקשר אליה. גוף ההעברה חייב להתקיים

// הצהרה חיצונית יוצרת import בלבד. שום אובייקט מקושר לא יכול
// לקשר אליה
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  external 'msvcrt.dll' name 'memcmp';

// גוף Pascal שמפורסם תחת שם סמל C המדויק הוא מה שקבוצת
// האובייקטים באמת מקשרת אליו
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  public name 'memcmp';
begin
  Result := crt_memcmp(Buf1, Buf2, Count);
end;

נקודות כניסה עם מספר ארגומנטים משתנה שוברות את התבנית הזו, מפני ש-wrapper של Pascal אינו יכול להעביר את ה-varargs שלו ל-callee אחר עם varargs. הפתרון הוא להפסיק להיות wrapper: לייצא שגרה עירומה תחת שם ה-C ולבצע tail-jump למימוש האמיתי, כאשר אוגרי הארגומנטים והמחסנית נשארים בדיוק כפי שה-caller סידר אותם. שכבת JPEG 2000 כבר מטפלת כך ב-snprintf וב-vsnprintf, וקופצת לאיותים עם קו תחתון של msvcrt מפני שהשמות הרגילים מיוצאים רק על ידי UCRT. מגבלה קשורה מגיעה מאותה שגיאה פנימית: תאי הייבוא ששמם שונה מתמלאים מתוך מקטע initialization דרך GetModuleHandleA ו-GetProcAddress ולא מאתחולים סטטיים, מפני שלקיחת הכתובת של שגרה מיובאת בתוך מאתחל גורמת למהדר לפלוט תיקון שהוא אינו יודע לטפל בו ולהיכשל שוב עם 200603061

function crt_sprintf(Buffer: PAnsiChar; Fmt: PAnsiChar): Integer; cdecl; varargs;
  external 'msvcrt.dll' name 'sprintf';

var
  SPrintfTarget: Pointer = @crt_sprintf;

// אי אפשר להעביר varargs מ-wrapper של Pascal, לכן הסמל המיוצא
// קופץ לסופו של המימוש כשהמסגרת נשארת כפי שה-caller הכין אותה
procedure jbig2_sprintf; assembler; nostackframe;
  public name 'sprintf';
asm
  jmp qword ptr [rip + SPrintfTarget]
end;

מה פרויקט Free Pascal עושה אחרת עכשיו

שום דבר מלבד שם היחידה בסעיף uses, וכבר אין קובץ שצריך להפיץ. ה-backend רושם את עצמו מתוך מקטע ה-initialization שלו דרך RegisterJBIG2EncoderBackend, והקוראים מבקשים אותו בדיוק כמו קודם: דרך סיבית האפשרויות PDF_JBIG2_OPTION_EXTERNAL_ENCODER, שערכה 4, או דרך הארגומנט UseExternalEncoder של נקודות הכניסה המורחבות. הבקשה נשארת העדפה ולא הבטחה, מפני שבנייה שמשמיטה את היחידה חוזרת בשקט למקודד Pascal הטבעי של MMR ומייצרת קבצים גדולים יותר במקום שגיאה

uses
  Classes, SysUtils,
  PDFlibrary,
  PDFlibJBIG2EncC;   // Delphi, C++Builder ומגרסה 3.538.0 גם Free Pascal

var
  Pdf: TPDFlib;
  Scan: TStream;
  ImageId: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.NewDocument;
    Pdf.NewPage;
    Scan := TFileStream.Create('scan-page-1.tif', fmOpenRead);
    try
      // Interpolate, SymbolExtract, UseExternalEncoder, SkipBlackDots,
      // BlackDotSize, LossyLevel
      ImageId := Pdf.AddImageJBIG2FromStreamEx(Scan, 0, 1, 1, 0, 0, 0);
    finally
      Scan.Free;
    end;
    if ImageId = 0 then
      raise Exception.Create('JBIG2 encoding failed');
    Pdf.SaveToFile('archive.pdf');
  finally
    Pdf.Free;
  end;
end;

שתי מגבלות ראוי לציין במפורש. קיימת רק קבוצת אובייקטים ל-Win64, ולכן בכל יעד Free Pascal אחר נקודת הכניסה לקידוד החיצוני מדווחת על כשל והמקודד הטבעי של Pascal מבצע את העבודה. וגם הבדיקה שמאפשרת את כל זה היא השוואת רינדור ולא בדיקת גודל: שני המקודדים חסרי-הפסד על אותו מקור, ולכן הפלט עובר רינדור והשוואה בייט-לבייט, כאשר חבילת Lazarus עוברת 26 מתוך 26 כולל הבדיקה הזו. השוואת גדלי זרמים דחוסים לא הייתה מוכיחה דבר, מפני שעמוד הפוך נדחס בערך לגודל של עמוד נכון

הלקח הרחב יותר מתקיים גם מעבר ל-JBIG2. DLL הוא הצורה הנכונה כאשר הגבול באמת דינמי, וזה המקרה שלשמו קיימים משטחי האינטגרציה של DLL, ActiveX ו-dylib; הוא הצורה הלא נכונה כאשר הוא רק workaround לקורא COFF, מפני שהוא מוסיף קובץ לכל מתקין, נתיב חיפוש לכל פריסה ומצב כשל של אי-התאמת גרסאות שקישור סטטי אינו יכול לייצר. גם הצד שלפני המקודד חשוב, מפני שהאופן שבו מופקת התמונה הבינארית קובע יותר מהמקודד לגבי הגודל הסופי, ורינדור מונוכרומי לפי אזורים ב-Delphi מכסה את החצי הזה של הצינור. הכיסוי לפי שרשרת כלים, קבוצות האובייקטים לכל מהדר והיעדים הנתמכים מופיעים בדף המוצר losLab PDF Developer Library