מאמר טכני

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

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

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

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

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

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

PDFium Component קושר את טבלת הייצוא שלו ב-Delphi במעבר אחד, שבו CheckGetProcAddress מבטל את הטעינה על ייצוא נדרש חסר בזמן ש-TryGetProcAddress מוריד בבטחה ייצוא אופציונלי
ייצוא חובה קושר הכול-או-לא-כלום ומבטל את הטעינה ב-nil הראשון, בזמן שייצוא אופציונלי משאיר מצביע nil מאחורי בדיקת Assigned של יכולת
function CheckGetProcAddress(const Name: string): Pointer;
begin
  Result := GetProcAddress(PDFiumLibrary, PChar(Name));
  if Result = nil then
  begin
    // ייצוא נדרש חסר משמעו שה-pdfium.dll הפרוס ישן יותר
    // מבנייה זו של הקישור. שחררו כל מצביע שנפתר עד כה
    // כך שאף קורא לא יוכל להגיע אל המודול שאנו עומדים לשחרר.
    UnloadLibrary;
    raise EPdfError.Create('Required PDFium export not found: ' + Name);
  end;
end;

function TryGetProcAddress(const Name: string): Pointer;
begin
  // ייצוא אופציונלי. nil היא תשובה לגיטימית כאן; כל קורא
  // נדרש לבדוק Assigned() לפני הפניית המשתנה.
  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');
// תיאורי צרופות נוספו אחרי רוויזיית ה-DLL הכלולה.
// השאירו אותם אופציונליים כדי שפריסות ישנות ימשיכו להיטען.
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 שבו התיאור פשוט נעדר. אף אחד לא שם לב עד שצרכן במורד-הזרם שואל לאן זה הלך

ייצוא תיאור קבצים מצורפים חסר ב-PDFium ב-Delphi מחזיר קריאה ריקה דרך TPdf.GetAttachmentDescription ומעלה חריגה בכתיבה, חסום על ידי AttachmentDescriptionFeaturesAvailable
צד הקריאה מתדרדר אל תשובה ריקה, צד הכתיבה זורק עם סיבה מנוסחת בשם, וprobe בעל שם מאפשר ל-UI לנטרל את התכונה מראש
function TPdf.GetAttachmentDescription(Index: Integer): WString;
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  Result := '';

  // צד הקריאה מתדרדר: DLL ישן לא יכול לדווח על /Desc, ו-'' אינו
  // ניתן להבחנה מצרופה שאינה נושאת תיאור כלל.
  if not Assigned(FPDFAttachment_GetDescription) then
    Exit;
  // ... מידוד חוצץ בשני מעברים מול FPDFAttachment_GetDescription ...
end;

procedure TPdf.SetAttachmentDescription(Index: Integer; const Value: WString);
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  // צד הכתיבה מסרב: השמטה שקטה של הערך הייתה מייצרת קובץ
  // שהקורא מאמין שנושא תיאור, אך אינו נושא.
  Check(Assigned(FPDFAttachment_SetDescription),
    'Attachment descriptions are not supported by the loaded PDFium DLL');
  // ... FPDFDoc_GetAttachment, ואז FPDFAttachment_SetDescription ...
end;

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

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

procedure TAttachmentFrame.SyncCapabilities;
begin
  // בדקו פעם אחת, בהקמת הטופס, במקום לגלות את המגבלה בזמן שמירה.
  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 המלא שהם חושפים