Gdy biblioteka Delphi zyskuje konfigurację buildu bez wizualnego frameworka, klasy zastępcze są miejscem, gdzie mieszkają błędy. Nie platforma, nie kompilator: zamienniki. PDFlibPas ma warstwę graficzną dostarczającą odpowiedników bitmapy, canvasu, fontu, metafila i drukarki dla buildów bez VCL, a portowanie jej na Free Pascal wyniosło na powierzchnię każdy tryb awarii, jaki zamiennik może mieć. Sortują się elegancko wg kosztu diagnozy, a kolejność jest odwrotna do tej, którą podpowiada intuicja
Zamiennik, który zgłasza wyjątek, jest tani w znalezieniu; wyjątek wymienia metodę. Zamiennik zwracający puste dane jest drogi, bo awaria pojawia się kilka warstw od swojej przyczyny. Zamiennik zwracający sukces jest najgorszy ze wszystkich, bo kod zwrotny jest poprawny, kod błędu zerowy, żaden wyjątek nie pada, a jedyne poszlaki, że coś poszło nie tak, leżą w bajtach, które wyszły
Postać trzecia: poprawny identyfikator obrazu nad pustym XObject
Wektorowy konwerter metafili był pustym ciałem procedury w konfiguracji bez VCL. Wszystko nad nim dalej działało. Punkty wejścia importu EMF i punkt wejścia przechwytywania canvasu dochodziły do końca i zwracały legalny identyfikator obrazu, który wywołujący potem umieszczał na stronie. To, co wylądowało w pliku, to form XObject o długości zawartości zero. Strona renderowała się na biało
Nic nie zgłaszało problemu, włącznie z własnym programem demonstracyjnym biblioteki dla tej funkcji, który narysował pustą stronę i tego nie zauważył. Nie było niespełnionego kodu zwrotnego do sprawdzenia, bo sekwencja wywołań naprawdę w całości się powiodła; jedyną rzeczą, która była zła, był rozmiar wyprodukowanego strumienia. Diagnozowanie tej klasy defektów znaczy zadawanie innego pytania: nie „czy wywołanie zawiodło”, tylko „czy artefakt jest wiarygodny”. Form XObject o długości zero, obraz o zerze pikseli, strona o zerze bajtów zawartości — to są asercje, które to łapią
Poprawka ma dwie połówki i drugą łatwo zapomnieć. Po pierwsze, spraw, by pusta implementacja zgłaszała wyjątek, żeby awaria w ogóle miała kanał. Po drugie, przekonwertuj ten wyjątek na wynik null w fabryce obrazów i dodaj sprawdzenia null w dwóch miejscach konsumujących identyfikator obrazu, bo inaczej „czysta awaria” przechodzi prosto w naruszenie dostępu, gdy drzewo stron wyłuskuje nicość. Stub rzucający jest ulepszeniem tylko wtedy, gdy wywołujący byli przygotowani na awarię, której wcześniej nigdy nie mogli dostać
Postać druga: puste dane, trzy warstwy od awarii
Zamiennik canvasu metafila nie wypełniał swoich wymiarów fizycznych. Ta wartość dzieli się do obliczenia geometrii strony, więc obliczenie dało zero, więc obliczenie ramki ograniczającej dzieliło przez zero. Goły handler wyjątków to połknął, fabryka obrazów zwróciła wynik null, a naruszenie dostępu w końcu wydarzyło się w drzewie stron, gdy null został użyty. Trzy warstwy między przyczyną a objawem, z handlerem wyjątków pośrodku, wymazującym dowody
Ta sama jednostka miała dwa kolejne wystąpienia wzorca. Klasa fontu miała puste ciała Assign i konstruktora, co znaczy więcej, niż wygląda, bo własność fontu canvasu jest tylko do odczytu: przypisanie do niej to jedyny sposób dostarczenia fontu, więc pusta implementacja czyni wybór fontu po cichu nieskutecznym, a tekst wychodzi w tym, co było domyślne. A wartość pikseli na cal równa zero sprawiała, że każdy wywołujący wymiarujący canvas z metryk fontu produkował canvas zero na zero, co daje pustą stronę i zwrot sukcesu
// Kształt, którego szukać w jednostce zamiennika: metoda, która ani
// nie zgłasza wyjątku, ani nic nie robi. Obie te metody się kompilują
// i obie produkują „sukces” bez żadnego wyjścia
procedure TMetafileCanvasStandIn.Create(...);
begin
// ani wywołania inherited, ani inicjalizacji pól
end;
function TBitmapStandIn.LoadFromStream(Stream: TStream): Boolean;
begin
Result := True; // a bitmapa wciąż jest pusta
end;
Szeroka struktura, która zachowuje tylko pierwszy znak
To w ogóle nie jest problem zamiennika, ale należy do tego samego katalogu, bo objaw jest równie daleko od przyczyny. Struktura enumeracji drukarek została zadeklarowana z wszystkimi dwunastoma polami tekstowymi wpisanymi jako wskaźniki na znaki jednobajtowe, podczas gdy funkcja, która ją wypełnia, jest wariantem szerokoznakowym API enumeracji
Rozmiary wskaźników są identyczne, więc układ struktury jest poprawny i nic nie pada. Zamiast tego czytanie łańcucha UTF-16 jako łańcucha jednobajtowego zatrzymuje się na pierwszym bajcie zerowym, który dla dowolnej drukarkowej nazwy ASCII jest górną połówką drugiego znaku. Każda nazwa drukarki wracała jako dokładnie jeden znak. W dół strumienia walidacja nazwy zawodziła, tworzenie drukarki zawodziło i drukowanie zawodziło dla każdej prawdziwej drukarki w maszynie, a żaden z tych objawów nie wskazuje na deklarację struktury
// Źle: właściwy rozmiar, zły typ elementu. Żadnego błędu kompilacji,
// żadnej awarii, każdy łańcuch skrócony do jednego znaku
type
TPrinterInfo2Wrong = record
pServerName: PAnsiChar;
pPrinterName: PAnsiChar;
// ... dziesięć dalszych
end;
// Dobrze: struktura *W ma szerokie pola na całej długości
type
TPrinterInfo2W = record
pServerName: PWideChar;
pPrinterName: PWideChar;
// ... dziesięć dalszych
end;
Reguła, która z tego wynika, jest mechaniczna i warta stosowania bez myślenia: dla każdej struktury Win32, której nazwa kończy się na W, zweryfikuj, że każdy człon tekstowy jest wariantem szerokim, pole po polu. Mieszanie świata ANSI i szerokiego nie produkuje ani diagnostyki kompilatora, ani awarii, tylko ciche skrócenie, i to samo działa w odwrotną stronę dla wariantów ANSI
Goły handler wyjątków to prawdziwy przeciwnik
Każde z tych śledztw było spowalniane przez tę samą konstrukcję: handler łapiący wszystko i konwertujący to na zwrot false. To rozsądna rzecz wokół dekodera obrazów, bo uszkodzony obraz nie powinien zawalać zadania dokumentowego. To również przyrząd do wymazywania tej jednej informacji, której potrzebujesz
Praktyczną reakcją jest czasowe zrobienie z handlera hałaśliwego. Zrzucenie klasy wyjątku, komunikatu i śladu stosu z wnętrza gołego handlera, pod warunkiem debugowym, przekształca niewyjaśniony zwrot null w nazwany wyjątek z lokalizacją. W dwóch z trzech powyższych przypadków ten jeden krok zakończył śledztwo, bo wyjątkiem było dzielenie przez zero albo naruszenie dostępu w metodzie zamiennika, której nazwa mówiła wszystko
Checklista adoptowania ścieżki zamiennika
Cztery punkty, w kolejności, w jakiej się opłacają. Zanim wywołasz klasę zastępczą, przeczytaj metody, których zamierzasz użyć, i potwierdź, że każda ma realne ciało; puste ciało to nie szczegół implementacji, to brakująca funkcja. Preferuj zamienniki zgłaszające wyjątki nad zamiennikami zwracającymi neutralne wartości i sparuj to ze sprawdzeniami null w miejscach, gdzie fabryka może teraz legalnie nie zwrócić nic. Weryfikuj funkcję badając artefakt, nie kod zwrotny, bo cały tryb awarii tutaj to czysty kod zwrotny nad pustym artefaktem; bajtowy rozbior tego, co dokument faktycznie zawiera, to najszybszy sposób, by to zobaczyć, a artykuł o audycie rozmiaru pliku omawia to narzędzie. I gdy funkcja nie ma żywotnej implementacji zastępczej, kieruj dotknięte próbki ścieżką, która działa, i napisz dlaczego w komentarzu, zamiast zostawiać demonstrację, która po cichu produkuje puste wyjście
Szerszy punkt stosuje się daleko poza jedną biblioteką. Dowolna baza kodu z warunkową drugą implementacją, warstwą mocków, trybem headless, platformowym shimem jest wystawiona na postać trzecią. Przyczyna, dla której tak dobrze się kryje, jest taka, że każda brama jakości, na którą zespół normalnie liczy — kody zwrotne, kody błędów, wyjątki, statusy wyjścia — jest kanałem statusów, a postać trzecia utrzymuje je wszystkie czystymi. Tylko wyjście ją zdradza. To jest też rozumowanie stojące za sprawdzaniem artefaktów zamiast statusów przy obsłudze niezaufanego wejścia, opisane w artykule o parsowaniu niezaufanego PDF, i za porównywaniem wyrenderowanego wyjścia między silnikami zamiast ufania jednemu, opisane w renderowaniu wielosilnikowym
PDFlibPas to natywna biblioteka PDF w Object Pascalu dla Delphi, C++Builder i Free Pascala, a jej konfiguracja bez VCL to właśnie to, co umożliwia buildy headless i między-toolchainowe; aktualne pokrycie konfiguracji jest wypisane na stronie produktu losLab PDF Developer Library