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
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
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
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