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

Συνδέσεις PDF Library for Delphi DLL, ActiveX και dylib: Κλήση μίας μηχανής PDF από οποιαδήποτε γλώσσα

Να ένα πρόβλημα που εμφανίζεται τη στιγμή που μια βιβλιοθήκη PDF βγαίνει από τη μητρική της γλώσσα. Έχετε ένα binding που δουλεύει άψογα από C# σε Windows. Χρειάζεστε τις ίδιες κλήσεις από Python σε macOS, οπότε αντιγράφετε το αρχείο δηλώσεων των Windows, αλλάζετε το όνομα του binary και το τρέχετε. Κάθε σύμβολο επιλύεται. Η πρώτη κλήση επιστρέφει σκουπίδια, η δεύτερη καταρρέει με access violation, και ο κώδικας PDF σας δεν έχει αλλάξει σε τίποτα. Το σφάλμα βρίσκεται ένα επίπεδο κάτω από το PDF: τα exports των Windows χρησιμοποιούν τη σύμβαση Stdcall, το dylib του macOS εξάγει τις ίδιες συναρτήσεις ως Cdecl με προπορευόμενη κάτω παύλα, και μια δήλωση foreign-function που αστοχεί σε οποιαδήποτε από τις δύο λεπτομέρειες καταστρέφει τη στοίβα προτού καν ανοίξει ένα έγγραφο

Ολόκληρη αυτή η κατηγορία αστοχιών προκύπτει από μία σχεδιαστική απόφαση που αξίζει να κατανοήσετε εξαρχής. Το PDF Library for Delphi, η μηχανή PDF της losLab με διαθέσιμο πηγαίο κώδικα για Delphi και C++Builder, τυλίγει ολόκληρο το object model της σε μία flat facade class, την TPDFlib, και ύστερα διαθέτει αυτή τη facade σε τρεις δυαδικές μορφές: ένα Windows DLL με περίπου 1.250 εξαγόμενες συναρτήσεις, ένα αντικείμενο αυτοματισμού COM/ActiveX και ένα dylib για macOS. Η σημασιολογία του PDF είναι πανομοιότυπη και στις τρεις. Αυτό που σας δαγκώνει ζει στο ABI από κάτω: συμβάσεις κλήσης, κωδικοποιήσεις συμβολοσειρών, ιδιοκτησία των handle και το ποια πλευρά επιτρέπεται να απελευθερώσει ποιον buffer

Μία facade, τρεις δυαδικές μορφές

Κάθε δημόσια συνάρτηση του TPDFlib έχει ένα flat αντίστοιχο με όνομα DL συν το όνομα της μεθόδου. Το LoadFromFile γίνεται DLLoadFromFile, το Encrypt γίνεται DLEncrypt, το NewSignProcessFromFile γίνεται DLNewSignProcessFromFile. Η πρώτη παράμετρος σχεδόν κάθε export είναι ένα InstanceID που επιστρέφει η DLCreateLibrary, στη θέση της αναφοράς αντικειμένου που θα κρατούσε αλλιώς ένας καλών από Delphi. Αφομοιώστε νωρίς αυτή την αντιστοίχιση. Σημαίνει ότι η αναφορά API της Delphi λειτουργεί ταυτόχρονα ως τεκμηρίωση για κάθε άλλη γλώσσα: ό,τι μπορεί να κάνει η κλάση μπορεί να το κάνει και το DLL κάτω από ένα προβλέψιμο όνομα, και μπορείτε να διαβάσετε μια υπογραφή μεθόδου Pascal για να μάθετε την κλήση που χρειάζεστε από Python ή C#

Το build για Windows παράγει τα PDFlibDLL32.dll και PDFlibDLL64.dll· επιλέξτε αυτό που ταιριάζει με το bitness της διεργασίας-ξενιστή, αφού μια διεργασία Java ή .NET 64 bit δεν μπορεί να φορτώσει τη βιβλιοθήκη 32 bit όσο σωστή κι αν δείχνει η δήλωση

Διάγραμμα αρχιτεκτονικής μιας πρόσοψης TPDFlib που εκτίθεται ως Windows DLL με Stdcall, ως αντικείμενο αυτοματισμού ActiveX με Safecall και ως macOS dylib με Cdecl
Και τα τρία δυαδικά μοιράζονται μία επίπεδη πρόσοψη PDF αλλά διαφέρουν στη σύμβαση κλήσης, στον χειρισμό συμβολοσειρών και στις απαιτήσεις καταχώρησης

Windows: instances με Stdcall και τα ζεύγη συναρτήσεων W/A

Κάθε export που δέχεται συμβολοσειρά υπάρχει δύο φορές. Μια wide έκδοση δέχεται PWideChar (UTF-16, η φυσική επιλογή για .NET, Java και το c_wchar_p της Python) και μια έκδοση με κατάληξη A δέχεται PAnsiChar. Οι δύο φέρουν πανομοιότυπη σημασιολογία και διαφέρουν μόνο στην κωδικοποίηση, κάτι που είναι ακριβώς ο λόγος που η ανάμειξή τους εντοπίζεται τόσο δύσκολα: τίποτα δεν πετάει εξαίρεση, τίποτα δεν επιστρέφει κωδικό σφάλματος, απλώς παίρνετε κατεστραμμένους χαρακτήρες στα metadata ή ένα ανυπόστατο «file not found» για κάθε διαδρομή με χαρακτήρα εκτός απλού ASCII. Το πρώτο σφάλμα κωδικοποίησης που συναντά μια ομάδα με αυτόν τον τρόπο συνήθως κοστίζει ένα απόγευμα, γιατί το σύμπτωμα δείχνει προς τα δεδομένα ενώ η αιτία βρίσκεται στη δήλωση

// Binding για Windows (PDFlibDLL64.dll): Stdcall, απλά ονόματα export
function DLCreateLibrary: Integer; stdcall;
  external 'PDFlibDLL64.dll' name 'DLCreateLibrary';
function DLReleaseLibrary(InstanceID: Integer): Integer; stdcall;
  external 'PDFlibDLL64.dll' name 'DLReleaseLibrary';
function DLLoadFromFile(InstanceID: Integer;
  FileName, Password: PWideChar): Integer; stdcall;
  external 'PDFlibDLL64.dll' name 'DLLoadFromFile';

// Binding για macOS: ίδια συνάρτηση, Cdecl, και πρόθεμα κάτω παύλας στο export
function DLCreateLibrary: Integer; cdecl;
  external 'PDFlibDylib.dylib' name '_DLCreateLibrary';

Επιλέξτε ένα πλάτος χαρακτήρα ανά ξενιστή και κωδικοποιήστε το στον generator του binding. Ένας πρακτικός κανόνας: αν η γλώσσα-ξενιστής έχει εγγενείς συμβολοσειρές UTF-16, δέστε παντού τις εκδόσεις W και μην ξαναγγίξετε ποτέ την οικογένεια A

macOS: ίδια ονόματα, διαφορετικό ABI

Το dylib εξάγει το ίδιο σύνολο συναρτήσεων DL με δύο συστηματικές αλλαγές. Η σύμβαση κλήσης είναι Cdecl αντί για Stdcall, και κάθε όνομα export φέρει προπορευόμενη κάτω παύλα (_DLCreateLibrary, _DLLoadFromFile και ούτω καθεξής). Και οι δύο αλλαγές είναι καθαρά μηχανικές, κάτι που τις κάνει ιδανικές για ένα παραγόμενο binding και επικίνδυνες για ένα χειροκίνητα επεξεργασμένο αντίγραφο του αρχείου των Windows. Κρατήστε μία κανονική λίστα συναρτήσεων και εκπέμψτε από αυτήν δηλώσεις ανά πλατφόρμα, αν το εργαλείο σας το επιτρέπει. Παραλείψτε το και παίρνετε ακριβώς την καταστροφή στοίβας που περιγράφεται στην αρχή αυτής της σελίδας, η οποία αναπαράγεται μόνο στην πλατφόρμα που το CI σας τυχαίνει να δοκιμάζει λιγότερο

Ξενιστές COM και ActiveX: Safecall και φορτία Olevariant

Για VB.NET, C#, VBScript και παλαιούς ξενιστές αυτοματισμού, το build OCX τυλίγει την ίδια facade σε ένα αντικείμενο αυτοματισμού IDispatch, το IPDFlibrary, με κάθε μέθοδο δηλωμένη ως Safecall. Αυτή η σύμβαση αλλάζει τον τρόπο με τον οποίο σας φτάνουν τα σφάλματα. Το Safecall μεταφράζει μια εσωτερική αστοχία σε HRESULT του COM, οπότε ένας καλών από C# πιάνει εξαίρεση εκεί που το flat DLL θα είχε επιστρέψει έναν σιωπηλό ακέραιο τον οποίο ο καλών όφειλε να θυμηθεί να ελέγξει. Η ίδια λειτουργία, δύο ιδιώματα αστοχίας, ανάλογα με το ποιο binary φορτώσατε

Τα δυαδικά δεδομένα ακολουθούν έναν δεύτερο κανόνα ειδικό για το COM. Η διεπαφή αυτοματισμού δεν έχει καθόλου παραμέτρους δείκτη. Οτιδήποτε δυαδικό, bytes εικόνας που μπαίνουν ή bytes PDF που βγαίνουν, περνά το όριο ως Olevariant μέσω μεθόδων όπως οι AddImageFromVariant και AppendToVariant. Το marshaling ενός πίνακα bytes σε variant είναι μία γραμμή στο .NET. Δοκιμάστε αντ' αυτού να του δώσετε έναν ακατέργαστο δείκτη, με το σκεπτικό ότι έτσι κι αλλιώς πρόκειται για την ίδια διεργασία, και το επίπεδο dispatch θα απορρίψει ή θα παραμορφώσει την κλήση. Άλλη μία λεπτομέρεια καταχώρισης σκοντάφτει στα deployments: η καταχώριση COM γίνεται ανά bitness, οπότε ένα OCX καταχωρισμένο με τον regsvr32 των 32 bit είναι αόρατο σε έναν ξενιστή 64 bit. Αυτή η αναντιστοιχία εμφανίζεται ως το περιβόητα άχρηστο «class not registered» στο μηχάνημα του πελάτη, πολύ αφότου έφυγε από το δικό σας

Πειθαρχία στα handle: τα instances κατέχουν τα έγγραφα

Το flat API τρέχει πάνω σε ακέραια handles. Η DLCreateLibrary επιστρέφει ένα instance. Η φόρτωση ενός αρχείου επιστρέφει ένα document ID μέσα σε εκείνο το instance. Οι διεργασίες υπογραφής, οι λίστες συμβολοσειρών και τα αρχεία άμεσης πρόσβασης επιστρέφουν το καθένα τα δικά του ακέραια handles, όλα εντός της εμβέλειας του ίδιου instance. Ο κύκλος ζωής φαίνεται ίδιος από οποιονδήποτε ξενιστή FFI, και εδώ παρουσιάζεται σε Pascal επειδή διαβάζεται καθαρά:

var
  Inst, Doc: Integer;
begin
  Inst := DLCreateLibrary;                       // ένα instance ανά worker thread
  try
    Doc := DLLoadFromFile(Inst, 'in.pdf', '');   // επιστρέφει DocumentID, 0 σε αποτυχία
    if Doc <> 0 then
    begin
      DLEncrypt(Inst, 'owner-secret', 'user-secret', 3,
        DLEncodePermissions(Inst, 1, 0, 0, 0, 0, 0, 0, 1));
      DLSaveToFile(Inst, 'out.pdf');
    end;
  finally
    DLReleaseLibrary(Inst);                      // απελευθερώνει κάθε έγγραφο του instance
  end;
end;

Δύο πράγματα προκύπτουν από αυτό το δέντρο ιδιοκτησίας. Η DLReleaseLibrary είναι η μόνη κλήση καθαρισμού που χρειάζεστε αυστηρά, αφού γκρεμίζει με μια κίνηση κάθε έγγραφο και κάθε handle διεργασίας κάτω από το instance. Σε ένα σύντομο script αυτό αρκεί. Σε μια μακρόβια υπηρεσία γίνεται αργή διαρροή με επιπλέον τελετουργικό, οπότε απελευθερώνετε τα έγγραφα καθώς τελειώνετε μαζί τους αντί να τα αφήνετε να στοιβάζονται μέχρι να πεθάνει το instance. Το instance είναι επίσης η φυσική μονάδα απομόνωσης νημάτων. Δώστε σε κάθε worker thread το δικό του InstanceID και μην μοιράζεστε ποτέ ένα ανάμεσα σε νήματα χωρίς εξωτερικό κλείδωμα, για τον ίδιο λόγο που δεν θα μοιραζόσασταν ποτέ ένα μοναδικό αντικείμενο TPDFlib ανάμεσα σε νήματα

Οι επιστρεφόμενες συμβολοσειρές είναι δανεικές, όχι δικές σας

Οι συναρτήσεις που επιστρέφουν κείμενο, όπως η DLGetPageText, δίνουν πίσω έναν PWideChar ή PAnsiChar που δείχνει μέσα σε buffer τον οποίο κατέχει και ανακυκλώνει το instance της βιβλιοθήκης. Το συμβόλαιο είναι: αντιγράψτε αμέσως, μην απελευθερώσετε ποτέ

Χρονογραμμή του PDF Library for Delphi που αντιπαραθέτει την άμεση αντιγραφή δανεικού δείκτη DLGetPageText με τη διατήρησή του μέχρι η βιβλιοθήκη να ανακυκλώσει το υποκείμενο buffer
Οι επιστρεφόμενοι δείκτες char δανείζονται αποθήκευση που το στιγμιότυπο ανακυκλώνει, οπότε η αντιγραφή πρέπει να πραγματοποιηθεί πριν την επόμενη κλήση της βιβλιοθήκης
var
  P: PWideChar;
  PageText: string;
begin
  P := DLGetPageText(Inst, 7);   // δείκτης σε buffer που ανήκει στη βιβλιοθήκη
  PageText := P;                 // αντιγραφή τώρα· επόμενη κλήση ίσως τον ξαναχρησιμοποιήσει
end;

Στη C# αυτό σημαίνει marshaling του IntPtr σε managed string πριν από την επόμενη κλήση της βιβλιοθήκης. Στο ctypes της Python σημαίνει να αποσπάσετε αμέσως τη wide συμβολοσειρά από τον δείκτη. Κρατήστε τον ακατέργαστο δείκτη ανάμεσα σε κλήσεις και έχετε γράψει ένα σφάλμα που περνά κάθε unit test και μετά αποτυγχάνει την πρώτη φορά που δύο αιτήματα επικαλύπτονται στην παραγωγή, επειδή η δεύτερη κλήση ανακύκλωσε τον buffer που η πρώτη διάβαζε ακόμη. Ο ίδιος κανόνας ιδιοκτησίας ισχύει και προς την αντίθετη κατεύθυνση για τα callbacks που καταχωρίζονται μέσω DLSetProgressCallback. Κάθε δείκτης που η βιβλιοθήκη παραδίδει στο callback σας είναι έγκυρος μόνο για το σώμα εκείνου του callback, και το ίδιο το αντικείμενο callback πρέπει να παραμείνει ζωντανό (καρφιτσωμένο, σε ξενιστή με garbage collection) για όσο το instance ενδέχεται ακόμη να το καλέσει. Ένα delegate που συλλέχθηκε στη μέση μιας εργασίας είναι η κλασική πηγή του «τυχαίου» access violation που εμφανίζεται σε ένα binding .NET το οποίο έτρεχε καθαρά επί μήνες

Ενσωματώστε ένα smoke test μέσα στο ίδιο το binding και τρέξτε το προτού κυκλοφορήσει οποιοδήποτε παραγόμενο σύνολο δηλώσεων. Δοκιμάστε μία κλήση από κάθε κατηγορία που τείνει να αποκαλύπτει λάθη ABI: μια συνάρτηση χωρίς παραμέτρους όπως η DLCreateLibrary για να αποδείξετε ότι η σύμβαση είναι σωστή, μια συνάρτηση με συμβολοσειρά εισόδου τροφοδοτημένη με διαδρομή που περιέχει μη ASCII χαρακτήρες για να αποδείξετε ότι η κωδικοποίηση είναι σωστή, μια συνάρτηση με συμβολοσειρά εξόδου για να αποδείξετε ότι ο χειρισμός του δανεικού buffer είναι σωστός, και μια λειτουργία που αποτυγχάνει επίτηδες ώστε να δείτε πώς φτάνει ένα σφάλμα στον ξενιστή σας. Είναι δεκαπέντε λεπτά δουλειάς, και πιάνει τις αστοχίες σύμβασης κλήσης και κωδικοποίησης που αλλιώς θα έφταναν μήνες αργότερα ως crash dump πελάτη

Πλέγμα 2x2 του PDF Library for Delphi με δοκιμές καπνού binding που καλύπτουν σύμβαση κλήσης, κωδικοποίηση συμβολοσειρών, δανεικά buffers και την εμφάνιση αποτυχιών
Τέσσερις οικονομικοί έλεγχοι εντοπίζουν σφάλματα σύμβασης, κωδικοποίησης και ιδιοκτησίας πριν οι παραγόμενες δηλώσεις φτάσουν ποτέ σε μηχάνημα πελάτη

Η περίπτωση του ctypes της Python, συγκεκριμένα

Το ctypes της Python είναι το binding που βλέπω να γράφεται στο χέρι πιο συχνά, και κάνει εύκολη την επίδειξη του διαχωρισμού ανάμεσα στις πλατφόρμες. Στα Windows, φορτώστε τη βιβλιοθήκη με ctypes.WinDLL ώστε το ctypes να εφαρμόσει Stdcall, δέστε τις συναρτήσεις W χωρίς κατάληξη και δηλώστε κάθε παράμετρο συμβολοσειράς ως c_wchar_p. Στο macOS, φορτώστε τη με ctypes.CDLL για Cdecl, κρατήστε την ίδια λίστα συναρτήσεων και επιλύστε τα ονόματα χωρίς την προπορευόμενη κάτω παύλα. Τα περισσότερα επίπεδα FFI, μαζί και το ctypes, επαναφέρουν τη σύμβαση της κάτω παύλας για λογαριασμό σας στο macOS, αλλά αυτή είναι η μία υπόθεση που πρέπει να επιβεβαιώσετε με μία και μόνο επιλυμένη κλήση προτού παραγάγετε εκατοντάδες δηλώσεις πάνω της

Δύο ερωτήματα ανάπτυξης ακολουθούν τη δουλειά του binding και έχουν καθαρές απαντήσεις. Το απλό DLL δεν χρειάζεται καταχώριση: ο regsvr32 αφορά μόνο το build ActiveX, ενώ το DLL διανέμεται με απλή αντιγραφή αρχείου, που είναι και ο βασικός λόγος να το προτιμήσετε για υπηρεσίες Windows και containers όπου δεν θέλετε να αγγίξετε καθόλου το μητρώο. Η ασφάλεια νημάτων ανάγεται στον κανόνα που ήδη ισχύει παραπάνω, ένα instance ανά νήμα. Το handle του instance κρατά κάθε κομμάτι μεταβλητής κατάστασης που παρακολουθεί η μηχανή, το επιλεγμένο έγγραφο, τις επιλογές απόδοσης, τις ρυθμίσεις εξαγωγής, οπότε δύο νήματα που μοιράζονται ένα instance μπερδεύουν το ένα την κατάσταση του άλλου ακόμη κι όταν κάθε μεμονωμένη κλήση επιστρέφει επιτυχία

Μόλις ένα binding σταθεροποιηθεί, οι λειτουργίες στην άλλη πλευρά του είναι ακριβώς εκείνες που καλύπτουν σε βάθος τα άρθρα για Delphi, όπως η εφαρμογή και ο έλεγχος κρυπτογράφησης PDF και η εξαγωγή κειμένου και εικόνων από υπάρχοντα έγγραφα

Οι δυαδικές λήψεις και για τα τρία επίπεδα ενσωμάτωσης διανέμονται μαζί με τη βιβλιοθήκη· δείτε τη σελίδα προϊόντος PDF Library for Delphi για εκδόσεις και αδειοδότηση