מאמר טכני

ייצואי PDFium אופציונליים: שערי יכולת בדלפי

ה-pdfium.dll שלך נטען בסדר ופרוצדורה אחת עדיין חסרה. PDFium Component מטפלת בזה על ידי פיצול הקישורים שלה לשתי מחלקות: ייצואים נדרשים שנפתרים דרך CheckGetProcAddress, שמפסיקים את הטעינה לגמרי, וייצואים אופציונליים שנפתרים דרך TryGetProcAddress, שמשאירים מצביע nil ובדיקת יכולת מאחוריהם במקום זאת

זו לא אותה בעיה כמו DLL שלא ניתן למצוא. אם היישום שלך מת עם שגיאת פורמט EXE שגוי, קובץ חסר, או אי-התאמת ארכיטקטורה, הסיפור ההוא מסופר בהמאמר המשלים על פריסת pdfium.dll ואבחון כשלי טעינה. כאן הטוען הצליח. מזהה המודול תקף, מאות ייצואים נפתרו, וההרצה עדיין מסתיימת לפני שהעמוד הראשון שלך מתרנדר כי נקודת כניסה אחת שהגיעה בבנייה חדשה יותר של PDFium אינה בבינארי שעל הדיסק

למה ייצוא חסר אחד שובר את כל הספרייה?

מפני שקישור נדרש הוא חוזה קשיח, והוא נאכף במהלך רצף קישור הכול-או-כלום יחיד. PDFium Component פותרת את כל טבלת הייצוא שלה בתוך LoadLibrary, קריאת CheckGetProcAddress אחת אחרי השנייה. התוצאה הראשונה של nil מעלה EPdfError וקוראת ל-UnloadLibrary לפני שהיא עושה זאת, וזה מכוון: קישור חלקי אחרת היה משאיר מצביעים שכבר נפתרו מכוונים לתוך מודול שעומד להשתחרר, ומבטל בשקט כל הגנת Assigned במורד הזרם

התוצאה היא מצב הכישלון שמביא אנשים לכאן. אתה משדרג את הרכיב, שולח את אותו pdfium.dll ששלחת במשך שנתיים, והיישום לא יתחיל. השגיאה שמת ייצוא עבור תכונה שמעולם לא קראת לה. שום דבר שאתה עושה בנקודת הקריאה לא עוזר, כי נקודת הקריאה אף פעם לא רצה; הכישלון קרה במהלך הקישור, לפני שמסמך כלשהו נפתח

function CheckGetProcAddress(const Name: string): Pointer;
begin
  Result := GetProcAddress(PDFiumLibrary, PChar(Name));
  if Result = nil then
  begin
    // A missing required export means the deployed pdfium.dll is older
    // than this build of the binding. Drop every pointer resolved so far
    // so no caller can reach into the module we are about to free.
    UnloadLibrary;
    raise EPdfError.Create('Required PDFium export not found: ' + Name);
  end;
end;

function TryGetProcAddress(const Name: string): Pointer;
begin
  // Optional export. nil is a legitimate answer here; every caller is
  // required to test Assigned() before dereferencing the variable.
  Result := GetProcAddress(PDFiumLibrary, PChar(Name));
end;

נדרש או אופציונלי: איפה הקו באמת יושב

הכלל ש-PDFium Component מיישמת בוטה. ייצוא נדרש כשהיעדרו הופך את הרכיב לחסר-יכולת לבצע את העבודה שהוא קיים בשבילה, ואופציונלי כשהיעדרו רק מסיר תכונת-עלה אחת. FPDF_InitLibrary, FPDF_LoadDocument, FPDF_RenderPageBitmap, FPDF_ClosePage נדרשים, וכישלון בקול-רם על אלה נכון: צופה שלא יכול לרנדר אינו צופה מתדרדר, הוא שבור

כל דבר שמגיע דרך הטוען הסבלני היום הוא עלה. FPDFBookmark_GetColor הגיע אחרי M109 ורק מספק את מערך הצבע האופציונלי /C של רשומת מתאר, כך ש-DLL שקודם לו פשוט מדווח על אין צבע-סימנייה. עוזרי ה-V8 FPDF_GetRecommendedV8Flags ו-FPDF_GetArrayBufferAllocatorSharedInstance, ועוזרי מחרוזת XFA FPDF_BStr_Init, FPDF_BStr_Set ו-FPDF_BStr_Clear, נעדרים מכל בנייה לא-V8 מעצם הבנייה, כך שהתייחסות אליהם כנדרשים הייתה הופכת את ה-pdfium.dll הרגיל לבלתי-ניתן-לטעינה. והזוג שהניע את המאמר הזה: FPDFAttachment_SetDescription ו-FPDFAttachment_GetDescription, שנוספו במעלה-הזרם ב-2026-07-13, מאוחר יותר מתאריך הבנייה של כל ארבעת בינארי ה-PDFium שהפרויקט שולח תחת DLLs/Win32 ו-DLLs/Win64. המקרה האחרון הזה הוא הצורה הכללית של הבעיה, לא מקרה חד-פעמי: שכבת קישור עוקבת אחרי כותרות במעלה-הזרם, שזזות ברציפות, בעוד ה-DLL במתקין שלך זז בקפיצות בדידות בכל פעם שמישהו בונה אותו מחדש. תמיד יש חלון שבו הצד ה-Pascal יודע על ייצואים שלבינארי הפרוס אין, והחלטה מראש על איזה צד של הקו נדרש/אופציונלי כל ייצוא חדש נופל היא הדבר היחיד ששומר על החלון הזה בר-הישרדות

FPDFDoc_GetAttachmentCount    := CheckGetProcAddress('FPDFDoc_GetAttachmentCount');
FPDFDoc_AddAttachment         := CheckGetProcAddress('FPDFDoc_AddAttachment');
FPDFAttachment_GetName        := CheckGetProcAddress('FPDFAttachment_GetName');
FPDFAttachment_GetStringValue := CheckGetProcAddress('FPDFAttachment_GetStringValue');
// Attachment descriptions were added after the bundled DLL revision.
// Keep them optional so older deployments continue to load.
FPDFAttachment_SetDescription := TryGetProcAddress('FPDFAttachment_SetDescription');
FPDFAttachment_GetDescription := TryGetProcAddress('FPDFAttachment_GetDescription');
FPDFAttachment_SetFile        := CheckGetProcAddress('FPDFAttachment_SetFile');
FPDFAttachment_GetFile        := CheckGetProcAddress('FPDFAttachment_GetFile');

מה שער יכולת צריך לעשות בנקודת הקריאה?

הוא צריך להיות אסימטרי, וזו האסימטריה היא כל העיצוב. לקריאה שלא יכולה לרוץ יש תשובה ריקה כנה. לכתיבה שלא יכולה לרוץ אין שום תשובה כנה כלל, אז היא חייבת להעלות חריגה. PDFium Component מפצלת את תכונת תיאור-הקובץ-המצורף בדיוק לאורך הקו הזה, והפיצול הוא מה שעוצר ייצוא חסר מלהפוך לאובדן נתונים שקט. TPdf.GetAttachmentDescription בודקת Assigned(FPDFAttachment_GetDescription) ויוצאת עם WString ריק. זה לא שקר: על DLL בלי הייצוא, הרכיב באמת לא יכול לומר האם הקובץ המצורף נושא רשומת /Desc, ותיאור ריק נקרא באותה דרך כמו קובץ מצורף שמעולם לא היה לו. שאר ה-API של קבצים מצורפים, מכוסה בהמאמר על עבודה עם קבצים מצורפים ל-PDF בדלפי, ממשיך לעבוד ללא-שינוי

TPdf.SetAttachmentDescription לוקחת את המסלול ההפוך. היא קוראת ל-Check על אותה בדיקת Assigned ומעלה EPdfError עם הטקסט "Attachment descriptions are not supported by the loaded PDFium DLL". חזרה בשקט כאן הייתה האפשרות הגרועה ביותר הזמינה: הקורא היה קובע תיאור, לא מקבל שגיאה, שומר את הקובץ, ומשלח PDF שבו התיאור פשוט נעדר. אף אחד לא שם לב עד שצרכן במורד-הזרם שואל לאן זה הלך

function TPdf.GetAttachmentDescription(Index: Integer): WString;
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  Result := '';

  // Read side degrades: an old DLL cannot report /Desc, and '' is
  // indistinguishable from an attachment that carries no description.
  if not Assigned(FPDFAttachment_GetDescription) then
    Exit;
  // ... two-pass buffer sizing against FPDFAttachment_GetDescription ...
end;

procedure TPdf.SetAttachmentDescription(Index: Integer; const Value: WString);
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  // Write side refuses: silently dropping the value would produce a file
  // the caller believes carries a description and does not.
  Check(Assigned(FPDFAttachment_SetDescription),
    'Attachment descriptions are not supported by the loaded PDFium DLL');
  // ... FPDFDoc_GetAttachment, then FPDFAttachment_SetDescription ...
end;

בדיקת היכולת לפני שאתה מציע את התכונה

לכידת חריגה היא דרך גרועה לגלות מה הפריסה שלך יכולה לעשות, כך ש-PDFium Component חושפת את אותה בדיקה כפונקציה בעלת-שם. AttachmentDescriptionFeaturesAvailable קוראת ל-LoadLibrary ומחזירה האם שני חצאי הזוג נפתרו. היא יושבת לצד V8FeaturesAvailable, XfaBStrHelpersAvailable ו-XfaFeaturesAvailable, שעוקבות אחרי אותה תבנית זהה עבור הקבוצות האופציונליות שלהן עצמן. שמת הבדיקה חשוב יותר ממה שזה נראה: בוליאני שנקרא AttachmentDescriptionFeaturesAvailable אומר למתחזק הבא שהתכונה הזו מותנית בבינארי הפרוס, מה שבדיקת Assigned חשופה קבורה בקובע-תכונה אף פעם לא עושה. זה גם נותן לשכבת ה-UI משהו להתחבר אליו, כך שתיבת עריכת התיאור מושבתת מראש במקום לקבל קלט ולדחות אותו בשמירה

procedure TAttachmentFrame.SyncCapabilities;
begin
  // Ask once, at form setup, instead of discovering the limit on save.
  DescriptionEdit.Enabled := AttachmentDescriptionFeaturesAvailable;
  if not DescriptionEdit.Enabled then
    DescriptionEdit.TextHint := 'Requires a newer pdfium.dll';
end;

procedure TAttachmentFrame.SaveDescription(Pdf: TPdf; Index: Integer);
begin
  if not AttachmentDescriptionFeaturesAvailable then
    Exit;
  Pdf.AttachmentDescription[Index] := DescriptionEdit.Text;
end;

למה כיסוי הקישור חייב להיות מוכח על ידי כלי?

מפני שהמספרים הם מעבר לנקודה שבה אפשר לסמוך על בן-אדם איתם. PDFium Component ביקרה 21 כותרות PDFium ציבוריות כנגד קו-בסיס במעלה-הזרם מ-2026-07-29 ומצאה 470 פונקציות ABI מיוצאות ב-C. הקישור כבר כיסה 468 מהן. אף אחד לא איתר את הפער הזה של שתיים על ידי קריאת כותרות; סקריפט עשה, בשנייה אחת, והוא יעשה זאת שוב בקפיצה הבאה במעלה-הזרם. tools/audit_pdfium_public_api.py קטן בכוונה: הוא regex-מתאים FPDF_EXPORT ... FPDF_CALLCONV name( על פני כל כותרת בספרייה הציבורית, regex-מתאים כל CheckGetProcAddress('Name') ו-TryGetProcAddress('Name') ב-PDFium.pas, ומדפיס את שני הבדלי הסטים: missing עבור ייצואים ללא קישור, stale עבור קישורים שהייצוא שלהם כבר לא קיים במעלה-הזרם. הוא יוצא לא-אפס כשאחד הסטים לא-ריק, כך שהוא נכנס לשלב בנייה ללא טקס נוסף. התוצאה הנוכחית היא 470 מתוך 470 מקושרים, missing 0, stale 0

הכיוון stale מרוויח את מקומו באותה מידה כמו missing. ייצוא שמעלה-הזרם מסיר משאיר שורת CheckGetProcAddress מאחור שתיכשל-קשיחות בכל טעינה עתידית, וסוג הרקב הזה בלתי-נראה עד היום שבו מישהו מעדכן את ה-DLL. סקירה ידנית מוצאת את הפונקציה שחשבת עליה; היא לא מוצאת את זו שלא חשבת עליה. שים לב גם שהביקורת סופרת בכוונה את שני הטוענים ככיסוי, וזו הקריאה הנכונה עבור סחיפת API, והסיבה שהפיצול נדרש/אופציונלי חייב להיות החלטה מתועדת ולא תוצר-לוואי של מי שהוסיף את השורה

איפה קישור אופציונלי מפסיק להיות כן

שני גבולות ראויים לציון ברור, כי התבנית קלה ליישום-יתר. הראשון הוא שמצביע פונקציה nil בטוח רק אם ממש כל נתיב שנוגע בו בודק Assigned קודם. ביחידה שמצהירה על מאות משתני פונקציית cdecl, קריאה בלתי-מוגנת אחת היא access violation בכתובת שלא אומרת כלום ב-stack trace. אותה משמעת ששולטת במוסכמות קריאה וחיי-מחזור על פני גבול ה-C חלה כאן, וזה הנושא של המאמר על חיזוק הקישור של PDFium כנגד תקלות ABI ובטיחות-זיכרון

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

עיצוב הטוען, בדיקות היכולת וכלי הביקורת המתוארים כאן משתלבים כחלק מ-PDFium Component עבור דלפי ו-C++Builder; עמוד המוצר מפרט את בינארי PDFium המצורפים ואת משטח ה-API המלא שהם חושפים