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

HotXLS σε Free Pascal: Unicode, COM slots και zlib

Το HotXLS χτίζει κάτω από Free Pascal και Lazarus σε Windows, και το port κρίθηκε σε τέσσερις αποφάσεις που δεν έχουν καμία σχέση με τη σύνταξη Object Pascal: κρατήστε τον πυρήνα σε λειτουργία DELPHIUNICODE, δηλώστε τα OLE structured-storage interfaces ως CORBA interfaces με χειροκίνητη διαχείριση μέτρησης αναφορών, αντικαταστήστε τα Win32 AES object files με Pascal υλοποίηση, και φτιάξτε έναν βρόχο inflate που μπορούσε να δεχτεί κομμένο ZIP ως πλήρες

Όποιος έκανε port ώριμη βιβλιοθήκη Delphi ξέρει το σχήμα αυτής της δουλειάς. Ο compiler δέχεται σχεδόν όλα στο πρώτο πέρασμα. Ακολουθεί μια μακριά ουρά συμπεριφορικών διαφορών που μεταγλωττίζονται καθαρά και παράγουν λάθος αποτελέσματα, και μια μηχανή λογιστικών φύλλων είναι ασυνήθιστα εκτεθειμένη σε αυτές επειδή ακουμπά κωδικοποίηση κειμένου, COM structured storage, συμπίεση και κρυπτογραφία σε ένα μονοπάτι κώδικα

Γιατί ο πυρήνας επιμένει σε DELPHIUNICODE και όχι σκέτο DELPHI;

Γιατί η μηχανή τύπων εξαρτάται από String και Char που κουβαλάνε σημασιολογία UTF-16, και η εναλλακτική ANSI χάνει χαρακτήρες προτού φτάσει τίποτα στο αρχείο. Είναι δελεαστικό να χτίσεις τον πυρήνα σε λειτουργία FPC DELPHI, αφού αυτό είναι ο διακόπτης συμβατότητας που πιάνουν τα περισσότερα ports, και ο κώδικας μεταγλωττίζεται. Μετά ένα βιβλίο με κινεζικά ονόματα φύλλων ή κυριλλικές ετικέτες κάνει πλήρες ταξίδι μέσα από το μονοπάτι υπολογισμού και οι χαρακτήρες έχουν χαθεί μέχρι να τους δει ο writer, χωρίς κανένα σφάλμα πουθενά

Η λειτουργία δεν είναι ομοιόμορφη σε όλη τη βιβλιοθήκη, και αυτό είναι σκόπιμο και όχι απροσεξία. Ο byte decoder του PNG και τα LCL overrides χρειάζονται ειλικρινά ANSI υπογραφές, γιατί δουλεύουν με bytes και με όσα τους παραδίδει το widgetset. Εκείνες οι μονάδες ενεργοποιούν ξεχωριστό διακόπτη LX_FPC_ANSI. Δύο λειτουργίες σε μία βιβλιοθήκη ακούγεται σαν smell μέχρι να προσέξεις ότι η εναλλακτική είναι ένας byte decoder που μεταχειρίζεται την είσοδό του ως κείμενο

Υπάρχει μια συνοδός λεπτομέρεια που πιάνει κόσμο αργότερα. Το DELPHIUNICODE δεν κάνει το TFormatSettings.DecimalSeparator WideChar στο runtime του FPC. Είσοδος που κουβαλά Unicode διαχωριστικό δεκαδικών πρέπει να κανονικοποιηθεί σε ASCII διαχωριστικό μέσα στη συμβολοσειρά Unicode πρώτα, και οποιαδήποτε είσοδος της οποίας το διαχωριστικό δεν ταιριάζει με το αναμενόμενο πρέπει να απορρίπτεται και όχι να περικόπτεται σιωπηλά στον χαρακτήρα που ο parser δεν αναγνώρισε

program ExportReport;
{$MODE DELPHI}
uses
  Interfaces,          // πρέπει πρώτο: αρχικοποιεί το LCL widgetset
  SysUtils, lxHandle;  // και το στρώμα μετατροπής UTF-8

var
  Book: TXLSWorkbook;
begin
  Book := TXLSWorkbook.Create(nil);
  try
    Book.LoadFromFile('input.xls');
    Book.Sheets[0].AsString[1, 1] := 'Quarterly summary';
    Book.SaveToFile('output.xls');
  finally
    Book.Free;
  end;
end.

Η μονάδα Interfaces δεν είναι προαιρετική και πρέπει να έρχεται πρώτη. Είναι ό,τι αρχικοποιεί το LCL widgetset και το στρώμα μετατροπής UTF-8, και το HotXLS βασίζεται και στα δύο μόλις γραμματοσειρές, paths αρχείων ή κείμενο διασχίζουν το όριο RTL και LCL. Ένα πρόγραμμα κονσόλας που το προσπερνά θα μεταγλωττιστεί και θα συμπεριφερθεί άσχημα σε οποιοδήποτε μη-ASCII path. Αυτό είναι και ο λόγος που μια πετυχημένη μεταγλώττιση αποδεικνύει τόσα λίγα εδώ: το port αποδείχτηκε δουλεύει μόνο όταν πραγματικά έγγραφα με πραγματικά ονόματα γραμματοσειρών και πραγματικά paths έκαναν πλήρες ταξίδι μετ επιστροφής

Μια class VMT δεν είναι COM vtable

Το Free Pascal δεν θα σας αφήσει να δώσετε class VMT στα Windows ως vtable COM interface, ακόμα κι όταν η δήλωση μοιάζει πανομοιότυπη με αυτή που δέχεται το Delphi. Οι διατάξεις διαφέρουν με τρόπους που παράγουν κλήση σε λάθος slot, που εκδηλώνεται ως κρασάρισμα κάπου άσχετο με το σημείο κλήσης. Το structured storage μετράει εδώ επειδή η κλασική δυαδική μορφή βιβλίου εργασίας είναι OLE compound file, και η ανάγνωση ή εγγραφή ενός σημαίνει υλοποίηση ILockBytes μέσα στην οποία θα καλεί πίσω το Windows storage API

Η λειτουργική διευθέτηση είναι CORBA interface με τα COM slots δηλωμένα ρητά και AddRef και Release διαχειριζόμενα στο χέρι. Αυτό σημαίνει παραίτηση από αυτόματη μέτρηση αναφορών για αυτούς τους τύπους και ανάληψη ευθύνης για τον κύκλο ζωής, που είναι δίκαιος διακανονισμός για μια χουφτά interfaces που ζουν μέσα σε μία μονάδα. Η συγκεκριμένη παγίδα μέσα σε εκείνη τη δουλειά είναι το QueryInterface: πρέπει να γυρνά δείκτη interface, όχι δείκτη αντικειμένου. Και τα δύο μεταγλωττίζονται. Το ένα παραδίδει στα Windows μια διεύθυνση της οποίας η πρώτη λέξη μηχανής δεν είναι vtable

Διάγραμμα που συγκρίνει την class VMT του Free Pascal με τη vtable COM interface που πρέπει να παρουσιάσει το HotXLS στο Windows structured storage API: διαφορετικές σειρές slots για την ίδια δήλωση Pascal, συν την παγίδα QueryInterface όπου η επιστροφή δείκτη αντικειμένου αντί για δείκτη interface στέλνει κλήση ILockBytes σε class slot και κρασάρει μακριά από το σημείο κλήσης
Το Free Pascal αρνείται να σερβίρει class VMT ως COM vtable, οπότε το HotXLS δηλώνει CORBA interfaces με ρητά COM slots και χειροκίνητα AddRef και Release, και το QueryInterface γυρνά τον δείκτη interface που τα Windows μπορούν να κάνουν dereference

Οι δηλώσεις ειδικές για FPC ζουν στο lxOleInterfaces.inc, δίπλα στα lxAESBackend.inc και lxZlibBackend.inc στον κατάλογο πηγαίου FPC, ώστε οι επιλογές ειδικές για compiler κάθονται σε ένα σημείο αντί να σκορπίζονται μέσα στη μηχανή. Η μορφή ίδια και πώς πλοηγείται σε αυτήν η βιβλιοθήκη περιγράφονται στο ανάγνωση OLE2 compound files σε Pascal

Μία ακόμη λεπτομέρεια τύπου ανήκει στην ίδια οικογένεια. Το LargeInt πρέπει να επιλύεται σε Int64 στον κλάδο FPC, και η ταξινόμηση του Comp από τον compiler διαφέρει αρκετά ανάμεσα στα δύο toolchains ώστε η επίλυση overloads να μπορεί να διαλέξει διαφορετικό υποψήφιο. Δοκιμάστε συμπεριφορά μεγάλων offsets με file stream και όχι HGLOBAL stream: το stream καθολικής μνήμης των Windows κάνει wrap μόνο του σε seeks πάνω από 4 GiB, οπότε ένα πετυχημένο test εκεί δεν αποδεικνύει τίποτα για τη δική σας αριθμητική

Τι κρύβει μια αυτοσυνεπής υλοποίηση AES

Τα Win32 AES object files που συνδέει το build του Delphi είναι OMF, και ο linker του Free Pascal δεν μπορεί να τα καταναλώσει, οπότε ο κλάδος FPC χρησιμοποιεί Pascal υλοποίηση AES. Το Delphi εξακολουθεί να συνδέει τα object files που έκανε πάντα, που κρατά το released binary αμετάβλητο για υπάρχοντες πελάτες

Η απαίτηση επαλήθευσης είναι το κομμάτι που αξίζει να μεταφερθεί σε οποιοδήποτε project. Κρυπτογράφηση δεδομένων και αποκρυπτογράφησή τους ξανά με την ίδια υλοποίηση δεν αποδεικνύει απολύτως τίποτα: ένας συμμετρικός αλγόριθμος με λάθος key schedule, λάθος σειρά blocks ή λάθος αλυσιδωτό είναι απολύτως αυτοσυνεπής και θα κάνει round-trip τη δική του έξοδο κάθε φορά. Μόνο known-answer vectors το πιάνουν, ελέγχοντας την επέκταση κλειδιού, τη σειρά blocks και την αλυσίδωση CBC απέναντι σε δημοσιευμένες τιμές. Παραδώστε αυτοσυνεπή λάθος υλοποίηση και το σύμπτωμα εμφανίζεται την πρώτη φορά που ένας πελάτης ανοίγει το αρχείο στο Excel

Η συμπίεση είχε ελάττωμα άλλου χαρακτήρα. Ένα Pascal inflate backend μπορεί να έχει ακόμα εκκρεμή έξοδο αφού κατανάλωσε όλη τη συμπιεσμένη είσοδό του, οπότε ο caller πρέπει να συνεχίζει να καλεί μέχρι το stream να αναφέρει το τέλος του. Η μεταχείριση εξαντλημένης εισόδου ως τέλους ροής περικόπτει το τελευταίο block. Χειρότερα, γυρνά ένα κατεστραμμένο αρχείο σε σιωπηλά αποδεκτό, που είναι ακριβώς η μορφή αποτυχίας που υπάρχει για να αποτρέπει η ενίσχυση στο επικύρωση του ZIP end-of-central-directory record. Ο κανόνας είναι ότι μηδενική πρόοδος συν μη τελειωμένο είναι σφάλμα περικοπής, ποτέ EOF

Δύο παγίδες συστήματος χτισίματος που κοστίζουν πραγματικές ώρες

Τα search paths του LCL πρέπει να προηγούνται των wildcard paths των FPC packages, αλλιώς η μονάδα Menus του Free Vision σκιάζει τη μονάδα LCL με το ίδιο όνομα και παίρνετε ασυμφωνία checksum PPU που δεν λέει τίποτα για κανένα από τα δύο. Μια εγκατάσταση Lazarus που μετακινήθηκε αφού εγκαταστάθηκε μπορεί επίσης να αφήσει ξεπερασμένα paths στο fpc.cfg, οπότε τα σημεία εισόδου χτισίματος ορίζουν paths μονάδων και binaries ρητά αντί να κληρονομούν ό,τι προσφέρει το περιβάλλον

Η δεύτερη παγίδα δεν έχει καμία σχέση με την Pascal. Ένα batch αρχείο .cmd γραμμένο με LF τερματισμούς γραμμών δουλεύει μέχρι το αρχείο να ξεπεράσει το μέγεθος του read buffer του διερμηνέα, οπότε το call :label αποτυγχάνει με ισχυρισμό ότι η ετικέτα batch δεν υπάρχει, και η αποτυχία εμφανίζεται στο όποιο πρόγραμμα τυχαίνει να κάθεται πέρα από το όριο. Κάθε εργαλείο που ξαναγράφει batch script πρέπει να γράφει CRLF πίσω. Και το lazbuild --build-all αδειάζει τον κατάλογο εξόδου μονάδων του package προτού μεταγλωττίσει, οπότε αρχείο options σταθμευμένο σε εκείνον τον κατάλογο σβήνεται προτού διαβαστεί: κρατήστε το έξω, και θυμηθείτε ότι το path @ επιλύεται σχετικά με τον κατάλογο του package επειδή το lazbuild καλεί τον compiler από εκεί

Χάρτης των δύο στρωμάτων κινδύνου πίσω από ένα καθαρό compile HotXLS Free Pascal: η λειτουργία DELPHIUNICODE που κρατά String και Char σε UTF-16, η διέξοδος LX_FPC_ANSI για τον byte decoder του PNG και τα LCL overrides, και παγίδες χτισίματος από τη σκιά Menus του Free Vision, ξεπερασμένα paths fpc.cfg, batch αρχεία μόνο LF και σβήσιμο εξόδου από lazbuild
Ένα πρώτο compile αποδεικνύει λίγα: ο χάρτης λειτουργιών αποφασίζει ποιες χαρακτήρες επιβιώνουν μέχρι τον writer, ενώ οι παγίδες του συστήματος χτισίματος έρχονται στην επιφάνεια ως ασυμφωνίες checksum, φανταστικές χαμένες ετικέτες και αρχεία options σβησμένα προτού διαβαστούν
// Εξαγωγή grid Lazarus: η TGridToXLS παραδίδεται στο Lazarus package,
// ώστε ο ίδιος κώδικας εξαγωγής DB-grid δουλεύει σε εφαρμογή LCL
var
  Exporter: TGridToXLS;
begin
  Exporter := TGridToXLS.Create(nil);
  try
    Exporter.DBGrid := GridOrders;
    Exporter.WorksheetName := 'Orders';
    Exporter.ExportHeader := True;
    Exporter.SetColumnsWidth := True;
    Exporter.ExportDBGrid;
    Exporter.SaveAs('orders.xls');
  finally
    Exporter.Free;
  end;
end;

Τι αξίζει μια προειδοποίηση compiler

Το Free Pascal αναφέρει τοπικές μεταβλητές χωρίς αρχικοποίηση που το Delphi δεν αναφέρει, και το τρέξιμο του build FPC μετέτρεψε εκείνη τη διαφορά σε δύο πραγματικά ελαττώματα στη μονάδα υπολογισμού. Μια συνάρτηση διάβαζε μεταβλητή μέτρησης που δεν είχε ανατεθεί ποτέ προ χρήσης, και μια άλλη χρησιμοποιούσε δύο συντεταγμένες σε έναν κλάδο προτού ο κώδικας που τις υπολόγιζε τρέξει σε διαφορετικό κλάδο. Κάτω από Delphi και οι δύο συμπεριφέρονταν ανάλογα με ό,τι τυχαία κρατούσε η στοίβα, που είναι ο ορισμός bug που αναπαράγεται σε μια μηχανή και όχι σε άλλη

Το πρακτικό συμπέρασμα είναι ότι ο δεύτερος compiler αξίζει να μείνει στον βρόχο ακόμα και για προϊόν που παραδίδει πρωτίστως στον πρώτο. Το περιοδικό σάρωμα των κλάσεων προειδοποιήσεων FPC είναι ένα φθηνό πέρασμα στατικής ανάλυσης πάνω σε codebase Delphi, και βρίσκει κατηγορία ελαττωμάτων που καμία σουίτα tests δεν φτάνει αξιόπιστα. Η φαρδύτερη πειθαρχία version-matrix μέσα στην οποία κάθεται αυτό περιγράφεται στο cross-compiler build matrix

Η υποστήριξη Free Pascal και Lazarus για Windows παραδίδεται με το HotXLS Delphi spreadsheet component ως Lazarus package δίπλα στα πακέτα Delphi και C++Builder, χτισμένο από το ίδιο source tree και όχι από fork. Αυτό είναι το νόημα της άσκησης: μία μηχανή, τέσσερα toolchains, και οι αποφάσεις ειδικές για compiler απομονωμένες σε include files όπου διαβάζονται σε μία συνεδρία