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

Διάγνωση σιωπηλών αστοχιών stub σε βιβλιοθήκη PDF Pascal

Όταν μια βιβλιοθήκη Delphi αποκτά μια διαμόρφωση build χωρίς το οπτικό framework, οι κλάσεις αντικατάστασης είναι εκεί όπου ζουν τα σφάλματα. Όχι η πλατφόρμα, όχι ο μεταγλωττιστής: οι stand-ins. Το PDFlibPas έχει ένα στρώμα γραφικών που παρέχει αντίστοιχα bitmap, canvas, γραμματοσειράς, metafile και εκτυπωτή για builds χωρίς το VCL, και η μεταφορά του σε Free Pascal έφερε στην επιφάνεια κάθε λειτουργία αστοχίας που μπορεί να έχει ένα stand-in. Ταξινομούνται καθαρά κατά κόστος διάγνωσης, και η σειρά είναι το αντίθετο από όσο υπονοεί η διαίσθηση

Ένα stand-in που πετάει εξαίρεση είναι φθηνό να βρεθεί· η εξαίρεση ονομάζει τη μέθοδο. Ένα stand-in που επιστρέφει κενά δεδομένα είναι ακριβό, γιατί η αστοχία εμφανίζεται αρκετά στρώματα μακριά από την αιτία της. Ένα stand-in που επιστρέφει επιτυχία είναι το χειρότερο από όλα, γιατί ο κωδικός επιστροφής είναι έγκυρος, ο κωδικός σφάλματος είναι μηδέν, καμία εξαίρεση δεν πετιέται, και το μόνο στοιχείο ότι κάτι πήγε στραβά βρίσκεται στα bytes που βγήκαν

Τρία σχήματα σιωπηλής αστοχίας stub σε βιβλιοθήκη PDF Pascal κατατασσόμενα κατά κόστος διάγνωσης, από stub που πετάει εξαίρεση έως επιτυχία πάνω σε κενή έξοδο
Ένα stand-in με εξαίρεση είναι φθηνό στη διάγνωση, τα κενά δεδομένα είναι ακριβά, και η επιστροφή επιτυχίας πάνω σε κενό προϊόν είναι το χειρότερο στον εντοπισμό

Σχήμα τρία: έγκυρο αναγνωριστικό εικόνας πάνω σε κενό XObject

Ο διανυσματικός μετατροπέας metafile ήταν ένα κενό σώμα διαδικασίας στη διαμόρφωση χωρίς VCL. Όλα πάνω του εξακολουθούσαν να δουλεύουν. Τα σημεία εισόδου εισαγωγής EMF και το σημείο εισόδου λήψης canvas έτρεχαν μέχρι τέλους και επέστρεφαν νόμιμο αναγνωριστικό εικόνας, που ο καλών τοποθετούσε μετά σε μια σελίδα. Αυτό που κατέληγε στο αρχείο ήταν ένα form XObject με μήκος περιεχομένου μηδέν. Η σελίδα αποδιδόταν λευκή

Τίποτα δεν ανέφερε πρόβλημα, και αυτό περιλαμβάνει το δικό του πρόγραμμα επίδειξης της βιβλιοθήκης για αυτή τη λειτουργία, που ζωγράφιζε λευκή σελίδα και δεν το παρατηρούσε. Δεν υπήρχε αποτυχημένη τιμή επιστροφής προς έλεγχο, γιατί η σειρά των κλήσεων όντως επιτύγχανε όλες· το μόνο που ήταν στραβά ήταν το μέγεθος του παραγόμενου stream. Η διάγνωση αυτής της κατηγορίας ελαττωμάτων σημαίνει ότι θέτετε διαφορετικό ερώτημα: όχι «απέτυχε η κλήση» αλλά «είναι πειστικό το προϊόν». Ένα form XObject μήκους μηδέν, μια εικόνα μηδενικών pixels, μια σελίδα μηδενικών bytes περιεχομένου — αυτοί είναι οι ισχυρισμοί που το πιάνουν

Η διόρθωση έχει δύο μισά και το δεύτερο μισό ξεχνιέται εύκολα. Πρώτα, κάντε την κενή υλοποίηση να πετάει εξαίρεση, ώστε η αστοχία να έχει έστω ένα κανάλι. Μετά, μετατρέψτε αυτή την εξαίρεση σε null αποτέλεσμα στο εργοστάσιο εικόνων και προσθέστε ελέγχους null στα δύο σημεία που καταναλώνουν αναγνωριστικό εικόνας, γιατί αλλιώς η «καθαρή αστοχία» γίνεται κατευθείαν παραβίαση πρόσβασης καθώς το δέντρο σελίδων αναφέρεται στο τίποτα. Ένα stub που πετάει είναι βελτίωση μόνο αν οι καλούντες ήταν προετοιμασμένοι για αστοχία που δεν είχαν ποτέ ξαναμπορέσει να λάβουν

Σχήμα δύο: κενά δεδομένα, τρία στρώματα μακριά από την κατάρρευση

Το stand-in canvas metafile δεν συμπλήρωνε τις φυσικές του διαστάσεις. Η τιμή αυτή διαιρείται σε έναν υπολογισμό γεωμετρίας σελίδας, οπότε ο υπολογισμός παρήγαγε μηδέν, οπότε ο υπολογισμός του bounding box διαίρεσε με μηδέν. Ένας γυμνός χειριστής εξαιρέσεων τον κατάπιε, το εργοστάσιο εικόνων επέστρεψε null αποτέλεσμα, και η παραβίαση πρόσβασης συνέβη τελικά στο δέντρο σελίδων όταν χρησιμοποιήθηκε το null. Τρία στρώματα ανάμεσα σε αιτία και σύμπτωμα, με έναν χειριστή εξαιρέσεων στη μέση που σβήνει τα αποδεικτικά στοιχεία

Αλυσίδα αστοχίας ενός κενού stand-in canvas metafile στο PDFlibPas που φτάνει σε παραβίαση πρόσβασης τρία στρώματα μετά τη διαίρεση με μηδέν
Η κενή διάσταση οδηγεί υπολογισμό γεωμετρίας στο μηδέν, ένας γυμνός χειριστής σβήνει την εξαίρεση, και το null αναγνωριστικό καταρρίπτει το δέντρο σελίδων

Η ίδια μονάδα είχε δύο ακόμα περιπτώσεις του μοτίβου. Η κλάση γραμματοσειράς είχε κενά σώματα Assign και constructor, που έχει μεγαλύτερη σημασία απ' όσο φαίνεται γιατί η ιδιότητα γραμματοσειράς του canvas είναι μόνο για ανάγνωση: η ανάθεση σε αυτήν είναι ο μόνος τρόπος παράδοσης γραμματοσειράς, οπότε μια κενή υλοποίηση κάνει την επιλογή γραμματοσειράς σιωπηλά ανενεργή και το κείμενο βγαίνει σε ό,τι ήταν η προεπιλογή. Και μια τιμή pixels-per-inch μηδέν έκανε κάθε καλούντα που μετράει canvas από μετρικές γραμματοσειράς να παράγει canvas μηδενικό επί μηδενικού, που δίνει λευκή σελίδα και επιστροφή επιτυχίας

// Το σχήμα προς αναζήτηση σε μονάδα stand-in: μια μέθοδος που ούτε
// πετάει εξαίρεση ούτε κάνει τίποτα. Και τα δύο μεταγλωττίζονται και
// παράγουν «επιτυχία» χωρίς έξοδο
procedure TMetafileCanvasStandIn.Create(...);
begin
  // χωρίς κλήση inherited, χωρίς αρχικοποίηση πεδίων
end;

function TBitmapStandIn.LoadFromStream(Stream: TStream): Boolean;
begin
  Result := True;    // και το bitmap εξακολουθεί να είναι κενό
end;

Η wide δομή που κρατά μόνο τον πρώτο χαρακτήρα

Αυτό δεν είναι καθόλου πρόβλημα stand-in, αλλά ανήκει στον ίδιο κατάλογο γιατί το σύμπτωμα είναι εξίσου μακριά από την αιτία. Η δομή απαρίθμησης εκτυπωτών είχε δηλωθεί με και τα δώδεκα μέλη συμβολοσειρών της τυποποιημένα ως δείκτες σε χαρακτήρες ενός byte, ενώ η συνάρτηση που τη γεμίζει είναι η παραλλαγή wide-character του API απαρίθμησης

Τα μεγέθη των δεικτών είναι πανομοιότυπα, οπότε η διάταξη της δομής είναι σωστή και τίποτα δεν καταρρέει. Αυτό που συμβαίνει αντίθετα είναι ότι η ανάγνωση συμβολοσειράς UTF-16 ως συμβολοσειράς ενός byte σταματά στο πρώτο byte μηδέν, που για οποιοδήποτε όνομα εκτυπωτή ASCII είναι το ψηλό μισό του δεύτερου χαρακτήρα. Κάθε όνομα εκτυπωτή επέστρεφε με ακριβώς έναν χαρακτήρα. Κατάντη, η επικύρωση ονόματος απέτυχε, η δημιουργία εκτυπωτή απέτυχε και η εκτύπωση απέτυχε για κάθε πραγματικό εκτυπωτή στη μηχανή, και κανένα από αυτά τα συμπτώματα δεν δείχνει σε δήλωση δομής

Όνομα εκτυπωτή UTF-16 περικομμένο σε έναν χαρακτήρα μετά τη δήλωση wide δομής Win32 με μέλη PAnsiChar αντί για PWideChar
Μέλη ενός byte διαβάζουν όνομα UTF-16 μόνο έως το πρώτο byte μηδέν του, οπότε κάθε όνομα εκτυπωτή επιστρέφει με ακριβώς έναν χαρακτήρα
// Λάθος: σωστό μέγεθος, λάθος τύπος στοιχείου. Κανένα σφάλμα
// μεταγλώττισης, καμία κατάρρευση, κάθε συμβολοσειρά περικομμένη
// σε έναν χαρακτήρα
type
  TPrinterInfo2Wrong = record
    pServerName: PAnsiChar;
    pPrinterName: PAnsiChar;
    // ... δέκα ακόμα
  end;

// Σωστό: μια δομή *W έχει wide μέλη παντού
type
  TPrinterInfo2W = record
    pServerName: PWideChar;
    pPrinterName: PWideChar;
    // ... δέκα ακόμα
  end;

Ο κανόνας που βγαίνει από εκεί είναι μηχανικός και αξίζει να εφαρμόζεται χωρίς σκέψη: για κάθε δομή Win32 του οποίου το όνομα τελειώνει σε W, επαληθεύστε ότι κάθε μέλος συμβολοσειράς είναι η wide παραλλαγή, πεδίο προς πεδίο. Η ανάμειξη των κόσμων ANSI και wide δεν παράγει ούτε διαγνωστικό μεταγλωττιστή ούτε κατάρρευση, μόνο σιωπηλή περικοπή, και το ίδιο ισχύει αντιστρόφως για τις παραλλαγές ANSI

Ένας γυμνός χειριστής εξαιρέσεων είναι ο πραγματικός αντίπαλος

Κάθε μία από αυτές τις έρευνες επιβραδύνθηκε από την ίδια κατασκευή: έναν χειριστή που πιάνει τα πάντα και τα μετατρέπει σε τιμή επιστροφής false. Είναι λογικό να γράφεται γύρω από έναν αποκωδικοποιητή εικόνων, αφού μια κατεστραμμένη εικόνα δεν πρέπει να ρίξει κάτω μια εργασία εγγράφου. Είναι επίσης μια συσκευή διαγραφής του μοναδικού κομματιού πληροφορίας που χρειάζεστε

Η πρακτική απάντηση είναι να γίνει ο χειριστής προσωρινά θορυβώδης. Η εκτύπωση της κλάσης εξαίρεσης, του μηνύματος και του backtrace από μέσα στον γυμνό χειριστή, υπό συνθήκη debug, μετατρέπει μια ανεξήγητη επιστροφή null σε ονομασμένη εξαίρεση με τοποθεσία. Σε δύο από τις τρεις παραπάνω περιπτώσεις αυτό το μοναδικό βήμα τερμάτισε την έρευνα, γιατί η εξαίρεση ήταν διαίρεση με μηδέν ή παραβίαση πρόσβασης σε μέθοδο stand-in του οποίου το όνομα έλεγε τα πάντα

Λίστα ελέγχου για υιοθέτηση διαδρομής stand-in

Τέσσερα σημεία, με τη σειρά που αποδίδουν. Πριν καλέσετε σε κλάση αντικατάστασης, διαβάστε τις μεθόδους που πρόκειται να χρησιμοποιήσετε και επιβεβαιώστε ότι η καθεμία έχει πραγματικό σώμα· ένα κενό σώμα δεν είναι λεπτομέρεια υλοποίησης, είναι χαρακτηριστικό που λείπει. Προτιμήστε stand-ins που πετούν εξαίρεση από stand-ins που επιστρέφουν ουδέτερες τιμές, και συνδυάστε το με ελέγχους null στα σημεία όπου ένα εργοστάσιο μπορεί πλέον νόμιμα να μην επιστρέφει τίποτα. Επαληθεύστε μια λειτουργία εξετάζοντας το προϊόν, όχι τον κωδικό επιστροφής, αφού ολόκληρη η λειτουργία αστοχίας εδώ είναι καθαρός κωδικός επιστροφής πάνω σε κενό προϊόν· μια ανάλυση σε επίπεδο bytes του τι περιέχει πραγματικά ένα έγγραφο είναι ο ταχύτερος τρόπος να το δείτε, και το άρθρο για τον έλεγχο μεγέθους αρχείου καλύπτει εκείνα τα εργαλεία. Και όταν μια λειτουργία δεν έχει βιώσιμη αντικαταστατική υλοποίηση, δρομολογήστε τα επηρεαζόμενα δείγματα στη διαδρομή που δουλεύει και πείτε το γιατί σε ένα σχόλιο, αντί να αφήσετε μια επίδειξη που παράγει σιωπηλά λευκή έξοδο

Το ευρύτερο σημείο ισχύει πολύ πέρα από μία βιβλιοθήκη. Κάθε codebase με μια δεύτερη υλοποίηση υπό συνθήκη, ένα στρώμα mock, μια λειτουργία headless, ένα shim πλατφόρμας, εκτίθεται στο σχήμα τρία. Ο λόγος που κρύβεται τόσο καλά είναι ότι κάθε πύλη ποιότητας στην οποία βασίζεται συνήθως μια ομάδα, κωδικοί επιστροφής, κωδικοί σφάλματος, εξαιρέσεις, καταστάσεις εξόδου, είναι κανάλι κατάστασης, και το σχήμα τρία τα κρατά όλα καθαρά. Μόνο η έξοδος το προδίδει. Αυτή είναι και η συλλογιστική πίσω από τον έλεγχο προϊόντων αντί για καταστάσεων όταν χειρίζεστε μη αξιόπιστη είσοδο, που περιγράφεται στο άρθρο για την ανάλυση μη αξιόπιστων PDF, και πίσω από τη σύγκριση αποδιδόμενης εξόδου ανάμεσα σε μηχανές αντί να εμπιστεύεστε μία, που περιγράφεται στην απόδοση πολλαπλών μηχανών

Το PDFlibPas είναι μια εγγενής βιβλιοθήκη PDF σε Object Pascal για Delphi, C++Builder και Free Pascal, και η διαμόρφωσή του χωρίς VCL είναι αυτό που καθιστά δυνατά τα builds headless και cross-toolchain· η τρέχουσα κάλυψη διαμορφώσεων καταγράφεται στη σελίδα προϊόντος losLab PDF Developer Library