Τεχνικό Άρθρο

Προαιρετικά Exports PDFium: Πύλες Δυνατοτήτων σε Delphi

Το pdfium.dll σας φορτώνει κανονικά και μία διαδικασία εξακολουθεί να λείπει. Το PDFium Component το χειρίζεται διαχωρίζοντας τις συνδέσεις του σε δύο κλάσεις: υποχρεωτικά exports επιλυμένα μέσω της CheckGetProcAddress, που ματαιώνουν τη φόρτωση εντελώς, και προαιρετικά exports επιλυμένα μέσω της TryGetProcAddress, που αφήνουν πίσω έναν δείκτη nil και έναν έλεγχο δυνατότητας αντ' αυτού

Αυτό δεν είναι το ίδιο πρόβλημα με ένα DLL που δεν μπορεί να βρεθεί. Αν η εφαρμογή σας πεθαίνει με σφάλμα κακής μορφής EXE, λείπον αρχείο, ή αναντιστοιχία αρχιτεκτονικής, αυτή η ιστορία λέγεται στο συνοδευτικό άρθρο για την ανάπτυξη του pdfium.dll και τη διάγνωση αποτυχιών φόρτωσης. Εδώ ο loader πέτυχε. Ο χειριστής module είναι έγκυρος, εκατοντάδες exports επιλύθηκαν, και η εκτέλεση εξακολουθεί να τελειώνει πριν αποδοθεί η πρώτη σελίδα σας επειδή ένα σημείο εισόδου που έφτασε σε νεότερο build PDFium δεν υπάρχει στο δυαδικό στον δίσκο

Γιατί ένα λείπον export σπάει ολόκληρη τη βιβλιοθήκη;

Επειδή μια υποχρεωτική σύνδεση είναι σκληρό συμβόλαιο, και επιβάλλεται κατά τη διάρκεια μιας ενιαίας ακολουθίας σύνδεσης όλα-ή-τίποτα. Το PDFium Component επιλύει ολόκληρο τον πίνακα export του μέσα στη LoadLibrary, μία κλήση CheckGetProcAddress μετά την άλλη. Το πρώτο αποτέλεσμα nil προκαλεί EPdfError και καλεί την UnloadLibrary πριν από αυτό, που είναι σκόπιμο: μια μερική σύνδεση διαφορετικά θα άφηνε ήδη επιλυμένους δείκτες στραμμένους σε ένα module που πρόκειται να απελευθερωθεί, νικώντας ήσυχα κάθε φρουρά Assigned downstream

Η συνέπεια είναι η κατάσταση αποτυχίας που φέρνει κόσμο εδώ. Αναβαθμίζετε το component, στέλνετε το ίδιο pdfium.dll που στέλνετε εδώ και δύο χρόνια, και η εφαρμογή δεν θα ξεκινήσει. Το σφάλμα ονομάζει ένα export για ένα χαρακτηριστικό που ποτέ δεν καλέσατε. Τίποτα από αυτά που κάνετε στο σημείο κλήσης δεν βοηθά, επειδή το σημείο κλήσης ποτέ δεν τρέχει· η αποτυχία συνέβη κατά τη σύνδεση, πριν ανοιχτεί οποιοδήποτε έγγραφο

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 είναι απευθείας. Ένα export είναι υποχρεωτικό όταν η απουσία του καθιστά το component ανίκανο να κάνει τη δουλειά για την οποία υπάρχει, και προαιρετικό όταν η απουσία του απλώς αφαιρεί ένα χαρακτηριστικό-φύλλο. Τα FPDF_InitLibrary, FPDF_LoadDocument, FPDF_RenderPageBitmap, FPDF_ClosePage είναι υποχρεωτικά, και η αποτυχία με θόρυβο σε αυτά είναι σωστή: ένας viewer που δεν μπορεί να αποδώσει δεν είναι ένας υποβαθμισμένος viewer, είναι σπασμένος

Καθετί που προσεγγίζεται μέσω του ανεκτικού loader σήμερα είναι φύλλο. Η FPDFBookmark_GetColor έφτασε μετά το M109 και παρέχει μόνο τον προαιρετικό πίνακα χρώματος /C μιας καταχώρισης περιγράμματος, οπότε ένα DLL που προηγείται αναφέρει απλώς καμία χρωματική σελιδοδεικτοδότηση. Οι βοηθοί V8 FPDF_GetRecommendedV8Flags και FPDF_GetArrayBufferAllocatorSharedInstance, και οι βοηθοί string XFA FPDF_BStr_Init, FPDF_BStr_Set και FPDF_BStr_Clear, απουσιάζουν από κάθε build χωρίς V8 εκ κατασκευής, οπότε η μεταχείρισή τους ως υποχρεωτικά θα έκανε το απλό pdfium.dll αφόρτωτο. Και το ζεύγος που κίνησε αυτό το άρθρο: FPDFAttachment_SetDescription και FPDFAttachment_GetDescription, προστέθηκαν upstream στις 2026-07-13, αργότερα από την ημερομηνία build και των τεσσάρων δυαδικών PDFium που διανέμει το project κάτω από τα DLLs/Win32 και DLLs/Win64. Αυτή η τελευταία περίπτωση είναι το γενικό σχήμα του προβλήματος, όχι μία μεμονωμένη: ένα στρώμα σύνδεσης παρακολουθεί κεφαλίδες upstream, που κινούνται συνεχώς, ενώ το DLL στον installer σας κινείται σε διακριτά άλματα όποτε κάποιος το ξαναχτίζει. Υπάρχει πάντα ένα παράθυρο στο οποίο η πλευρά Pascal γνωρίζει exports που το αναπτυγμένο δυαδικό δεν έχει, και το να αποφασίζεται εκ των προτέρων ποια πλευρά της γραμμής υποχρεωτικού/προαιρετικού πέφτει κάθε νέο export είναι το μόνο πράγμα που κρατά αυτό το παράθυρο επιβιώσιμο

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 διαχωρίζει την ιδιότητα περιγραφής συνημμένου ακριβώς κατά μήκος αυτής της γραμμής, και ο διαχωρισμός είναι αυτό που σταματά ένα λείπον export από το να γίνει σιωπηλή απώλεια δεδομένων. Η TPdf.GetAttachmentDescription ελέγχει το Assigned(FPDFAttachment_GetDescription) και βγαίνει με κενό WString. Αυτό δεν είναι ψέμα: σε ένα DLL χωρίς το export, το component γνησίως δεν μπορεί να πει αν το συνημμένο φέρει καταχώριση /Desc, και μια κενή περιγραφή διαβάζεται με τον ίδιο τρόπο όπως ένα συνημμένο που ποτέ δεν είχε μία. Το υπόλοιπο του API συνημμένων, που καλύπτεται στο άρθρο για την εργασία με συνημμένα PDF σε Delphi, συνεχίζει να λειτουργεί ανέγγιχτο

Η TPdf.SetAttachmentDescription παίρνει την αντίθετη διαδρομή. Καλεί το Check στην ίδια δοκιμή Assigned και προκαλεί EPdfError με το κείμενο "Attachment descriptions are not supported by the loaded PDFium DLL". Η ήσυχη επιστροφή εδώ θα ήταν η χειρότερη διαθέσιμη επιλογή: ο καλών θα όριζε μια περιγραφή, δεν θα έπαιρνε σφάλμα, θα αποθήκευε το αρχείο, και θα διένειμε ένα PDF όπου η περιγραφή απλώς απουσιάζει. Κανείς δεν το προσέχει μέχρι έναν downstream καταναλωτή να ρωτήσει πού πήγε

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, που ακολουθούν το πανομοιότυπο μοτίβο για τις δικές τους προαιρετικές ομάδες. Η ονοματοδοσία της δοκιμής έχει μεγαλύτερη σημασία απ' ό,τι φαίνεται: ένα boolean που ονομάζεται AttachmentDescriptionFeaturesAvailable λέει στον επόμενο συντηρητή ότι αυτό το χαρακτηριστικό είναι υπό όρους στο αναπτυγμένο δυαδικό, κάτι που ένας γυμνός έλεγχος Assigned θαμμένος σε έναν property setter ποτέ δεν κάνει. Δίνει επίσης στο επίπεδο 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 έναντι μιας γραμμής βάσης upstream 2026-07-29 και βρήκε 470 εξαγόμενες συναρτήσεις C ABI. Η σύνδεση ήδη κάλυπτε 468 από αυτές. Κανείς δεν εντόπισε αυτό το κενό των δύο διαβάζοντας κεφαλίδες· ένα script το έκανε, σε ένα δευτερόλεπτο, και θα το ξανακάνει στην επόμενη αναβάθμιση upstream. Το tools/audit_pdfium_public_api.py είναι σκόπιμα μικρό: κάνει regex-match FPDF_EXPORT ... FPDF_CALLCONV name( σε κάθε κεφαλίδα στον δημόσιο κατάλογο, κάνει regex-match κάθε CheckGetProcAddress('Name') και TryGetProcAddress('Name') στο PDFium.pas, και τυπώνει τις δύο διαφορές συνόλων: missing για exports χωρίς σύνδεση, stale για συνδέσεις των οποίων το export δεν υπάρχει πια upstream. Βγαίνει με μη μηδενικό κωδικό όταν οποιοδήποτε σύνολο δεν είναι κενό, οπότε πέφτει σε ένα βήμα build χωρίς περαιτέρω τελετουργία. Το τρέχον αποτέλεσμα είναι 470 από 470 συνδεδεμένα, 0 λείποντα, 0 μπαγιάτικα

Η κατεύθυνση stale κερδίζει το ψωμί της εξίσου με τη missing. Ένα export που το upstream αφαιρεί αφήνει πίσω μια γραμμή CheckGetProcAddress που θα αποτυγχάνει σκληρά σε κάθε μελλοντική φόρτωση, και αυτό το είδος σαπίλας είναι αόρατο μέχρι τη μέρα που κάποιος ενημερώνει το DLL. Η χειροκίνητη επιθεώρηση βρίσκει τη συνάρτηση που σκεφτόσασταν· δεν βρίσκει αυτή που δεν σκεφτόσασταν. Σημειώστε επίσης ότι ο έλεγχος σκόπιμα μετρά και τους δύο loaders ως κάλυψη, που είναι η σωστή απόφαση για μετατόπιση API και ο λόγος που ο διαχωρισμός υποχρεωτικού/προαιρετικού πρέπει να είναι τεκμηριωμένη απόφαση και όχι υποπροϊόν του όποιου πρόσθεσε τη γραμμή

Πού η προαιρετική σύνδεση σταματά να είναι ειλικρινής

Δύο όρια αξίζει να δηλωθούν καθαρά, επειδή το μοτίβο είναι εύκολο να υπερ-εφαρμοστεί. Το πρώτο είναι ότι ένας δείκτης συνάρτησης nil είναι ασφαλής μόνο αν κυριολεκτικά κάθε διαδρομή που τον αγγίζει ελέγχει πρώτα Assigned. Σε μια μονάδα που δηλώνει εκατοντάδες μεταβλητές συνάρτησης cdecl, μία μη φρουρούμενη κλήση είναι παραβίαση πρόσβασης σε μια διεύθυνση που δεν σημαίνει τίποτα σε ένα stack trace. Η ίδια πειθαρχία που διέπει συμβάσεις κλήσης και διάρκειες ζωής κατά μήκος του ορίου C ισχύει εδώ, και είναι το θέμα του άρθρου για την ενίσχυση της σύνδεσης PDFium έναντι σφαλμάτων ABI και ασφάλειας μνήμης

Το δεύτερο όριο είναι το εύρος. Η προαιρετική σύνδεση δεν είναι γενική άδεια να κάνετε τα πάντα ανεκτικά. Αν η FPDF_RenderPageBitmap ήταν προαιρετική, το component θα φόρτωνε ευχάριστα και μετά θα απέτυχε σε κάθε σελίδα, μετατρέποντας ένα σαφές σφάλμα εκκίνησης σε σκόρπιες αποτυχίες χρόνου εκτέλεσης χωρίς προφανή αιτία. Το υποχρεωτικό είναι η σωστή προεπιλογή. Το προαιρετικό είναι η εξαίρεση στην οποία απλώνεστε όταν ένα χαρακτηριστικό είναι γνησίως φύλλο, όταν η απουσία έχει μια υπερασπίσιμη υποβαθμισμένη συμπεριφορά στην πλευρά ανάγνωσης, και όταν η πλευρά εγγραφής μπορεί να αρνηθεί με μήνυμα που ονομάζει τον λόγο

Ο σχεδιασμός loader, οι δοκιμές δυνατότητας και το εργαλείο ελέγχου που περιγράφονται εδώ διατίθενται ως μέρος του PDFium Component για Delphi και C++Builder· η σελίδα προϊόντος αναφέρει τα ενσωματωμένα δυαδικά PDFium και την πλήρη επιφάνεια API που εκθέτουν