כאשר קובץ pdfium.dll מסרב להיטען במחשב של לקוח, רכיב ה-PDFium עבור דלפי ולזרוס הופך את השגיאה המקורית המסתורית לגורם ספציפי. שגרת CheckLoadLibrary מפרשת את ERROR_BAD_EXE_FORMAT (193) כאי-תאימות ארכיטקטורה של 32/64 סיביות, ו-CheckGetProcAddress מדווחת על ייצוא (export) חסר כקובץ pdfium.dll שהוא ישן יותר מהקישור (binding). שתי ההודעות מצביעות על התיקון במקום להשאיר אותך לנחש
זה חשוב מכיוון ש-DLL מקורי נכשל בשלוש דרכים שונות, והטקסט הגולמי של מערכת ההפעלה מאחד את כולן. הארכיטקטורה יכולה להיות שגויה, הקובץ הבינארי שנפרס יכול להיות ישן מדי עבור קישור ה-Pascal שקורא לו, או ששני תהליכונים (threads) יכולים להתחרות על קישורו באותו רגע. לכל אחת מהבעיות יש תיקון שונה, וכל המטרה של האבחונים שנוספו בעבודת מחזור חיי הטעינה בגרסה v2.11.0 היא לומר לך במה אתה באמת מסתכל
מדוע pdfium.dll נכשל בטעינה עם שגיאת פורמט EXE שגוי?
שגיאת Windows 193, ERROR_BAD_EXE_FORMAT, פירושה שהקובץ נמצא ונפתח אך סוג מכונת ה-PE שלו אינו תואם לתהליך המארח. קובץ הרצה של 64 סיביות אינו יכול לטעון pdfium.dll של 32 סיביות, וקובץ הרצה של 32 סיביות אינו יכול לטעון אחד של 64 סיביות. הקובץ קיים, ולכן האינסטינקט לחפש DLL חסר שולח אותך בדיוק לכיוון הלא נכון
רכיב ה-PDFium מיירט מקרה זה ב-CheckLoadLibrary: כאשר LoadLibrary מחזיר handle ריק ו-GetLastError הוא 193, הוא מצרף רמז שה-DLL נמצא אך הארכיטקטורה שלו אינה תואמת לתהליך המארח, ומצביע על הבנייה המתאימה של DLLs/Win32 או DLLs/Win64. תת-הספרייה נבחרת על ידי BuildDllSubDir, המחזירה Win64 כאשר IsWin64 הוא true ו-Win32 אחרת. פרוס את ה-DLL תחת העץ שבו הקישור באמת מחפש והאי-תאימות תיעלם
uses
SysUtils, PDFium;
procedure OpenDocument(const AFileName: string);
var
Pdf: TPdf;
begin
// Deploy pdfium.dll next to the executable, matched to the build target:
// <AppDir>\DLLs\Win32\pdfium.dll for a 32-bit host process
// <AppDir>\DLLs\Win64\pdfium.dll for a 64-bit host process
Pdf := TPdf.Create(nil);
try
Pdf.FileName := AFileName;
try
Pdf.Active := True; // first use lazily binds pdfium.dll
except
on E: EPdfError do
// The message already names the real cause: a 32/64-bit mismatch
// (ERROR_BAD_EXE_FORMAT) or a pdfium.dll older than this binding
raise Exception.CreateFmt('PDF engine did not start: %s', [E.Message]);
end;
if Pdf.Active then
RenderFirstPage(Pdf);
finally
Pdf.Free;
end;
end;
מה מספר לך ייצוא PDFium חסר?
סוג הכשל השני הוא סטיית גרסאות. PDFium מספקת ייצואים חדשים לאורך זמן, וקישור ה-Pascal פותר כל פונקציה שהוא צריך באמצעות CheckGetProcAddress במהלך LoadLibrary. אם ייצוא נדרש חסר, הקישור חדש יותר מה-pdfium.dll שבדיסק, והאבחנה הכנה היא שה-DLL שנפרס מיושן ולא פגום. הפונקציה CheckGetProcAddress מצהירה בדיוק על כך: היא מעלה EPdfError המדווחת שה-pdfium.dll שנפרס ישן יותר מבנייה זו של קישור PDFiumPas, ומציינת את הנתיב DLLs/<subdir> שבו הקובץ הבינארי המתאים צריך להימצא
שני פרטים הופכים נתיב זה ליציב. ראשית, CheckGetProcAddress קוראת ל-UnloadLibrary לפני שהיא מעלה חריגה, כך שקישור חלקי לעולם אינו משאיר מצביעי פונקציות פתורים למחצה מאחור שהניסיון הבא ייתקל בהם. שנית, שם ה-DLL שהיא מדווחת עליו מגיע מ-BuildDllName, המחזירה pdfium.dll כרגיל ו-pdfium.v8.dll כאשר דגל EnableV8Engine הגלובלי מוגדר. אם האפליקציה שלך מאפשרת את מנוע ה-JavaScript V8 עבור טפסי XFA או טפסים עם סקריפטים, טקסט השגיאה מצביע על בניית ה-V8, ולא על הרגילה, כך שתחליף את הקובץ הנכון כבר בפעם הראשונה
האם בטוח לטעון את ה-DLL של PDFium מתהליכון רקע?
טעינת הספרייה בטוחה כעת מכל תהליכון; קריאה ל-API של PDFium ממספר תהליכונים עדיין לא בטוחה. אלו שתי הבטחות נפרדות, ושמירה על הפרדה ביניהן היא ההבדל בין מאגר עובדים יציב לבין קריסה לסירוגין. רינדור ברקע מריץ בדרך כלל כל רינדור עמוד דרך עובד TPdfFuture<T>, וה-future הראשון שנוגע ב-TPdf הוא זה שמפעיל את הקישור העצל. ללא הגנה, שני עובדים יכולים שניהם להבחין בספרייה לא טעונה, שניהם להריץ את רצף הקישור, לדרוס את module handle ולהדליף את הטעינה הראשונה
העבודה בגרסה v2.11.0 סוגרת חלון זה עם PDFiumLoadLock, TRTLCriticalSection גלובלי לתהליך העוטף את הכניסה הן ל-LoadLibrary והן ל-UnloadLibrary. הקטע הקריטי (critical section) הופך את רצף הבדיקה-ואז-טעינה לאטומי, כך שאף שני תהליכונים אינם יכולים לראות שניהם Loaded=False ולבצע קישור כפול, ואף תהליכון אינו יכול לשחרר את ה-DLL בזמן שאחר נמצא באמצע הקישור. הנעילה נוצרת בסעיף ה-initialization של היחידה ומפורקת ב-finalization, מוגנת על ידי דגל PDFiumLoadLockReady כך שההתאמה בטוחה אפילו במהלך כיבוי. אם אתה זקוק לתבנית המלאה של עובד-ומענה עם ביטול, המאמר הנלווה על רינדור רקע עם futures ניתנים לביטול מפרט זאת מקצה לקצה
uses
PDFium, FPdfAsync;
// Worker method runs on a background thread. The first future to bind
// pdfium.dll is serialised by PDFiumLoadLock, so a second concurrent
// worker cannot double-bind or leak the module handle.
function TReportForm.RenderThumbnail(
const AToken: IPdfCancellationToken): TBitmap;
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Active := True; // safe concurrent bind
AToken.ThrowIfCancelled;
Result := Pdf.Thumbnail; // this TPdf is owned by one thread only
finally
Pdf.Free;
end;
end;
procedure TReportForm.StartRender;
begin
TPdfFuture<TBitmap>.Run(RenderThumbnail, ThumbnailReady);
end;
procedure TReportForm.ThumbnailReady(
const AResult: TPdfFutureResult<TBitmap>);
begin
if AResult.IsSuccess then
Preview.Picture.Assign(AResult.Value);
end;
מה שנעילת הטעינה אינה מגנה עליו
הגבול כאן ראוי לציון ברור, מכיוון שקל לפרש את התיקון מעבר למה שהוא באמת. PDFiumLoadLock מסנכרן רק את מצב הקישור הגלובלי: ה-module handle והקצאות מצביעי הפונקציות FPDF_*. ה-API הבסיסי של PDFium ב-C עדיין אינו בטוח לשימוש בריבוי תהליכונים, בדיוק כפי שמצהירות כותרות המקור, ולכן קריאה לפונקציות FPDF_* או שיתוף של FPDF_DOCUMENT יחיד בין תהליכונים נשארים באחריותך לסנכרן. הצורה הבטוחה היא זו שלעיל: כל עובד מחזיק ב-TPdf משלו ולעולם אינו מוסר מסמך פעיל לתהליכון אחר. לדיון עמוק יותר בגבול זה ובכללי ה-ABI סביבו, ראה הקשחת קישור ה-VCL של PDFium מפני תקלות ABI ובטיחות זיכרון
מדוע ה-finalization ו-FPDF_DestroyLibrary חשובים
פער מתוחכם יותר שכן בנתיב הכיבוי. ליחידת PDFium לא היה סעיף finalization, מה שאמר ש-FPDF_DestroyLibrary מעולם לא רצה ביציאה מהתהליך; מערכת ההפעלה דרשה בחזרה את זיכרון ה-DLL ודילגה על הניקוי העצמי של PDFium, כלומר הצטרפות תהליכון העובד (worker-thread join), פינוי מבודד ה-V8 (V8 isolate disposal) כאשר EnableV8Engine מוגדר, ופירוק הגופנים והמטמון. בעומס עבודה רגיל של רינדור זוהי דליפה שקטה בפירוק, אך כאשר מנוע ה-V8 מאופשר, היא מדליפה JavaScript isolate בכל הרצה, מה שמופיע במהירות תחת אוטומציה חוזרת ונשנית
סעיף ה-finalization קורא כעת ל-UnloadLibrary, המפעיל את FPDF_DestroyLibrary ומשחרר את המודול. זה חי בטווח של יחידה (unit scope) מסיבה כלשהי: TPdf.Destroy רק מגדיר Active := False כדי לסגור את המסמך הנוכחי, והוא בכוונה אינו פורק את הספרייה הגלובלית, כך שאפליקציה מרובת מופעים שומרת על קישור PDFium משותף אחד פעיל לכל אורך חיי התהליך. הצבת הפירוק ב-finalization פירושה שהספרייה המשותפת משתחררת בדיוק פעם אחת, כאשר היחידה נפרקת, ו-UnloadLibrary בודק תחילה את דגל ה-Loaded שלו כך שהקריאה היא ללא פעולה (no-op) בלתי מזיקה אם מעולם לא נעשה שימוש ב-PDFium
רשימת בדיקה קצרה לפריסה
שלושה הרגלים מונעים כמעט כל כשל טעינה בשטח. התאם את ארכיטקטורת ה-DLL לתהליך המארח ושלח אותה תחת DLLs/Win32 או DLLs/Win64; שמור על pdfium.dll תואם לגרסת הקישור כדי שאף ייצוא נדרש לא יחסר; ולעולם אל תשתף TPdf או handle של מסמך בין תהליכונים, למרות שהקישור עצמו הוא כעת אטומי. אם אתה בונה את אותו קוד מקור הן עבור דלפי והן עבור לזרוס, הבדלי האריזה המכשילים פריסה חוצת-יעדים מכוסים בהערות על מלכודות מהדרים צולבים של דלפי ו-FPC. האבחון, נעילת הטעינה וניקוי ה-finalization המתוארים כאן מגיעים כולם ב-PDFium Component עבור דלפי ו-C++Builder