Quando una libreria Delphi acquisisce una configurazione di build senza il framework visuale, le classi sostitutive sono dove vivono i bug. Non la piattaforma, non il compilatore: le classi segnaposto. PDFlibPas ha uno strato grafico che fornisce equivalenti di bitmap, canvas, font, metafile e stampante per le build senza la VCL, e portarlo su Free Pascal ha fatto emergere ogni modalità di guasto che un segnaposto può avere. Si ordinano ordinatamente per costo di diagnosi, e l'ordine è l'opposto di quanto l'intuizione suggerisca
Un segnaposto che genera eccezioni è economico da trovare; l'eccezione nomina il metodo. Un segnaposto che restituisce dati vuoti è costoso, perché il guasto compare a diversi strati di distanza dalla sua causa. Un segnaposto che restituisce successo è il peggiore di tutti, perché il codice di ritorno è valido, il codice di errore è zero, nessuna eccezione viene generata, e l'unica prova che qualcosa sia andato storto sta nei byte prodotti
Forma tre: un identificatore di immagine valido su un XObject vuoto
Il convertitore vettoriale di metafile era un corpo di procedura vuoto nella configurazione non VCL. Tutto ciò che sta sopra continuava a funzionare. I punti di ingresso di importazione EMF e il punto di ingresso di cattura della canvas giravano fino in fondo e restituivano un identificatore di immagine legale, che il chiamante poi collocava su una pagina. Ciò che finiva nel file era un form XObject con lunghezza del contenuto pari a zero. La pagina veniva resa bianca
Nulla segnalava un problema, e questo include il programma dimostrativo della libreria stessa per questa funzione, che disegnava una pagina bianca senza accorgersene. Non c'era alcun valore di ritorno fallito da controllare, perché la sequenza di chiamate riusciva davvero tutta; l'unica cosa sbagliata era la dimensione dello stream prodotto. Diagnosticare questa classe di difetti significa porsi una domanda diversa: non "la chiamata è fallita" ma "l'artefatto è plausibile". Un form XObject di lunghezza zero, un'immagine di zero pixel, una pagina di zero byte di contenuto: queste sono le asserzioni che lo colgono
La correzione ha due metà e la seconda metà è facile da dimenticare. Primo, fare in modo che l'implementazione vuota generi eccezioni, così che il guasto abbia almeno un canale. Secondo, convertire quell'eccezione in un risultato nullo alla fabbrica di immagini e aggiungere controlli nulli nei due punti che consumano un identificatore di immagine, perché altrimenti il "guasto pulito" diventa direttamente una access violation mentre l'albero di pagine dereferenzia il nulla. Uno stub che lancia eccezioni è un miglioramento solo se i chiamanti erano preparati a un guasto che prima non avevano mai potuto ricevere
Forma due: dati vuoti, a tre strati dal crash
Il segnaposto della canvas metafile non compilava le proprie dimensioni fisiche. Quel valore entra in divisione in un calcolo della geometria di pagina, quindi il calcolo produceva zero, quindi il calcolo del bounding box divideva per zero. Un gestore di eccezioni nudo lo assorbiva, la fabbrica di immagini restituiva un risultato nullo, e l'access violation avveniva finalmente nell'albero di pagine quando il null veniva usato. Tre strati tra causa e sintomo, con un gestore di eccezioni in mezzo a cancellare le prove
La stessa unità aveva altri due casi del modello. La classe font aveva corpi vuoti di Assign e del costruttore, il che conta più di quanto sembri perché la proprietà font della canvas è di sola lettura: assegnarci dentro è l'unico modo di consegnare un font, così un'implementazione vuota rende la selezione del font silenziosamente inefficace e il testo esce in qualunque cosa fosse il default. E un valore pixel per pollice di zero faceva sì che ogni chiamante che dimensiona una canvas dalle metriche del font producesse una canvas zero per zero, il che genera una pagina bianca e un ritorno di successo
// La forma da cercare in un'unità segnaposto: un metodo che non
// genera eccezioni e non fa nulla. Entrambi compilano ed entrambi
// producono "successo" senza output
procedure TMetafileCanvasStandIn.Create(...);
begin
// nessuna chiamata inherited, nessuna inizializzazione di campi
end;
function TBitmapStandIn.LoadFromStream(Stream: TStream): Boolean;
begin
Result := True; // e la bitmap resta vuota
end;
La struttura wide che conserva solo il primo carattere
Questo non è affatto un problema di segnaposto, ma appartiene allo stesso catalogo perché il sintomo è altrettanto lontano dalla causa. La struttura di enumerazione delle stampanti era dichiarata con tutti e dodici i suoi membri stringa tipizzati come puntatori a caratteri a un byte, mentre la funzione che la riempie è la variante a caratteri wide dell'API di enumerazione
Le dimensioni dei puntatori sono identiche, quindi il layout della struttura è corretto e nulla va in crash. Ciò che accade invece è che leggere una stringa UTF-16 come stringa a un byte si ferma al primo byte zero, che per qualsiasi nome stampante ASCII è la metà alta del secondo carattere. Ogni nome stampante tornava come esattamente un carattere. A valle, la validazione del nome falliva, la creazione della stampante falliva e la stampa falliva per ogni stampante reale sulla macchina, e nessuno di quei sintomi punta a una dichiarazione di struttura
// Sbagliato: dimensione giusta, tipo di elemento sbagliato. Nessun
// errore di compilazione, nessun crash, ogni stringa troncata a un carattere
type
TPrinterInfo2Wrong = record
pServerName: PAnsiChar;
pPrinterName: PAnsiChar;
// ... altri dieci
end;
// Giusto: una struttura *W ha membri wide ovunque
type
TPrinterInfo2W = record
pServerName: PWideChar;
pPrinterName: PWideChar;
// ... altri dieci
end;
La regola che ne esce è meccanica e vale la pena applicarla senza pensarci: per qualsiasi struttura Win32 il cui nome termina in W, verificare che ogni membro stringa sia la variante wide, campo per campo. Mescolare il mondo ANSI e quello wide non produce né un diagnostico del compilatore né un crash, solo troncamento silenzioso, e lo stesso vale al contrario per le varianti ANSI
Un gestore di eccezioni nudo è il vero avversario
Ognuna di queste indagini è stata rallentata dallo stesso costrutto: un gestore che cattura tutto e lo converte in un valore di ritorno falso. È una cosa ragionevole da scrivere attorno a un decoder di immagini, poiché un'immagine corrotta non dovrebbe far cadere un lavoro sui documenti. È anche un congegno per cancellare l'unico dato di cui si ha bisogno
La risposta pratica è rendere il gestore temporaneamente rumoroso. Scaricare la classe dell'eccezione, il messaggio e il backtrace dall'interno del gestore nudo, sotto una condizionale di debug, converte un ritorno null inspiegabile in un'eccezione nominata con una posizione. In due dei tre casi sopra quel singolo passo ha chiuso l'indagine, perché l'eccezione era una divisione per zero o una access violation in un metodo segnaposto il cui nome diceva tutto
Lista di controllo per adottare una via segnaposto
Quattro punti, nell'ordine in cui ripagano. Prima di chiamare una classe sostitutiva, leggere i metodi che si stanno per usare e confermare che ognuno abbia un corpo reale; un corpo vuoto non è un dettaglio implementativo, è una funzione mancante. Preferire i segnaposto che generano eccezioni ai segnaposto che restituiscono valori neutri, e abbinare a ciò controlli nulli nei punti in cui una fabbrica ora può legittimamente non restituire nulla. Verificare una funzione ispezionando l'artefatto, non il codice di ritorno, dato che l'intera modalità di guasto qui è un codice di ritorno pulito su un artefatto vuoto; una disamina a livello di byte di ciò che un documento contiene davvero è il modo più rapido per vederlo, e l'articolo sull'audit della dimensione dei file copre quel tipo di strumento. E quando una funzione non ha un'implementazione sostitutiva praticabile, incanalare i campioni interessati verso la via che funziona e dire il perché in un commento, invece di lasciare una dimostrazione che produce silenziosamente output bianco
Il punto più ampio vale ben oltre una libreria. Qualsiasi codebase con una seconda implementazione condizionale, uno strato mock, una modalità headless, uno shim di piattaforma, è esposta alla forma tre. La ragione per cui si nasconde così bene è che ogni gate di qualità su cui un team normalmente conta — codici di ritorno, codici di errore, eccezioni, stati di uscita — è un canale di stato, e la forma tre li tiene tutti puliti. Solo l'output lo tradisce. È anche il ragionamento dietro il controllo degli artefatti piuttosto che degli stati quando si gestisce input non fidato, descritto nell'articolo sul parsing di PDF non fidati, e dietro il confronto dell'output renderizzato tra motori invece di fidarsi di uno solo, descritto in rendering multi-motore
PDFlibPas è una libreria PDF nativa in Object Pascal per Delphi, C++Builder e Free Pascal, e la sua configurazione non VCL è ciò che rende possibili build headless e cross-toolchain; l'attuale copertura delle configurazioni è elencata nella pagina di prodotto di losLab PDF Developer Library