Teknisk artikkel

Diagnostisere lydløse stub-feil i et Pascal PDF-bibliotek

Når et Delphi-bibliotek får en byggkonfigurasjon uten det visuelle rammeverket, er erstatningsklassene der feilene bor. Ikke plattformen, ikke kompilatoren: stand-in-ene. PDFlibPas har et grafikklag som leverer bitmap-, canvas-, font-, metafil- og skriverekvivalenter for bygg uten VCL, og porteringen av det til Free Pascal brakte fram hver feilmodus en stand-in kan ha. De sorterer pent etter diagnosekostnad, og rekkefølgen er det motsatte av hva intuisjonen antyder

En stand-in som kaster unntak er billig å finne; unntaket navngir metoden. En stand-in som returnerer tomme data er dyr, fordi feilen viser seg flere lag unna sin årsak. En stand-in som returnerer suksess er det verste av alle, fordi returkoden er gyldig, feilkoden er null, intet unntak kastes, og det eneste beviset på at noe gikk galt, ligger i bytene som kom ut

Tre former for lydløs stub-feil i et Pascal PDF-bibliotek rangert etter diagnosekostnad, fra en kastende stub til suksess over tom output
En kastende stand-in er billig å diagnostisere, tomme data er dyre, og en suksessretur over et tomt artefakt er det vanskeligste å finne

Form tre: en gyldig bildeidentifikator over et tomt XObject

Vektormetafilkonvertereren var en tom prosedyrekropp i ikke-VCL-konfigurasjonen. Alt over den fortsatte å fungere. EMF-importinngangspunktene og canvas-fangst-inngangspunktet kjørte til fullføring og returnerte en gyldig bildeidentifikator, som kalleren deretter plasserte på en side. Det som landet i filen, var et form XObject med innholdslengde null. Siden rendret hvit

Ingenting rapporterte et problem, og det inkluderer bibliotekets eget demonstrasjonsprogram for denne funksjonen, som tegnet en blank side og ikke la merke til det. Det fantes ingen feilende returverdi å sjekke, fordi sekvensen av kall genuint alle lyktes; det eneste som var feil, var størrelsen på den produserte strømmen. Å diagnostisere denne klassen av defekter betyr å stille et annet spørsmål: ikke «feilet kallet», men «er artefaktet plausibelt». Et form XObject med lengde null, et bilde med null piksler, en side med null innholdsbyte, det er påstandene som fanger det

Fiksen har to halvdeler, og den andre halvdelen er lett å glemme. For det første, la den tomme implementeringen kaste, slik at feilen i det hele tatt har en kanal. For det andre, konverter det unntaket til et nullresultat ved bildefabrikken og legg til nullsjekker ved de to stedene som konsumerer en bildeidentifikator, for ellers blir «ren feil» rett inn i et tilgangsbrudd når sidetreet derefererer ingenting. En stub som kaster, er bare en forbedring hvis kallerne var forberedt på en feil de aldri tidligere hadde kunnet motta

Form to: tomme data, tre lag fra krasjet

Metafil-canvas stand-in-en fylte ikke ut sine fysiske dimensjoner. Den verdien divideres inn i en sidegeometriberegning, så beregningen produserte null, så bbox-beregningen delte på null. En naken unntakshåndterer svelget det, bildefabrikken returnerte et nullresultat, og tilgangsbruddet skjedde til slutt i sidetreet da nullen ble brukt. Tre lag mellom årsak og symptom, med en unntakshåndterer i midten som visker ut beviset

Feilkjeden til en tom metafil-canvas stand-in i PDFlibPas som når et tilgangsbrudd tre lag etter divisjonen på null
Den tomme dimensjonen deler en geometriberegning til null, en naken håndterer visker ut unntaket, og nullidentifikatoren krasjer sidetreet

Samme enhet hadde to instanser til av mønsteret. Fontklassen hadde tomme Assign- og konstruktørkropper, noe som betyr mer enn det ser ut som fordi canvasens fontegenskap er skrivebeskyttet: å tildele inn i den er den eneste måten å levere en font på, så en tom implementering gjør fontvalget lydløst virkningsløst og teksten kommer ut i hva som enn var standarden. Og en piksler-per-tommeverdi på null fikk hver kaller som dimensjonerer en canvas fra fontmetrikker til å produsere en null-ganger-null-canvas, noe som gir en blank side og en suksessretur

// Formen å lete etter i en stand-in-enhet: en metode som verken
// kaster eller gjør noe. Begge disse kompilerer, og begge
// produserer «suksess» uten output
procedure TMetafileCanvasStandIn.Create(...);
begin
  // intet inherited-kall, ingen feltinitialisering
end;

function TBitmapStandIn.LoadFromStream(Stream: TStream): Boolean;
begin
  Result := True;    // og bitmapen er fortsatt tom
end;

Brede strukturen som beholder bare det første tegnet

Denne er i det hele tatt ikke et stand-in-problem, men den hører hjemme i samme katalog fordi symptomet er like langt fra årsaken. Skriverenummeringsstrukturen ble deklarert med alle tolv av sine strengmedlemmer typet som pekere til én-byte-tegn, mens funksjonen som fyller den, er bredtegnvarianten av enummerings-API-en

Pekerstørrelsene er identiske, så strukturoppsettet er korrekt og ingenting krasjer. Det som skjer i stedet, er at lesing av en UTF-16-streng som en én-bytestreng stopper ved den første null-byten, noe som for ethvert ASCII-skrivernavn er den høye halvdelen av det andre tegnet. Hvert skrivernavn kom tilbake som nøyaktig ett tegn. Nedstrøms feilet navnvalidering, skriveroppretting feilet og utskrift feilet for hver ekte skriver på maskinen, og intet av de symptomene peker på en strukturdeklarasjon

UTF-16-skrivernavn avkortet til ett tegn etter at en bred Win32-struktur ble deklarert med PAnsiChar-medlemmer i stedet for PWideChar
Én-byte-medlemmer leser et UTF-16-navn bare til sin første null-byte, så hvert skrivernavn kommer tilbake med nøyaktig ett tegn
// Feil: riktig størrelse, feil elementtype. Ingen kompileringsfeil,
// intet krasj, hver streng avkortet til ett tegn
type
  TPrinterInfo2Wrong = record
    pServerName: PAnsiChar;
    pPrinterName: PAnsiChar;
    // ... ti flere
  end;

// Riktig: en *W-struktur har brede medlemmer gjennomgående
type
  TPrinterInfo2W = record
    pServerName: PWideChar;
    pPrinterName: PWideChar;
    // ... ti flere
  end;

Regelen som kommer ut av det, er mekanisk og verdt å anvende uten å tenke: for enhver Win32-struktur hvis navn slutter på W, verifiser at hvert strengmedlem er den brede varianten, felt for felt. Å blande ANSI- og bredverdenen produserer verken en kompilatordiagnostikk eller et krasj, bare lydløs avkorting, og det samme gjelder omvendt for ANSI-variantene

En naken unntakshåndterer er den egentlige motstanderen

Hver av disse undersøkelsene ble tregere av den samme konstruksjonen: en håndterer som fanger alt og konverterer det til en falsk returverdi. Det er fornuftig å skrive rundt en bildeavkoder, siden et korrupt bilde ikke bør ta ned en dokumentjobb. Det er også et redskap for å slette den eneste informasjonen du trenger

Det praktiske svaret er å gjøre håndtereren midlertidig høylytt. Å dumpe unntaksklassen, meldingen og backtracen fra innsiden av den nakne håndtereren, under en debug-betingelse, konverterer en uforklarlig nullretur til et navngitt unntak med en plassering. I to av de tre tilfellene over avsluttet det enkelte steget undersøkelsen, fordi unntaket var en divisjon på null eller et tilgangsbrudd i en stand-in-metode hvis navn sa alt

Sjekkliste for å ta i bruk en stand-in-vei

Fire punkter, i den rekkefølgen de betaler seg. Før du kaller inn i en erstatningsklasse, les metodene du er i ferd med å bruke, og bekreft at hver har en ekte kropp; en tom kropp er ikke en implementeringsdetalj, den er en manglende funksjon. Foretrekk stand-ins som kaster fremfor stand-ins som returnerer nøytrale verdier, og par det med nullsjekker på stedene en fabrikk nå legitimt kan returnere ingenting. Verifiser en funksjon ved å inspisere artefaktet, ikke returkoden, siden hele feilmodusen her er en ren returkode over et tomt artefakt; en bytenivå-nedbrytning av hva et dokument faktisk inneholder, er den raskeste måten å se det på, og artikkelen om filstørrelsesrevisjon dekker det verktøyet. Og når en funksjon ikke har noen levedyktig erstatningsimplementering, rutt de berørte eksemplene til veien som fungerer, og si hvorfor i en kommentar, i stedet for å etterlate en demonstrasjon som lydløst produserer tom output

Det bredere poenget gjelder langt utover ett bibliotek. Enhver kodebase med en betinget annen implementering, et mock-lag, en headless-modus, en plattformshim, er utsatt for form tre. Grunnen til at den gjemmer seg så godt, er at hver kvalitetsport et team normalt stoler på, returkoder, feilkoder, unntak, avslutningsstatuser, er en statuskanal, og form tre holder alle rene. Bare outputen forråder den. Det er også resonneringen bak å sjekke artefakter snarere enn statuser ved håndtering av ikke-tiltrudt input, beskrevet i artikkelen om ikke-tiltrudt PDF-parsing, og bak å sammenligne rendret output på tvers av motorer i stedet for å stole på én, beskrevet i multimotor-rendering

PDFlibPas er et nativt Object Pascal PDF-bibliotek for Delphi, C++Builder og Free Pascal, og dets ikke-VCL-konfigurasjon er det som gjør headless- og tvers-verktøykjede-bygg mulig; nåværende konfigurasjonsdekning er oppført på produktsiden for losLab PDF Developer Library