Articolo tecnico

HotPDF su Free Pascal: deflate, AES e limiti dei codec

HotPDF compila e gira sotto Free Pascal 3.2.2 con Lazarus, e il riassunto onesto di quel porting sono due frasi. Creazione di documenti, caricamento, salvataggio, compressione, decompressione, cifratura e decifratura funzionano tutti su backend solo Pascal, così un'applicazione Lazarus può produrre e consumare vero PDF senza alcuna dipendenza C. I codec immagine nativi opzionali no, perché gli oggetti Win64 precompilati usano una variante COFF che nessun linker di Free Pascal può consumare, quindi su quella toolchain i punti di ingresso si risolvono in stub che falliscono chiudendosi

Mappa delle capacità di HotPDF su Free Pascal: backend deflate, AES e documenti in Pascal funzionanti accanto a stub di codec immagine che falliscono chiudendosi
Le funzioni di documento, compressione e cifratura girano su backend solo Pascal, mentre i codec immagine nativi si risolvono in stub che falliscono chiudendosi

Passare da "compila" a "funziona" ha richiesto un insieme specifico di correzioni, e ognuna è una trappola che troverà qualsiasi altra codebase Delphi che si sposti su Free Pascal. Vale la pena scriverle nell'ordine in cui fanno male

Perché un'unità che compila non prova nulla?

Perché un'unità Pascal può riferire un simbolo che non farà mai nulla di utile e soddisfare comunque il compilatore. Al punto in cui tutte le 113 unità della libreria compilavano pulite sotto Free Pascal, i gestori dei contenitori archivio funzionavano davvero, verificati da uno smoke test che apriva un CBZ e lo convertiva in PDF. L'appiattimento dei moduli XFA non funzionava affatto, perché appiattire richiede di gonfiare lo stream di pacchetti compresso /XFA e il punto di ingresso deflate era ancora uno stub. Nulla nell'output di build distingueva i due casi

La regola che ne è uscita è breve. Prima di scrivere in una release note che una funzione funziona su una nuova toolchain, scrivere una sonda a runtime che eserciti la funzione da capo a fondo su quella toolchain. La copertura di compilazione è un prerequisito, mai una prova. Il quadro più ampio di ciò che il porting copre è nelle note di supporto Free Pascal e Lazarus Win64

Una raise dentro uno stub cdecl non arriva al chiamante

Questa merita una sezione propria perché il sintomo è così fuorviante. Le unità stub espongono punti di ingresso C come farebbe una libreria statica, quindi uno stub appare così

// Sembra ragionevole. Non lo è.
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
  public name 'inflate';
begin
  raise ENotSupportedException.Create('codec unavailable');
end;

Su Free Pascal per Win64 quell'eccezione non si propaga al chiamante. Non c'è gestore try..except che la veda, perché lo svolgimento attraverso un confine cdecl dichiarato in questo modo non trasporta il frame di eccezione Pascal; il processo termina con exit code 217. Dal lato applicazione non c'è errore, né messaggio, né riga di log, solo un programma che scompare. È strettamente peggio di una risposta sbagliata, perché una risposta sbagliata si può gestire

Perché un'eccezione generata dentro uno stub cdecl termina un processo Free Pascal con exit code 217 e come il gating del punto di ingresso Pascal lo risolve
Il frame di eccezione Pascal non può svolgersi attraverso il confine cdecl, quindi il processo muore in silenzio; la correzione interviene con il gating prima che lo stub sia raggiunto

La correzione allettante è far restituire allo stub un codice di fallimento, e per inflate è giusto perché zlib ha un ritorno di errore ben definito. È sbagliato in generale: uno stub per jpeg_read_header che restituisce zero dice al chiamante di proseguire con una struttura che nessuno ha inizializzato. La correzione durevole è fare il gating al punto di ingresso Pascal piuttosto che dentro lo stub a forma C, usando la convenzione di fallimento che quell'API già ha

function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
  // Rifiutare prima che lo stub sia mai raggiunto, con la convenzione
  // di fallimento di questa API piuttosto che un'eccezione attraverso cdecl
  Bitmap := nil;
  Result := False;
  Exit;
{$ENDIF}
  Result := DecodeJPEGNative(Data, Bitmap);
end;

paszlib non è zlib, e la differenza sono due classi di documenti

L'implementazione Pascal del deflate disponibile su Free Pascal gestisce due incorniciature: il wrapper zlib e il deflate raw. Non gestisce l'incorniciatura gzip, che zlib seleziona con valori di windowBits da 16 a 31, e non gestisce la modalità di rilevamento automatico selezionata dai valori da 32 a 47. HotPDF ha bisogno di entrambe. La via sicura di importazione SVG chiede 31, e il loader ha una scala di fallback che chiede 47 quando l'incorniciatura di uno stream è ambigua. Saltarne una e un'intera famiglia di documenti smette di aprirsi, con un errore di decodifica che punta allo stream anziché all'incorniciatura mancante

Copertura windowBits di paszlib rispetto a zlib: intervalli gzip e auto-detect mancanti per l'importazione SVG di HotPDF e il fallback del loader
paszlib gestisce il wrapper zlib e il deflate raw, ma HotPDF richiede anche windowBits 31 e 47, quindi lo shim deve fornire da sé l'incorniciatura gzip

C'è una seconda incompatibilità più tagliente. Il record z_stream che paszlib dichiara non ha lo stesso layout di memoria di quello C: il suo campo msg è una short string anziché un puntatore, e total_in e total_out sono a 64 bit dove l'ABI C ha parole macchina. Un record del chiamante quindi non può essere passato dritto. L'assetto che funziona è tenere lo stato paszlib dietro il puntatore state che il record pubblico riserva già, e copiare i campi pubblici dentro e fuori attorno a ogni chiamata. Il CRC gzip e il trailer di lunghezza a otto byte sono contabilizzati nello stesso strato shim, che è il posto naturale per loro poiché è già proprietario della decisione di incorniciatura

Passare un array dinamico a un parametro var senza tipo

Questo è il bug con più probabilità di trovarsi proprio ora nel codice di Lei. Quando si passa un array dinamico a un parametro var senza tipo, ciò che il chiamato riceve è l'indirizzo della variabile array, che è l'indirizzo di un puntatore, non l'indirizzo del payload. Quindi una lettura dentro di esso sovrascrive la variabile stessa e tutto ciò che le sta accanto

var
  FBuffer: TBytes;
begin
  SetLength(FBuffer, 65536);

  // Sbagliato: passa l'indirizzo della variabile FBuffer
  FStream.Read(FBuffer, Length(FBuffer));

  // Giusto: passa l'indirizzo del primo byte del payload
  FStream.Read(FBuffer[0], Length(FBuffer));
end;

Su Delphi la forma sbagliata spesso sembra funzionare, perché ciò che corrompe è uno slot di stack adiacente che nulla legge dopo. Su Free Pascal la stessa linea va in segmentation fault al primo uso. Ciò che rende difficile individuarla a occhio è che gli array statici non hanno questo problema, poiché una variabile array statico è il proprio payload, quindi entrambe le forme sono corrette nello stesso file a seconda della dichiarazione qualche centinaio di righe più in là

Contenitori ZIP senza System.Zip

Free Pascal non ha un equivalente dell'unità zip della RTL, e l'alternativa disponibile ha sia una superficie API diversa sia nessun supporto per la cifratura legacy che i formati contenitore più vecchi usano ancora, così un piccolo lettore interno alla libreria è risultato più corto che adattarsi a quella. Due dettagli di formato costano tempo e sono facili da sbagliare

Il primo è il byte di controllo dell'header di cifratura. Il suo dodicesimo byte è normalmente il byte alto del CRC, ma quando il bit 3 del flag general-purpose è impostato, a significare che le dimensioni vivono in un data descriptor finale e il CRC non è ancora noto, il byte di controllo viene dal byte alto dell'ora di modifica. Implementando solo la forma CRC ogni archivio scritto in modalità streaming rifiuta una password corretta. Il secondo è il campo extra ZIP64: i suoi tre campi a 64 bit compaiono in un ordine fisso ma sono scritti solo quando il corrispondente campo a 32 bit è saturo, quindi leggerli a offset fissi funziona sugli archivi testati e fallisce sul successivo. Interpretarli posizionalmente in base a quali campi a 32 bit sono saturi

Una comodità che vale la pena conoscere: lo stream di decompressione di Free Pascal accetta un secondo argomento del costruttore che salta l'header zlib, che è esattamente ciò che le voci ZIP richiedono poiché memorizzano deflate raw. Quella via non tocca affatto lo shim zlib della libreria, quindi non è toccata dal backend C mancante

Trasparenza dei glifi a colori sotto la LCL

Leggere il canale alfa di un glifo a colori rasterizzato è l'unico dettaglio grafico senza traduzione diretta. La classe PNG della LCL non ha alcun accessore di scanline che esponga l'alfa, e assegnare un PNG a una bitmap lo scarta, così un emoji a colori arriva completamente opaco e si compone con un riquadro nero dietro. La via che funziona è l'immagine dell'interfaccia: crearla dal PNG, poi leggere i pixel attraverso l'accessore di colore, ricordando che i suoi componenti sono a 16 bit e richiedono uno spostamento a destra di otto per diventare byte. Quella superficie usa anche l'ordine naturale delle righe dall'alto verso il basso, quindi l'inversione Height - 1 - Y di cui il codice scanline VCL ha bisogno va rimossa anziché portata

Due note sul sistema di build prima di aprire un bug

Una ricostruzione completa a volte fallisce con un simbolo non definito il cui nome termina con un suffisso $crc e un valore esadecimale. Quel suffisso è calcolato dai tipi dei parametri, e non corrisponde quando una build compila un'unità contro due versioni diverse dell'interfaccia nella stessa passata. Rieseguire la build lo chiarisce; la firma non è sbagliata

Secondo, Free Pascal 3.2.2 non ha metodi anonimi, così ovunque la libreria usava closure per cablare una pipeline parallela la build Free Pascal prende un fallback seriale deterministico. L'output è identico, il throughput no; se si dipende dal rendering parallelo delle pagine, questo è un motivo per restare su Delphi per il momento, e il progetto della pipeline è descritto nell'articolo sulla pipeline di rendering parallela. La situazione dei codec immagine è l'altro posto in cui la scelta della toolchain cambia capacità e non solo velocità, quindi un deployment Lazarus dovrebbe pianificare i propri formati immagine di conseguenza; l'attuale matrice per toolchain è sulla pagina di prodotto di HotPDF Delphi PDF component