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

Ανάπτυξη (Deployment) της PDFium DLL στο Delphi: Επίλυση Σφαλμάτων Φόρτωσης

Όταν η pdfium.dll αρνείται να φορτωθεί στο μηχάνημα ενός πελάτη, το PDFium Component για Delphi και Lazarus μετατρέπει το ασαφές εγγενές σφάλμα σε συγκεκριμένη αιτία. Η ρουτίνα CheckLoadLibrary ερμηνεύει το σφάλμα ERROR_BAD_EXE_FORMAT (193) ως αναντιστοιχία αρχιτεκτονικής 32/64-bit, και το CheckGetProcAddress αναφέρει μια εξαγωγή που λείπει ως pdfium.dll παλαιότερη από το binding. Και τα δύο μηνύματα υποδεικνύουν τη λύση αντί να σας αφήνουν να μαντεύετε

Αυτό έχει σημασία επειδή μια εγγενής DLL αποτυγχάνει με τρεις διαφορετικούς τρόπους, και το ακατέργαστο κείμενο του λειτουργικού συστήματος τους συγχέει όλους. Η αρχιτεκτονική μπορεί να είναι λάθος, το αναπτυγμένο δυαδικό αρχείο (binary) μπορεί να είναι πολύ παλιό για το Pascal binding που το καλεί, ή δύο νήματα μπορεί να ανταγωνίζονται (race) για να το δεσμεύσουν την ίδια στιγμή. Κάθε περίπτωση έχει διαφορετική λύση, και ο σκοπός των διαγνωστικών που προστέθηκαν στις εργασίες κύκλου ζωής φόρτωσης της έκδοσης v2.11.0 είναι να σας πουν ποιο από αυτά αντιμετωπίζετε στην πραγματικότητα

Γιατί η pdfium.dll αποτυγχάνει να φορτώσει με σφάλμα εσφαλμένης μορφής EXE;

Το σφάλμα των Windows 193, ERROR_BAD_EXE_FORMAT, σημαίνει ότι το αρχείο βρέθηκε και ανοίχτηκε αλλά ο τύπος μηχανής PE (PE machine type) δεν ταιριάζει με τη διεργασία υποδοχής (host process). Ένα εκτελέσιμο αρχείο 64-bit δεν μπορεί να φορτώσει μια pdfium.dll 32-bit, και ένα εκτελέσιμο 32-bit δεν μπορεί να φορτώσει μια 64-bit. Το αρχείο είναι παρόν, επομένως το ένστικτο να ψάξετε για μια DLL που λείπει σας στέλνει σε εντελώς λάθος κατεύθυνση

Το PDFium Component αναχαιτίζει αυτήν την περίπτωση στο CheckLoadLibrary: όταν η LoadLibrary επιστρέφει μια μηδενική λαβή (null handle) και η GetLastError είναι 193, προσαρτά μια υπόδειξη ότι η DLL βρέθηκε αλλά η αρχιτεκτονική της δεν ταιριάζει με τη διεργασία υποδοχής, και δείχνει προς την αντίστοιχη έκδοση DLLs/Win32 ή DLLs/Win64. Ο υποκατάλογος επιλέγεται από το BuildDllSubDir, το οποίο επιστρέφει Win64 όταν το IsWin64 είναι αληθές και Win32 διαφορετικά. Αναπτύξτε τη DLL κάτω από το δέντρο που αναζητά πραγματικά το binding και η αναντιστοιχία εξαφανίζεται

uses
  SysUtils, PDFium;

procedure OpenDocument(const AFileName: string);
var
  Pdf: TPdf;
begin
  // Αναπτύξτε την pdfium.dll δίπλα στο εκτελέσιμο, αντιστοιχισμένη με το στόχο build:
  //   <AppDir>\DLLs\Win32\pdfium.dll   για μια διεργασία υποδοχής 32-bit
  //   <AppDir>\DLLs\Win64\pdfium.dll   για μια διεργασία υποδοχής 64-bit
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := AFileName;
    try
      Pdf.Active := True;   // η πρώτη χρήση δεσμεύει αργοπορημένα (lazily binds) την pdfium.dll
    except
      on E: EPdfError do
        // Το μήνυμα ονομάζει ήδη την πραγματική αιτία: αναντιστοιχία 32/64-bit (ERROR_BAD_EXE_FORMAT) ή μια pdfium.dll παλαιότερη από αυτό το binding
        raise Exception.CreateFmt('PDF engine did not start: %s', [E.Message]);
    end;
    if Pdf.Active then
      RenderFirstPage(Pdf);
  finally
    Pdf.Free;
  end;
end;

Τι σας λέει μια εξαγωγή (export) PDFium που λείπει;

Η δεύτερη κατηγορία αποτυχίας είναι η απόκλιση έκδοσης (version skew). Το PDFium παρέχει νέες εξαγωγές με την πάροδο του χρόνου, και το Pascal binding επιλύει κάθε συνάρτηση που χρειάζεται μέσω του CheckGetProcAddress κατά τη διάρκεια της LoadLibrary. Εάν μια απαιτούμενη εξαγωγή απουσιάζει, το binding είναι νεότερο από την pdfium.dll στον δίσκο, και η ειλικρινής διάγνωση είναι ότι η αναπτυγμένη DLL είναι παλιά και όχι κατεστραμμένη. Η συνάρτηση CheckGetProcAddress αναφέρει ακριβώς αυτό: προκαλεί ένα EPdfError αναφέροντας ότι η αναπτυγμένη pdfium.dll είναι παλαιότερη από αυτήν την έκδοση του PDFiumPas binding, και ονομάζει τη διαδρομή DLLs/ όπου ανήκει το αντίστοιχο δυαδικό αρχείο

Δύο λεπτομέρειες καθιστούν αυτήν τη διαδρομή ισχυρή. Πρώτον, το CheckGetProcAddress καλεί την UnloadLibrary προτού εγείρει σφάλμα, οπότε μια μερική δέσμευση δεν αφήνει ποτέ ημι-επιλυμένους δείκτες συναρτήσεων (function pointers) για να σκοντάψει η επόμενη προσπάθεια. Δεύτερον, το όνομα της DLL που αναφέρει προέρχεται από το BuildDllName, το οποίο επιστρέφει pdfium.dll κανονικά και pdfium.v8.dll όταν έχει οριστεί η καθολική σημαία EnableV8Engine. Εάν η εφαρμογή σας ενεργοποιεί τη μηχανή V8 JavaScript για XFA ή σενάρια φορμών, το κείμενο του σφάλματος δείχνει προς την έκδοση V8 και όχι την απλή, ώστε να αντικαταστήσετε το σωστό αρχείο με την πρώτη φορά

Είναι η PDFium DLL ασφαλής για φόρτωση από ένα νήμα παρασκηνίου (background thread);

Η φόρτωση της βιβλιοθήκης είναι πλέον ασφαλής από οποιοδήποτε νήμα. Η κλήση του PDFium API από διάφορα νήματα εξακολουθεί να μην είναι. Αυτές είναι δύο ξεχωριστές εγγυήσεις, και η διατήρηση τους ξεχωριστά είναι η διαφορά μεταξύ μιας σταθερής δεξαμενής εργατών και μιας περιστασιακής κατάρρευσης. Η απόδοση στο παρασκήνιο συνήθως εκτελεί κάθε απόδοση σελίδας μέσω ενός εργάτη TPdfFuture<T>, και το πρώτο future που αγγίζει το TPdf είναι αυτό που ενεργοποιεί την καθυστερημένη δέσμευση (lazy bind). Χωρίς προστασία, δύο εργάτες θα μπορούσαν και οι δύο να παρατηρήσουν μια μη φορτωμένη βιβλιοθήκη, να εκτελέσουν και οι δύο τη σειρά δέσμευσης, να αντικαταστήσουν τη λαβή της μονάδας (module handle) και να προκαλέσουν διαρροή της πρώτης φόρτωσης

Οι εργασίες της έκδοσης v2.11.0 κλείνουν αυτό το παράθυρο με το PDFiumLoadLock, ένα καθολικό για τη διεργασία TRTLCriticalSection που περιβάλλει την είσοδο τόσο στη LoadLibrary όσο και στην UnloadLibrary. Το κρίσιμο τμήμα (critical section) καθιστά τη σειρά ελέγχου-φόρτωσης ατομική, οπότε δεν μπορούν δύο νήματα να δουν και τα δύο το Loaded=False και να κάνουν διπλή δέσμευση, και κανένα νήμα δεν μπορεί να ελευθερώσει τη DLL ενώ ένα άλλο βρίσκεται στη μέση της δέσμευσης. Το κλείδωμα δημιουργείται στο τμήμα initialization της μονάδας και καταστρέφεται στο finalization, προστατευμένο από μια σημαία PDFiumLoadLockReady ώστε η σύζευξη να είναι ασφαλής ακόμη και κατά τον τερματισμό λειτουργίας. Εάν χρειάζεστε το πλήρες μοτίβο εργάτη-και-απάντησης με ακύρωση, το συνοδευτικό άρθρο σχετικά με την απόδοση παρασκηνίου με ακυρώσιμα futures το εξηγεί από άκρη σε άκρη

uses
  PDFium, FPdfAsync;

// Η μέθοδος εργάτη εκτελείται σε νήμα παρασκηνίου. Το πρώτο future που δεσμεύει την pdfium.dll
// σειριοποιείται από το PDFiumLoadLock, οπότε ένας δεύτερος ταυτόχρονος
// εργάτης δεν μπορεί να κάνει διπλή δέσμευση ή να προκαλέσει διαρροή της λαβής της μονάδας.
function TReportForm.RenderThumbnail(
  const AToken: IPdfCancellationToken): TBitmap;
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Active := True;         // ασφαλής ταυτόχρονη δέσμευση
    AToken.ThrowIfCancelled;
    Result := Pdf.Thumbnail;    // αυτό το TPdf ανήκει σε ένα μόνο νήμα
  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;

Τι δεν προστατεύει το load lock

Το όριο εδώ αξίζει να αναφερθεί ξεκάθαρα, επειδή είναι εύκολο να παρερμηνευτεί η λύση. Το PDFiumLoadLock σειριοποιεί μόνο την καθολική κατάσταση δέσμευσης: τη λαβή της μονάδας και τις αντιστοιχίσεις δεικτών συναρτήσεων FPDF_*. Το υποκείμενο C API του PDFium εξακολουθεί να μην είναι thread-safe, ακριβώς όπως δηλώνουν οι κεφαλίδες του, οπότε η κλήση των συναρτήσεων FPDF_* ή η κοινή χρήση ενός FPDF_DOCUMENT σε διάφορα νήματα παραμένει δική σας ευθύνη σειριοποίησης. Το ασφαλές σχήμα είναι αυτό που περιγράφεται παραπάνω: κάθε εργάτης κατέχει το δικό του TPdf και δεν παραδίδει ποτέ ένα ζωντανό έγγραφο σε άλλο νήμα. Για μια βαθύτερη ανάλυση αυτού του ορίου και των κανόνων ABI γύρω από αυτό, δείτε το θωράκιση του PDFium VCL binding έναντι σφαλμάτων ABI και μνήμης

Γιατί το finalization και το FPDF_DestroyLibrary έχουν σημασία

Ένα πιο λεπτό κενό υπήρχε στη διαδρομή τερματισμού. Η μονάδα PDFium δεν είχε τμήμα finalization, πράγμα που σήμαινε ότι η FPDF_DestroyLibrary δεν εκτελούνταν ποτέ κατά την έξοδο της διεργασίας. Το λειτουργικό σύστημα ανακτούσε τη μνήμη της DLL και παρέλειπε τον καθαρισμό του ίδιου του PDFium, δηλαδή τη σύνδεση του νήματος εργάτη, την απόρριψη του V8 isolate όταν έχει οριστεί το EnableV8Engine, και την κατάργηση της γραμματοσειράς και της προσωρινής μνήμης. Σε έναν απλό φόρτο εργασίας απόδοσης αυτό αποτελεί μια σιωπηλή διαρροή κατά τον τερματισμό, αλλά με ενεργοποιημένη τη μηχανή V8, προκαλεί διαρροή ενός JavaScript isolate σε κάθε εκτέλεση, κάτι που εμφανίζεται γρήγορα υπό επαναλαμβανόμενη αυτοματοποίηση

Το τμήμα finalization καλεί τώρα την UnloadLibrary, η οποία καλεί την FPDF_DestroyLibrary και απελευθερώνει τη μονάδα. Αυτό ζει σε επίπεδο μονάδας για έναν λόγο: το TPdf.Destroy ορίζει μόνο το Active := False για να κλείσει το τρέχον έγγραφο, και σκόπιμα δεν ξεφορτώνει την καθολική βιβλιοθήκη, οπότε μια εφαρμογή πολλαπλών παρουσιών διατηρεί ένα κοινόχρηστο PDFium binding ζωντανό για όλη τη διάρκεια ζωής της διεργασίας. Η τοποθέτηση της κατάργησης στο finalization σημαίνει ότι η κοινόχρηστη βιβλιοθήκη απελευθερώνεται ακριβώς μία φορά, όταν ξεφορτώνεται η μονάδα, και η UnloadLibrary ελέγχει πρώτα τη σημαία Loaded, οπότε η κλήση είναι μια ακίνδυνη λειτουργία χωρίς αποτέλεσμα (no-op) εάν το PDFium δεν χρησιμοποιήθηκε ποτέ

Μια σύντομη λίστα ελέγχου ανάπτυξης (deployment checklist)

Τρεις συνήθειες αποτρέπουν σχεδόν κάθε αποτυχία φόρτωσης στην πράξη. Αντιστοιχίστε την αρχιτεκτονική της DLL με τη διεργασία υποδοχής και στείλτε την κάτω από το DLLs/Win32 ή το DLLs/Win64. Διατηρήστε την pdfium.dll σε συμφωνία με την έκδοση του binding, ώστε να μη λείπει καμία απαιτούμενη εξαγωγή. Και μην μοιράζεστε ποτέ ένα TPdf ή μια λαβή εγγράφου σε διάφορα νήματα, παρόλο που η ίδια η δέσμευση είναι πλέον ατομική. Εάν χτίζετε τον ίδιο κώδικα τόσο για Delphi όσο και για Lazarus, οι διαφορές συσκευασίας που δυσκολεύουν την ανάπτυξη σε διάφορους στόχους καλύπτονται στις σημειώσεις σχετικά με τις παγίδες cross-compiler Delphi και FPC. Τα διαγνωστικά, το load lock και ο καθαρισμός finalization που περιγράφονται εδώ παρέχονται όλα στο PDFium Component για Delphi και C++Builder