Technisch artikel

Stille stub-fouten in een Pascal-PDF-bibliotheek vinden

Wanneer een Delphi-bibliotheek een buildconfiguratie krijgt zonder het visuele framework, zijn de vervangende klassen de plek waar de bugs huizen. Niet het platform, niet de compiler: de stand-ins. PDFlibPas heeft een graphics-laag die equivalenten voor bitmap, canvas, font, metafile en printer levert voor builds zonder de VCL, en de portering naar Free Pascal bracht elke faalmodus aan het licht die een stand-in kan hebben. Ze sorteren netjes op diagnostische kosten, en de volgorde is precies tegengesteld aan wat intuïtie suggereert

Een stand-in die een exceptie gooit is goedkoop te vinden; de exception benoemt de methode. Een stand-in die lege data teruggeeft is duur, omdat de fout enkele lagen van zijn oorzaak verwijderd verschijnt. Een stand-in die succes teruggeeft is het ergste van alles, want de returncode is geldig, de foutcode is nul, er wordt geen exception gegooid, en het enige bewijs dat er iets misging zit in de bytes die eruit kwamen

Drie vormen van stille stub-fout in een Pascal PDF-bibliotheek gerangschikt op diagnostische kosten, van een stub die gooit naar succes boven lege uitvoer
Een gooiende stand-in is goedkoop te diagnosticeren, lege data is duur, en een succesvolle return boven een leeg artefact is het lastigst te vinden

Vorm drie: een geldige beeldidentifier boven een leeg XObject

De vector-metafileconverter was een lege procedurebody in de non-VCL-configuratie. Alles erboven bleef werken. De EMF-importtoegangspunten en het toegangspunt voor canvas-vastlegging liepen tot het einde en gaven een geldige beeldidentifier terug, die de aanroeper daarna op een pagina plaatste. Wat in het bestand belandde, was een form XObject met een contentlengte van nul. De pagina renderde wit

Niets meldde een probleem, en dat geldt ook voor het eigen demoprogramma van de bibliotheek voor deze functie, dat een lege pagina tekende en het niet merkte. Er was geen falende returnwaarde om te controleren, want de reeks aanroepen slaagde echt allemaal; het enige dat fout was, was de grootte van de geproduceerde stream. Deze klasse defecten diagnosticeren betekent een andere vraag stellen: niet "is de aanroep gefaald" maar "is het artefact plausibel". Een form XObject met lengte nul, een afbeelding van nul pixels, een pagina van nul contentbytes, dat zijn de asserties die het vangen

De oplossing heeft twee helften en de tweede helft is makkelijk te vergeten. Ten eerste: laat de lege implementatie een exceptie gooien, zodat de fout überhaupt een kanaal heeft. Ten tweede: zet die exception bij de beeldfabriek om in een null-resultaat en voeg null-controles toe op de twee plaatsen die een beeldidentifier consumeren, want anders verandert "nette fout" rechtstreeks in een access violation doordat de paginaboom een lege verwijzing probeert te derefereren. Een stub die gooit is alleen een verbetering als de aanroepers voorbereid waren op een fout die ze eerder nooit konden ontvangen

Vorm twee: lege data, drie lagen van de crash

De metafile-canvasstand-in vulde zijn fysieke afmetingen niet in. Die waarde deelt mee in een paginageometrieberekening, dus de berekening produceerde nul, dus de bounding-boxberekening deelde door nul. Een kale exception-handler slikte dat in, de beeldfabriek gaf een null-resultaat terug, en de access violation gebeurde uiteindelijk in de paginaboom toen de null werd gebruikt. Drie lagen tussen oorzaak en symptoom, met een exception-handler in het midden die het bewijs uitwist

Faalketen van een lege metafile-canvasstand-in in PDFlibPas die drie lagen na de deling door nul op een access violation uitkomt
De lege afmeting drukt een geometrieberekening naar nul, een kale handler wist de exception uit en de null-identifier laat de paginaboom crashen

Dezelfde unit had nog twee gevallen van het patroon. De fontklasse had lege Assign- en constructorbodies, wat erger is dan het lijkt omdat de fontproperty van de canvas alleen-lezen is: erin toewijzen is de enige manier om een font af te leveren, dus een lege implementatie maakt fontkeuze geruisloos onwerkzaam en de tekst komt eruit in wat de standaard ook was. En een pixels-per-inch-waarde van nul liet elke aanroeper die een canvas op fontmetrieken dimensioneert een canvas van nul bij nul produceren, wat een lege pagina en een succesvolle return oplevert

// De vorm om te zoeken in een stand-in-unit: een methode die niet
// gooit en niets doet. Beide compileren en beide leveren "succes"
// zonder uitvoer
procedure TMetafileCanvasStandIn.Create(...);
begin
  // geen inherited-aanroep, geen veldinitialisatie
end;

function TBitmapStandIn.LoadFromStream(Stream: TStream): Boolean;
begin
  Result := True;    // en de bitmap is nog steeds leeg
end;

De brede structuur die alleen het eerste teken bewaart

Dit is helemaal geen stand-inprobleem, maar het hoort in dezelfde catalogus omdat het symptoom even ver van de oorzaak zit. De printer-enumeratiestructuur was gedeclareerd met alle twaalf stringleden getypeerd als pointers naar single-byte tekens, terwijl de functie die die vult de wide-character-variant van de enumeratie-API is

Pointergroottes zijn identiek, dus de structuurlay-out is correct en er crasht niets. Wat er in plaats daarvan gebeurt, is dat het lezen van een UTF-16-string als single-byte-string stopt bij de eerste nulbyte, wat bij elke ASCII-printernaam de hoge helft van het tweede teken is. Elke printernaam kwam terug als precies één teken. Stroomafwaarts faalde naamvalidatie, faalde printercreatie en faalde het afdrukken voor elke echte printer op de machine, en geen van die symptomen wijst naar een structuurdeclaratie

UTF-16-printernaam afgekapt tot één teken nadat een brede Win32-structuur met PAnsiChar-leden in plaats van PWideChar is gedeclareerd
Single-byte-leden lezen een UTF-16-naam slechts tot de eerste nulbyte, dus elke printernaam komt terug met precies één teken
// Fout: juiste grootte, verkeerde elementtype. Geen compileerfout, geen crash,
// elke string afgekapt tot één teken
type
  TPrinterInfo2Wrong = record
    pServerName: PAnsiChar;
    pPrinterName: PAnsiChar;
    // ... nog tien meer
  end;

// Goed: een *W-structuur heeft overal brede leden
type
  TPrinterInfo2W = record
    pServerName: PWideChar;
    pPrinterName: PWideChar;
    // ... nog tien meer
  end;

De regel die daaruit volgt is mechanisch en het toepassen waard zonder na te denken: controleer bij elke Win32-structuur waarvan de naam op W eindigt, veld voor veld, of elk stringlid de brede variant is. De ANSI- en de brede wereld door elkaar mengen levert geen compilerdiagnose op en geen crash, alleen geruisloze afkapping, en hetzelfde geldt omgekeerd voor de ANSI-varianten

Een kale exception-handler is de werkelijke tegenstander

Elk van deze onderzoeken werd vertraagd door dezelfde constructie: een handler die alles vangt en omzet in een false-returnwaarde. Het is een redelijk iets om rond een beelddecoder te schrijven, want een corrupte afbeelding mag een documenttaak niet laten vallen. Het is tegelijk een apparaat om precies dat ene stukje informatie te wissen dat u nodig heeft

De praktische reactie is de handler tijdelijk luid te maken. Vanuit de kale handler, onder een debug-conditional, de exceptionklasse, het bericht en de backtrace dumpen zet een onverklaarde null-return om in een benoemde exception met een locatie. In twee van de drie gevallen hierboven beëindigde die ene stap het onderzoek, want de exception was een deling door nul of een access violation in een stand-in-methode waarvan de naam alles zei

Checklist voor het aannemen van een stand-in-pad

Vier punten, in de volgorde waarin ze renderen. Lees voordat u een vervangende klasse aanroept de methoden die u op het punt staat te gebruiken en bevestig dat elk een echte body heeft; een lege body is geen implementatiedetail, het is een ontbrekende functie. Geef de voorkeur aan stand-ins die gooien boven stand-ins die neutrale waarden teruggeven, en koppel daaraan null-controles op de plaatsen waar een fabriek nu terecht niets kan teruggeven. Verifieer een functie door het artefact te inspecteren, niet de returncode, want de hele faalmodus hier is een nette returncode boven een leeg artefact; een bytegewijze uitsplitsing van wat een document werkelijk bevat is de snelste manier om dat te zien, en het artikel over de bestandsgrootte-audit behandelt die tooling. En wanneer een functie geen levensvatbare vervangende implementatie heeft, stuur de betreffende voorbeelden dan naar het pad dat wel werkt en zeg waarom in een comment, in plaats van een demonstratie achter te laten die geruisloos lege uitvoer produceert

De bredere punt geldt ver buiten één bibliotheek. Elke codebase met een conditionele tweede implementatie, een mocklaag, een headless modus, een platformshim, is blootgesteld aan vorm drie. De reden dat die zich zo goed verbergt, is dat elke kwaliteitspoort waarop een team normaal vertrouwt, returncodes, foutcodes, exceptions, exit-statussen, een statuskanaal is, en vorm drie houdt ze allemaal schoon. Alleen de uitvoer verraadt het. Dat is ook de redenering achter het controleren van artefacten in plaats van statussen bij het verwerken van onbetrouwbare input, beschreven in het artikel over het parsen van onbetrouwbare PDFs, en achter het vergelijken van gerenderde uitvoer tussen engines in plaats van er één te vertrouwen, beschreven in multi-engine rendering

PDFlibPas is een native Object Pascal PDF-bibliotheek voor Delphi, C++Builder en Free Pascal, en juist de non-VCL-configuratie maakt headless en cross-toolchain-builds mogelijk; de huidige configuratiedekking staat op de productpagina van de losLab PDF Developer Library