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

HotPDF σε Free Pascal: Deflate, AES και όρια codecs

Το HotPDF μεταγλωττίζεται και τρέχει υπό Free Pascal 3.2.2 με Lazarus, και η ειλικρινής σύνοψη αυτής της μεταφοράς είναι δύο προτάσεις. Η δημιουργία, η φόρτωση, η αποθήκευση, η συμπίεση, η αποσυμπίεση, η κρυπτογράφηση και η αποκρυπτογράφηση εγγράφων δουλεύουν όλες σε backends μόνο Pascal, οπότε μια εφαρμογή Lazarus μπορεί να παράγει και να καταναλώνει πραγματικό PDF χωρίς καμία εξάρτηση C. Οι προαιρετικοί εγγενείς codecs εικόνων δεν δουλεύουν, γιατί τα προκατασκευασμένα objects Win64 χρησιμοποιούν μια παραλλαγή COFF που δεν μπορεί να καταναλώσει κανείς από τους δύο linkers του Free Pascal, οπότε σε αυτό το toolchain τα σημεία εισόδου επιλύονται σε stubs που αποτυγχάνουν κλειστά

Χάρτης δυνατοτήτων HotPDF σε Free Pascal: λειτουργικά backends deflate, AES και εγγράφων σε Pascal δίπλα σε stubs codecs εικόνων που αποτυγχάνουν κλειστά
Οι λειτουργίες εγγράφου, συμπίεσης και κρυπτογράφησης τρέχουν σε backends μόνο Pascal, ενώ οι εγγενείς codecs εικόνων επιλύονται σε stubs που αποτυγχάνουν κλειστά

Το πέρασμα από «μεταγλωττίζεται» σε «δουλεύει» πήρε ένα συγκεκριμένο σύνολο διορθώσεων, και καθεμία από αυτές είναι μια παγίδα που θα βρει κάθε άλλη codebase Delphi που μεταφέρεται σε Free Pascal. Αξίζει να γραφτούν με τη σειρά που πονούν

Γιατί μια μονάδα που μεταγλωττίζεται δεν αποδεικνύει τίποτα;

Επειδή μια μονάδα Pascal μπορεί να αναφέρεται σε ένα σύμβολο που δεν θα κάνει ποτέ τίποτα χρήσιμο και να ικανοποιεί ούτως τον μεταγλωττιστή. Στο σημείο που και οι 113 μονάδες της βιβλιοθήκης χτίστηκαν καθαρά υπό Free Pascal, οι χειριστές containers αρχείων δούλευαν πραγματικά, επαληθευμένοι από ένα smoke test που άνοιξε ένα CBZ και το μετέτρεψε σε PDF. Η ισοπέδωση φορμών XFA δεν δούλευε καθόλου, γιατί η ισοπέδωση πρέπει να αποσυμπιέζει το συμπιεσμένο stream πακέτου /XFA και το σημείο εισόδου deflate ήταν ακόμα stub. Τίποτα στην έξοδο του build δεν ξεχώριζε τις δύο περιπτώσεις

Ο κανόνας που βγήκε από εκεί είναι σύντομος. Πριν γράψετε σε σημείωση έκδοσης ότι μια λειτουργία δουλεύει σε νέο toolchain, γράψτε μια δοκιμή runtime που ασκεί τη λειτουργία από άκρη σε άκρη σε αυτό το toolchain. Η κάλυψη μεταγλώττισης είναι προϋπόθεση, ποτέ απόδειξη. Η ευρύτερη εικόνα του τι καλύπτει η μεταφορά βρίσκεται στις σημειώσεις υποστήριξης Win64 για Free Pascal και Lazarus

Μια εξαίρεση μέσα σε stub cdecl δεν φτάνει στον καλούντα

Αυτό αξίζει τη δική του ενότητα γιατί το σύμπτωμα είναι τόσο παραπλανητικό. Οι μονάδες stub εκθέτουν εισόδους C όπως θα έκανε μια static βιβλιοθήκη, οπότε ένα stub μοιάζει έτσι

// Μοιάζει λογικό. Δεν είναι.
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
  public name 'inflate';
begin
  raise ENotSupportedException.Create('codec unavailable');
end;

Σε Free Pascal για Win64 αυτή η εξαίρεση δεν διαδίδεται στον καλούντα. Δεν υπάρχει χειριστής try..except που τη βλέπει, γιατί το ξετύλιγμα πάνω από όριο cdecl δηλωμένο έτσι δεν κουβαλά το πλαίσιο εξαιρέσεων της Pascal· η διεργασία τερματίζει με κωδικό εξόδου 217. Από την πλευρά της εφαρμογής δεν υπάρχει σφάλμα, ούτε μήνυμα, ούτε γραμμή καταγραφής, απλώς ένα πρόγραμμα που εξαφανίζεται. Αυτό είναι αυστηρά χειρότερο από λάθος απάντηση, γιατί μια λάθος απάντηση μπορεί να χειριστεί

Γιατί μια εξαίρεση πεταμένη μέσα σε stub cdecl τερματίζει διεργασία Free Pascal με κωδικό εξόδου 217 και πώς η πύλη στο σημείο εισόδου Pascal τη διορθώνει
Το πλαίσιο εξαιρέσεων της Pascal δεν μπορεί να ξετυλιχθεί πάνω από το όριο cdecl, οπότε η διεργασία πεθαίνει σιωπηλά· η διόρθωση φράσσει πριν φτάσει ποτέ στο stub

Η δελεαστική διόρθωση είναι να κάνετε το stub να επιστρέφει κωδικό αστοχίας, και για την inflate αυτό είναι σωστό γιατί η zlib έχει καλά ορισμένη επιστροφή σφάλματος. Είναι λάθος γενικά: ένα stub για την jpeg_read_header που επιστρέφει μηδέν λέει στον καλόν να συνεχίσει με μια δομή που δεν αρχικοποίησε κανείς. Η ανθεκτική διόρθωση είναι να φράξετε στο σημείο εισόδου Pascal και όχι μέσα στο stub με σχήμα C, χρησιμοποιώντας όποια σύμβαση αστοχίας έχει ήδη αυτό το API

function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
  // Άρνηση πριν φτάσει ποτέ το stub, με τη σύμβαση αστοχίας του
  // ίδιου του API αντί για εξαίρεση πάνω από το όριο cdecl
  Bitmap := nil;
  Result := False;
  Exit;
{$ENDIF}
  Result := DecodeJPEGNative(Data, Bitmap);
end;

Το paszlib δεν είναι zlib, και η διαφορά είναι δύο κλάσεις εγγράφων

Η διαθέσιμη υλοποίηση deflate σε Pascal στο Free Pascal χειρίζεται δύο πλαισιώσεις: το wrapper zlib και το raw deflate. Δεν χειρίζεται πλαισίωση gzip, που η zlib επιλέγει μέσω τιμών windowBits από 16 έως 31, και δεν χειρίζεται τη λειτουργία αυτόματης ανίχνευσης που επιλέγουν οι τιμές 32 έως 47. Το HotPDF χρειάζεται και τα δύο. Η ασφαλής διαδρομή εισαγωγής SVG ζητά 31, και ο loader έχει σκάλα εναλλακτικών που ζητά 47 όταν η πλαισίωση ενός stream είναι ασαφής. Παραλείψτε όποιο από τα δύο και μια ολόκληρη οικογένεια εγγράφων σταματά να ανοίγει, με σφάλμα αποκωδικοποίησης που δείχνει το stream και όχι την απόντα πλαισίωση

Κάλυψη windowBits του paszlib έναντι της zlib: εύρη πλαισίωσης gzip και αυτόματης ανίχνευσης απόντα για την εισαγωγή SVG και την εναλλακτική του loader στο HotPDF
Το paszlib χειρίζεται το wrapper zlib και το raw deflate, αλλά το HotPDF χρειάζεται επίσης windowBits 31 και 47, οπότε το shim πρέπει να παρέχει μόνο του την πλαισίωση gzip

Υπάρχει μια δεύτερη, πιο οξεία ασυμβατότητα. Το record z_stream που δηλώνει το paszlib δεν έχει την ίδια διάταξη μνήμης με το C: το πεδίο msg του είναι short string και όχι δείκτης, και τα total_in και total_out είναι 64-bit εκεί που το ABI C έχει μηχανικές λέξεις. Ένα record καλούντος επομένως δεν μπορεί να περαστεί ευθεία. Η λειτουργική διευθέτηση είναι να κρατείται η κατάσταση paszlib πίσω από τον δείκτη state που το δημόσιο record κρατά ήδη δεσμευμένο, και να αντιγράφονται τα δημόσια πεδία μέσα και έξω γύρω από κάθε κλήση. Το CRC του gzip και το trailer μήκους οκτώ bytes λογαριάζονται στο ίδιο στρώμα shim, που είναι το φυσικό τους μέρος αφού αυτό κατέχει ήδη την απόφαση πλαισίωσης

Πέρασμα δυναμικού πίνακα σε untyped παράμετρο var

Αυτό είναι το σφάλμα που είναι πιο πιθανό να κάθεται στον κώδικά σας αυτή τη στιγμή. Όταν περνάτε έναν δυναμικό πίνακα σε untyped παράμετρο var, αυτό που λαμβάνει ο καλούμενος είναι η διεύθυνση της μεταβλητής του πίνακα, που είναι η διεύθυνση ενός δείκτη, όχι η διεύθυνση του payload. Έτσι μια ανάγνωση μέσα σε αυτόν αντικαθιστά την ίδια τη μεταβλητή και ό,τι κάθεται δίπλα της

var
  FBuffer: TBytes;
begin
  SetLength(FBuffer, 65536);

  // Λάθος: δίνει τη διεύθυνση της μεταβλητής FBuffer
  FStream.Read(FBuffer, Length(FBuffer));

  // Σωστό: δίνει τη διεύθυνση του πρώτου byte payload
  FStream.Read(FBuffer[0], Length(FBuffer));
end;

Σε Delphi η λάθος μορφή συχνά φαίνεται να δουλεύει, γιατί αυτό που χαλάει είναι μια γειτονική θέση στοίβας που δεν διαβάζει τίποτα μετά. Σε Free Pascal η ίδια γραμμή προκαλεί segmentation fault στην πρώτη χρήση. Αυτό που την κάνει τόσο δύσκολη να τη δει το μάτι είναι ότι οι στατικοί πίνακες δεν έχουν τέτοιο πρόβλημα, αφού μια μεταβλητή στατικού πίνακα είναι το ίδιο της το payload, οπότε και οι δύο γραφές είναι σωστές στο ίδιο αρχείο ανάλογα με τη δήλωση μερικές εκατοντάδες γραμμές πιο πέρα

Containers ZIP χωρίς System.Zip

Το Free Pascal δεν έχει αντίστοιχο της μονάδας zip του RTL, και η διαθέσιμη εναλλακτική έχει και διαφορετική επιφάνεια API και καμία υποστήριξη για την κληρονομική κρυπτογράφηση που εξακολουθούν να χρησιμοποιούν παλαιότερες μορφές containers, οπότε ένας μικρός αναγνώστης μέσα στη βιβλιοθήκη βγήκε συντομότερος από την προσαρμογή σε αυτήν. Δύο λεπτομέρειες μορφής κόστισαν χρόνο και είναι εύκολο να πάουν στραβά

Η πρώτη είναι το byte ελέγχου της κεφαλίδας κρυπτογράφησης. Το δωδέκατο byte της είναι κανονικά το υψηλό byte του CRC, αλλά όταν είναι αναμένο το bit 3 της γενικής σημαίας, που σημαίνει ότι τα μεγέθη ζουν σε ένα trailing data descriptor και το CRC δεν είναι ακόμα γνωστό, το byte ελέγχου προέρχεται αντ' αυτού από το υψηλό byte της ώρας τροποποίησης. Υλοποιήστε μόνο τη μορφή CRC και κάθε αρχείο γραμμένο σε λειτουργία streaming απορρίπτει σωστό κωδικό πρόσβασης. Η δεύτερη είναι το extra πεδίο ZIP64: τα τρία πεδία 64-bit του εμφανίζονται σε σταθερή σειρά αλλά γράφονται μόνο όταν το αντίστοιχο πεδίο 32-bit είναι κορεσμένο, οπότε η ανάγνωσή τους σε σταθερά offsets δουλεύει στα αρχεία που δοκιμάσατε και αποτυγχάνει στο επόμενο. Αναλύστε τα τοπολογικά βάσει των πεδίων 32-bit που είναι κορεσμένα

Μια διευκόλυνση που αξίζει να γνωρίζετε: το stream αποσυμπίεσης του Free Pascal δέχεται ένα δεύτερο όρισμα constructor που παρακάμπτει την κεφαλίδα zlib, που είναι ακριβώς αυτό που χρειάζονται οι εγγραφές ZIP αφού αποθηκεύουν raw deflate. Η διαδρομή αυτή δεν αγγίζει καθόλου το shim zlib της βιβλιοθήκης, οπότε δεν επηρεάζεται από το απόν backend C

Διαφάνεια έγχρωμων glyphs υπό το LCL

Η ανάγνωση του καναλιού alpha ενός ραστεροποιημένου έγχρωμου glyph είναι η μοναδική λεπτομέρεια γραφικών χωρίς άμεση μετάφραση. Η κλάση PNG του LCL δεν έχει accessor scanline που εκθέτει alpha, και η ανάθεση PNG σε bitmap την απορρίπτει, οπότε ένα έγχρωμο emoji φτάνει πλήρως αδιαφανές και συνθέτεται με μαύρο κουτί πίσω του. Η λειτουργική διαδρομή είναι η εικόνα interface: δημιουργήστε την από το PNG, και μετά διαβάστε pixels μέσω του accessor χρώματος, θυμόμενοι ότι τα συστατικά του είναι 16-bit και χρειάζονται ολίσθηση προς τα κάτω κατά οκτώ για να γίνουν bytes. Εκείνη η επιφάνεια χρησιμοποιεί επίσης φυσική σειρά γραμμών από πάνω προς τα κάτω, οπότε η αντιστροφή Height - 1 - Y που χρειάζεται ο κώδικας scanline του VCL πρέπει να αφαιρεθεί και όχι να μεταφερθεί

Δύο σημειώσεις για το σύστημα build πριν καταχωρίσετε σφάλμα

Μια πλήρης επαναδημιουργία αποτυγχάνει περιστασιακά με undefined σύμβολο του οποίου το όνομα τελειώνει σε επίθημα $crc και δεκαεξαδική τιμή. Το επίθημα υπολογίζεται από τους τύπους παραμέτρων, και αποτυγχάνει να ταυτιστεί όταν ένα build μεταγλωττίζει μια μονάδα απέναντι σε δύο διαφορετικές εκδόσεις interface στο ίδιο πέρασμα. Η επανεκτέλεση του build το καθαρίζει· η υπογραφή δεν είναι λάθος

Δεύτερον, το Free Pascal 3.2.2 δεν έχει anonymous methods, οπότε όπου η βιβλιοθήκη χρησιμοποιούσε closures για τη σύναξη παράλληλου αγωγού, το build Free Pascal παίρνει αντ' αυτής μια ντετερμινιστική σειριακή εναλλακτική. Η έξοδος είναι ταυτοίχια, ο ρυθμός απόδοσης όχι· αν εξαρτάστε από παράλληλη απόδοση σελίδων, αυτός είναι λόγος να μείνετε στο Delphi προς το παρόν, και ο σχεδιασμός του αγωγού περιγράφεται στο άρθρο για τον παράλληλο αγωγό απόδοσης. Η κατάσταση των codecs εικόνων είναι ο άλλος τόπος όπου η επιλογή toolchain αλλάζει δυνατότητα και όχι μόνο ταχύτητα, οπότε μια ανάπτυξη Lazarus πρέπει να σχεδιάσει τις μορφές εικόνων της αναλόγως· ο τρέχων πίνακας ανά toolchain βρίσκεται στη σελίδα προϊόντος HotPDF Delphi PDF component