Wenn eine Delphi-Bibliothek eine Build-Konfiguration ohne das visuelle Framework bekommt, leben die Fehler in den Ersatzklassen. Nicht die Plattform, nicht der Compiler: die Stellvertreter. PDFlibPas hat eine Grafikschicht, die Bitmap-, Canvas-, Font-, Metadatei- und Drucker-Äquivalente für Builds ohne die VCL bereitstellt, und die Portierung auf Free Pascal brachte jeden Fehlermodus ans Licht, den ein Stellvertreter haben kann. Sie sortieren sich sauber nach Diagnosekosten, und die Reihenfolge ist das Gegenteil dessen, was die Intuition nahelegt
Ein Stellvertreter, der auslöst, ist billig zu finden; die Exception nennt die Methode. Ein Stellvertreter, der leere Daten zurückgibt, ist teuer, denn der Fehler taucht mehrere Schichten von seiner Ursache entfernt auf. Ein Stellvertreter, der Erfolg zurückgibt, ist der schlimmste von allen, denn der Rückgabecode ist gültig, der Fehlercode ist null, keine Exception wird ausgelöst, und der einzige Beweis, dass etwas schiefging, liegt in den Bytes, die herauskamen
Form drei: eine gültige Bildkennung über leerem XObject
Der Vektor-Metadatei-Konverter war in der Nicht-VCL-Konfiguration ein leerer Prozedurrumpf. Alles darüber arbeitete weiter. Die EMF-Import-Einstiegspunkte und der Canvas-Capture-Einstiegspunkt liefen durch und gaben eine legale Bildkennung zurück, die der Aufrufer dann auf eine Seite setzte. Was in der Datei landete, war ein Form-XObject mit einer Contentlänge von null. Die Seite renderte weiß
Nichts meldete ein Problem, und das schließt das eigene Demonstrationsprogramm der Bibliothek für diese Funktion ein, das eine leere Seite zeichnete und es nicht bemerkte. Es gab keinen fehlgeschlagenen Rückgabewert zu prüfen, denn die Aufrufsequenz war tatsächlich komplett erfolgreich; das Einzige, was falsch war, war die Größe des erzeugten Streams. Diese Fehlerklasse zu diagnostizieren heißt, eine andere Frage zu stellen: nicht „Ist der Aufruf fehlgeschlagen“, sondern „Ist das Artefakt plausibel“. Ein Form-XObject der Länge null, ein Bild mit null Pixeln, eine Seite mit null Content-Bytes, das sind die Assertions, die es fangen
Die Korrektur hat zwei Hälften, und die zweite Hälfte vergisst man leicht. Erstens: Die leere Implementierung auslösen lassen, damit der Fehler überhaupt einen Kanal hat. Zweitens: Diese Exception an der Bildfabrik in ein Null-Ergebnis umwandeln und Null-Prüfungen an den zwei Stellen ergänzen, die eine Bildkennung konsumieren, denn sonst wird aus dem „sauberen Scheitern“ direkt eine Access Violation, wenn der Seitenbaum ins Leere dereferenziert. Ein Stub, der wirft, ist nur dann eine Verbesserung, wenn die Aufrufer auf einen Fehler vorbereitet waren, den sie bisher nie empfangen konnten
Form zwei: leere Daten, drei Schichten vom Absturz entfernt
Der Metadatei-Canvas-Stellvertreter füllte seine physischen Abmessungen nicht. Dieser Wert geht in eine Seitengeometrieberechnung ein, also ergab die Berechnung null, also teilte die Bounding-Box-Berechnung durch null. Ein nackter Exception-Handler schluckte das, die Bildfabrik gab ein Null-Ergebnis zurück, und die Access Violation passierte schließlich im Seitenbaum, als die Null verwendet wurde. Drei Schichten zwischen Ursache und Symptom, mittendrin ein Exception-Handler, der die Beweise tilgt
Dieselbe Unit hatte zwei weitere Instanzen des Musters. Die Font-Klasse hatte leere Assign- und Konstruktorrümpfe, was mehr wiegt, als es aussieht, denn die Canvas-Font-Property ist schreibgeschützt: Eine Zuweisung daran ist der einzige Weg, einen Font zu liefern, also macht eine leere Implementierung die Fontauswahl still wirkungslos, und der Text kommt in dem heraus, was der Default war. Und ein Pixels-per-Inch-Wert von null ließ jeden Aufrufer, der einen Canvas aus Font-Metriken dimensioniert, einen Null-mal-Null-Canvas erzeugen, was eine leere Seite und eine Erfolgsmeldung ergibt
// Das Muster, nach dem man in einer Stellvertreter-Unit sucht: eine
// Methode, die weder auslöst noch irgendetwas tut. Beides kompiliert,
// und beides produziert „Erfolg“ ohne Ausgabe
procedure TMetafileCanvasStandIn.Create(...);
begin
// kein inherited-Aufruf, keine Feldinitialisierung
end;
function TBitmapStandIn.LoadFromStream(Stream: TStream): Boolean;
begin
Result := True; // und das Bitmap bleibt leer
end;
Die Wide-Struktur, die nur das erste Zeichen behält
Das ist überhaupt kein Stellvertreter-Problem, aber es gehört in denselben Katalog, weil das Symptom ebenso weit von der Ursache entfernt ist. Die Drucker-Enumerationsstruktur war mit allen zwölf String-Membern als Zeiger auf Einzelbyte-Zeichen deklariert, während die Funktion, die sie füllt, die Wide-Character-Variante der Enumerations-API ist
Zeigergrößen sind identisch, also ist das Struktur-Layout korrekt und nichts stürzt ab. Stattdessen passiert Folgendes: Eine UTF-16-Zeichenkette als Einzelbyte-Zeichenkette gelesen stoppt am ersten Null-Byte, das bei jedem ASCII-Druckernamen die obere Hälfte des zweiten Zeichens ist. Jeder Druckername kam mit genau einem Zeichen zurück. Downstream scheiterte die Namensvalidierung, die Druckererzeugung und das Drucken für jeden echten Drucker auf der Maschine, und keines dieser Symptome zeigt auf eine Strukturdeklaration
// Falsch: richtige Größe, falscher Elementtyp. Kein Compile-Fehler,
// kein Absturz, jede Zeichenkette auf ein Zeichen verkürzt
type
TPrinterInfo2Wrong = record
pServerName: PAnsiChar;
pPrinterName: PAnsiChar;
// ... zehn weitere
end;
// Richtig: eine *W-Struktur hat durchgehend Wide-Member
type
TPrinterInfo2W = record
pServerName: PWideChar;
pPrinterName: PWideChar;
// ... zehn weitere
end;
Die Regel, die daraus entsteht, ist mechanisch, und es lohnt sich, sie bedingungslos anzuwenden: Bei jeder Win32-Struktur, deren Name auf W endet, prüfen, dass jedes String-Member die Wide-Variante ist, Feld für Feld. Die Vermischung der ANSI- und der Wide-Welt erzeugt weder eine Compilerdiagnose noch einen Absturz, nur stille Verkürzung, und umgekehrt gilt dasselbe für die ANSI-Varianten
Ein nackter Exception-Handler ist der eigentliche Gegner
Jede dieser Untersuchungen wurde vom selben Konstrukt ausgebremst: ein Handler, der alles fängt und in einen false-Rückgabewert umwandelt. Um einen Bilddecoder herum ist das vernünftig zu schreiben, denn ein korruptes Bild soll keinen Dokumentjob abwerfen. Es ist zugleich eine Vorrichtung, um genau die eine Information zu löschen, die Sie brauchen
Die praktische Antwort ist, den Handler vorübergehend laut zu machen. Das Ausgeben von Exceptionklasse, Meldung und Backtrace aus dem nackten Handler heraus, unter einer Debug-Condition, verwandelt eine unerklärliche Null-Rückgabe in eine benannte Exception mit einem Ort. In zwei der drei Fälle oben beendete dieser eine Schritt die Untersuchung, denn die Exception war eine Division durch null oder eine Access Violation in einer Stellvertreter-Methode, deren Name alles sagte
Checkliste für die Annahme eines Stellvertreterwegs
Vier Punkte, in der Reihenfolge ihres Ertrags. Bevor Sie eine Ersatzklasse aufrufen, lesen Sie die Methoden, die Sie gleich verwenden, und bestätigen Sie, dass jeder einen echten Rumpf hat; ein leerer Rumpf ist kein Implementierungsdetail, sondern ein fehlendes Feature. Ziehen Sie Stellvertreter, die auslösen, solchen vor, die neutrale Werte zurückgeben, und paaren Sie das mit Null-Prüfungen an den Stellen, an denen eine Fabrik nun legitim nichts zurückgeben kann. Verifizieren Sie ein Feature, indem Sie das Artefakt inspizieren, nicht den Rückgabecode, denn der ganze Fehlermodus hier ist ein sauberer Rückgabecode über leerem Artefakt; eine Byte-level-Aufschlüsselung dessen, was ein Dokument tatsächlich enthält, ist der schnellste Weg, das zu sehen, und der Artikel zum Dateigrößen-Audit behandelt dieses Werkzeug. Und wenn ein Feature keine brauchbare Ersatzimplementierung hat, routen Sie die betroffenen Beispiele auf den Weg, der funktioniert, und sagen in einem Kommentar warum, statt eine Demonstration zurückzulassen, die still leere Ausgabe produziert
Der allgemeinere Punkt gilt weit über eine Bibliothek hinaus. Jede Codebasis mit einer conditionalen zweiten Implementierung, einer Mock-Schicht, einem Headless-Modus, einem Plattform-Shim ist Form drei ausgesetzt. Der Grund, warum sie sich so gut versteckt: Jedes Qualitätstor, auf das sich ein Team normalerweise verlässt – Rückgabecodes, Fehlercodes, Exceptions, Exit-Status – ist ein Statuskanal, und Form drei hält alle davon sauber. Nur die Ausgabe verrät es. Das ist auch die Begründung dafür, bei nicht vertrauenswürdiger Eingabe Artefakte statt Status zu prüfen, beschrieben in dem Artikel zum nicht vertrauenswürdigen PDF-Parsing, und dafür, gerenderte Ausgabe über Engines hinweg zu vergleichen, statt einer zu vertrauen, beschrieben in Multi-Engine-Rendering
PDFlibPas ist eine native Object-Pascal-PDF-Bibliothek für Delphi, C++Builder und Free Pascal, und seine Nicht-VCL-Konfiguration ist es, die Headless- und Cross-Toolchain-Builds möglich macht; die aktuelle Konfigurationsabdeckung listet die Produktseite der losLab PDF Developer Library