Odborný článok

Diagnostika tichých zlyhaní stubov v PDF knižnici v Pascale

Keď knižnica pre Delphi získa zostavovaciu konfiguráciu bez vizuálneho frameworku, náhradné triedy sú miestom, kde bývajú chyby. Nie platforma, nie kompilátor: náhradníci. PDFlibPas má grafickú vrstvu, ktorá poskytuje ekvivalenty bitmapy, canvasu, fontu, metafilu a tlačiarne pre buildy bez VCL, a jej prenos do Free Pascalu vyniesol na povrch každý režim zlyhania, ktorý náhradník môže mať. Triedia sa prehľadne podľa ceny diagnostiky a toto poradie je opakom toho, čo naznačuje intuícia

Náhradník, ktorý vyhadzuje výnimky, je lacný na nájdenie; výnimka menuje metódu. Náhradník, ktorý vracia prázdne dáta, je drahý, pretože zlyhanie sa objaví niekoľko vrstiev od svojej príčiny. Náhradník, ktorý vracia úspech, je najhorší zo všetkých, pretože návratový kód je platný, chybový kód je nula, žiadna výnimka sa nezvýši a jediným dôkazom, že niečo sa pokazilo, sú bajty, ktoré vyšli

Tri tvary tichého zlyhania stubu v pascaleskej PDF knižnici zoradené podľa ceny diagnostiky, od stubu vyhadzujúceho výnimky po úspech nad prázdnym výstupom
Náhradník vyhadzujúci výnimky je lacný na diagnostiku, prázdne dáta sú drahé a návrat úspechu nad prázdnym artefaktom je najhorší na nájdenie

Tvar tri: platný identifikátor obrazu nad prázdnym XObject

Vektorový konvertor metafilu bolo prázdne telo procedúry v konfigurácii non-VCL. Všetko nad ním fungovalo ďalej. Vstupné body importu EMF a vstupný bod zachytávania canvasu bežali do konca a vrátili legálny identifikátor obrazu, ktorý volajúci potom umiestnil na stránku. Do súboru pristál form XObject s dĺžkou obsahu nula. Stránka sa vyrenderovala biela

Nič nehlásilo problém a to zahŕňa aj vlastnú demonštračnú aplikáciu knižnice pre túto funkciu, ktorá nakreslila prázdnu stránku a nevšimla si to. Nebola tu žiadna zlyhávajúca návratová hodnota na skontrolovanie, pretože sekvencia volaní naozaj všetky uspela; jediné, čo bolo zlé, bola veľkosť vyrobeného streamu. Diagnostikovanie tejto triedy defektov znamená položiť si inú otázku: nie „zlyhalo volanie“, ale „je artefakt plauzibilný“. Form XObject s dĺžkou nula, obraz s nulou pixelov, stránka s nulou bajtov obsahu — toto sú asercie, ktoré to zachytia

Oprava má dve polovice a druhá polovica sa ľahko zabúda. Najprv prinúťte prázdnu implementáciu vyhadzovať výnimky, aby zlyhanie vôbec malo kanál. Potom preveďte tú výnimku na null výsledok vo fabrike obrazov a pridajte kontroly null na dvoch miestach, ktoré konzumujú identifikátor obrazu, pretože inak sa „čisté zlyhanie“ rovno zmení na access violation, keď strom stránok dereferencuje nič. Stub, ktorý hádže výnimky, je zlepšením len vtedy, keď volajúci boli pripravení na zlyhanie, ktoré predtým nikdy neboli schopní prijať

Tvar dva: prázdne dáta, tri vrstvy od pádu

Náhradník metafilového canvasu nevyplnil svoje fyzické rozmery. Tá hodnota delí do výpočtu geometrie stránky, takže výpočet vyprodukoval nulu a výpočet ohraničujúceho rámca delil nulou. Holý handler výnimiek to prehltil, fabriky obrazov vrátila null výsledok a access violation sa konečne stal v strome stránok, keď sa null použil. Tri vrstvy medzi príčinou a príznakom, s handlerom výnimiek uprostred, ktorý maže dôkazy

Reťaz zlyhania prázdneho náhradníka metafilového canvasu v PDFlibPas, ktorý dosiahne access violation tri vrstvy po delení nulou
Prázdny rozmer delí výpočet geometrie na nulu, holý handler maže výnimku a null identifikátor zrúti strom stránok

Tá istá jednotka mala ďalšie dva prípady tohto vzoru. Trieda fontu mala prázdne telá Assign a konštruktora, čo je dôležitejšie, než vyzerá, pretože vlastnosť fontu canvasu je read-only: priradenie do nej je jediný spôsob, ako doručiť font, takže prázdna implementácia robí výber fontu potichu neúčinným a text vychádza v tom, čo bolo predvolené. A hodnota pixels-per-inch nula spôsobila, že každý volajúci, ktorý dimenzuje canvas z metrík fontu, vyprodukoval canvas nula krát nula, čo dáva prázdnu stránku a návrat úspechu

// Tvar, ktorý treba hľadať v jednotke náhradníka: metóda, ktorá ani
// nevyhadzuje výnimky, ani nič nespraví. Obe sa skompilujú a obe
// produkujú „úspech“ bez výstupu
procedure TMetafileCanvasStandIn.Create(...);
begin
  // žiadne volanie inherited, žiadna inicializácia polí
end;

function TBitmapStandIn.LoadFromStream(Stream: TStream): Boolean;
begin
  Result := True;    // a bitmapa je stále prázdna
end;

Široká štruktúra, ktorá si ponechá len prvý znak

Toto vôbec nie je problém náhradníka, ale patrí do rovnakého katalógu, pretože príznak je rovnako ďaleko od príčiny. Štruktúra enumerácie tlačiarní bola deklarovaná so všetkými dvanástimi reťazcovými členmi typovanými ako ukazovatele na jednobajtové znaky, zatiaľ čo funkcia, ktorá ju vyplňuje, je širokoznaková varianta enumeráčného API

Veľkosti ukazovateľov sú identické, takže rozloženie štruktúry je správne a nič sa nezrúti. Namiesto toho čítanie UTF-16 reťazca ako jednobajtového reťazca sa zastaví na prvom nulovom bajte, ktorý je pre akékoľvek ASCII meno tlačiarne hornou polovicou druhého znaku. Každé meno tlačiarne sa vrátilo presne ako jeden znak. Po prúde zlyhala validácia mien, vytváranie tlačiarní a tlač pre každú skutočnú tlačiareň na stroji a ani jeden z týchto príznakov neukazuje na deklaráciu štruktúry

Meno tlačiarne UTF-16 skrátené na jeden znak po deklarovaní širokej Win32 štruktúry s členmi PAnsiChar namiesto PWideChar
Jednobajtové členy čítajú meno UTF-16 len po jeho prvý nulový bajt, takže každé meno tlačiarne sa vráti presne s jedným znakom
// Zle: správna veľkosť, nesprávny typ prvku. Žiadna chyba kompilácie,
// žiadny pád, každý reťazec skrátený na jeden znak
type
  TPrinterInfo2Wrong = record
    pServerName: PAnsiChar;
    pPrinterName: PAnsiChar;
    // ... ďalších desať
  end;

// Správne: štruktúra *W má široké členy od začiatku do konca
type
  TPrinterInfo2W = record
    pServerName: PWideChar;
    pPrinterName: PWideChar;
    // ... ďalších desať
  end;

Pravidlo, ktoré z toho vychádza, je mechanické a stojí za uplatnenie bez premýšľania: pri každej Win32 štruktúre, ktorej meno končí na W, overte, že každý reťazcový člen je široká varianta, pole po poli. Miešanie svetov ANSI a wide neprináša ani kompilátorovú diagnostiku, ani pád, len tiché skracovanie a to isté platí naopak pre varianty ANSI

Holý handler výnimiek je skutočným protivníkom

Každé z týchto šetrení spomalila tá istá konštrukcia: handler, ktorý chytí všetko a prevedie to na návratovú hodnotu false. Je to rozumná vec napísať okolo dekódera obrazov, keďže poškodený obraz nemá zrútiť dokumentovú úlohu. Je to aj zariadenie na zmazanie tej jedinej informácie, ktorú potrebujete

Praktická reakcia je spraviť handler dočasne hlučným. Vypísanie triedy výnimky, správy a backtrace zvnútra holého handlera pod debug podmienkou premení nevysvetlený null návrat na pomenovanú výnimku s lokalitou. V dvoch z troch prípadov vyššie tento jediný krok šetrenie ukončil, pretože výnimka bola delenie nulou alebo access violation v metóde náhradníka, ktorej meno povedalo všetko

Kontrolný zoznam pri osvojovaní cesty náhradníka

Štyri položky, v poradí, v akom sa vyplácajú. Pred volaním do náhradnej triedy si prečítajte metódy, ktoré sa chystáte použiť, a potvrďte, že každá má reálne telo; prázdne telo nie je detail implementácie, je to chýbajúca funkcia. Uprednostnite náhradníkov, ktorí vyhadzujú výnimky, pred náhradníkmi vracajúcimi neutrálne hodnoty, a spojte to s kontrolami null na miestach, kde teraz fabrika môže legitímne nevrátiť nič. Overujte funkciu skúmaním artefaktu, nie návratového kódu, keďže celý režim zlyhania tu je čistý návratový kód nad prázdnym artefaktom; rozklad na úrovni bajtov toho, čo dokument naozaj obsahuje, je najrýchlejší spôsob, ako to vidieť, a článok o audite veľkosti súboru pokrýva toto náradie. A keď funkcia nemá životaschopnú náhradnú implementáciu, smerujte dotknuté vzorky na cestu, ktorá funguje, a povedzte prečo v komentári, namiesto ponechania demonštrácie, ktorá potichu produkuje prázdny výstup

Širší bod platí ďaleko za hranicami jednej knižnice. Každá kódová báza s podmienenou druhou implementáciou, mock vrstvou, headless režimom či platformovým shimom je vystavená tvaru tri. Dôvod, prečo sa tak dobre skrýva, je, že každá brána kvality, na ktorú sa tím zvyčajne spolieha — návratové kódy, chybové kódy, výnimky, exit statusy — je kanál statusov a tvar tri ich všetky udržuje čisté. Zradí ho len výstup. To je aj uvažovanie za kontrolou artefaktov namiesto statusov pri spracovaní nedôveryhodného vstupu, popísané v článku o parsovaní nedôveryhodných PDF, a za porovnávaním vyrenderovaného výstupu naprieč enginmi namiesto dôvery jednému, popísané v renderovaní s viacerými enginmi

PDFlibPas je natívna PDF knižnica v Object Pascale pre Delphi, C++Builder a Free Pascal a jej konfigurácia non-VCL je to, čo umožňuje headless a cross-toolchain buildy; aktuálne pokrytie konfigurácií je uvedené na produktovej stránke losLab PDF Developer Library