Articolo tecnico

HotXLS su Free Pascal: Unicode, slot COM e zlib

HotXLS compila sotto Free Pascal e Lazarus su Windows, e il porting si è giocato su quattro decisioni che non c'entrano niente con la sintassi Object Pascal: tenere il core in modalità DELPHIUNICODE, dichiarare le interfacce structured-storage OLE come interfacce CORBA con reference counting gestito a mano, sostituire i file oggetto AES Win32 con un'implementazione Pascal, e sistemare un ciclo inflate che poteva accettare uno ZIP troncato come completo

Chi ha mai portato una libreria Delphi matura conosce la forma di questo lavoro. Il compilatore accetta quasi tutto alla prima passata. Poi arriva una lunga coda di differenze comportamentali che compilano pulite e producono risultati sbagliati, e un motore di fogli di calcolo è insolitamente esposto perché tocca codifica del testo, structured storage COM, compressione e crittografia in un unico percorso di codice

Perché il core insiste su DELPHIUNICODE invece del semplice DELPHI?

Perché il motore delle formule dipende dal fatto che String e Char portino semantiche UTF-16, e l'alternativa ANSI perde caratteri prima che qualcosa arrivi al file. È tentante compilare il core in modalità FPC DELPHI, visto che è lo switch di compatibilità a cui la maggior parte dei porting tende, e il codice compila. Poi un workbook con nomi di foglio cinesi o etichette cirilliche fa un round-trip attraverso il percorso di calcolo e i caratteri sono perduti quando il writer li vede, senza alcun errore

La modalità non è uniforme nella libreria, e quello è deliberato e non disordine. Il decoder di byte PNG e gli override LCL hanno genuinamente bisogno di firme ANSI, perché trattano in byte e in ciò che il widgetset loro consegna. Quelle unit abilitano uno switch separato LX_FPC_ANSI. Due modalità in una libreria suona come uno smell finché non noti che l'alternativa è un decoder di byte che tratta il proprio input come testo

C'è un dettaglio compagno che becca la gente più tardi. DELPHIUNICODE non rende TFormatSettings.DecimalSeparator un WideChar nel runtime FPC. L'input che porta un separatore decimale Unicode va normalizzato a un separatore ASCII dentro la stringa Unicode prima, e qualunque input il cui separatore non corrisponde a quello atteso va rifiutato invece che troncato in silenzio al carattere che il parser non ha riconosciuto

program ExportReport;
{$MODE DELPHI}
uses
  Interfaces,          // deve venire per primo: inizializza il widgetset LCL
  SysUtils, lxHandle;  // e il layer di conversione UTF-8

var
  Book: TXLSWorkbook;
begin
  Book := TXLSWorkbook.Create(nil);
  try
    Book.LoadFromFile('input.xls');
    Book.Sheets[0].AsString[1, 1] := 'Quarterly summary';
    Book.SaveToFile('output.xls');
  finally
    Book.Free;
  end;
end.

L'unit Interfaces non è opzionale e deve venire per prima. È ciò che inizializza il widgetset LCL e il layer di conversione UTF-8, e HotXLS conta su entrambi appena font, percorsi file o testo attraversano il confine tra RTL e LCL. Un programma console che la salta compilerà e si comporterà male su qualsiasi percorso non ASCII. Questo è anche il motivo per cui una compilazione riuscita prova così poco qui: il porting era dimostrabilmente funzionante solo una volta che documenti veri con veri nomi di font e veri percorsi hanno fatto un round-trip completo

La VMT di una classe non è una vtable COM

Free Pascal non ti lascia consegnare una VMT di classe a Windows come vtable di interfaccia COM, anche quando la dichiarazione sembra identica a quella che Delphi accetta. I layout differiscono in modi che producono una chiamata nello slot sbagliato, che si manifesta come un crash da qualche parte non correlata al call site. Lo structured storage conta qui perché il classico formato binario dei workbook è un file composto OLE, e leggerlo o scriverlo significa implementare ILockBytes in cui l'API di storage di Windows richiamerà

L'assetto che funziona è un'interfaccia CORBA con gli slot COM dichiarati esplicitamente e AddRef e Release gestiti a mano. Significa rinunciare al reference counting automatico per questi tipi e prendersi la responsabilità del ciclo di vita, che è un compromesso equo per una manciata di interfacce che vivono dentro una sola unit. La trappola specifica dentro quel lavoro è QueryInterface: deve restituire un puntatore a interfaccia, non il puntatore all'oggetto. Entrambi compilano. Uno dei due consegna a Windows un indirizzo la cui prima parola macchina non è una vtable

Diagramma che confronta la VMT di classe Free Pascal con la vtable di interfaccia COM che HotXLS deve presentare all'API di structured storage di Windows: ordini di slot diversi per la stessa dichiarazione Pascal, più la trappola QueryInterface dove restituire il puntatore all'oggetto invece di un puntatore a interfaccia manda una chiamata ILockBytes in uno slot di classe e crasha lontano dal call site
Free Pascal rifiuta di servire una VMT di classe come vtable COM, quindi HotXLS dichiara interfacce CORBA con slot COM espliciti e AddRef e Release gestiti a mano, e QueryInterface restituisce il puntatore a interfaccia che Windows può dereferenziare

Le dichiarazioni specifiche FPC stanno in lxOleInterfaces.inc, accanto a lxAESBackend.inc e lxZlibBackend.inc nella directory sorgente FPC, così le scelte specifiche del compilatore stanno in un posto solo invece di essere sparse nel motore. Il formato in sé e come lo naviga la libreria sono descritti in leggere file composti OLE2 in Pascal

Un altro dettaglio di tipo appartiene alla stessa famiglia. LargeInt deve risolversi in Int64 nel ramo FPC, e la classificazione del compilatore di Comp differisce abbastanza tra le due toolchain che la risoluzione degli overload può scegliere un candidato diverso. Testa il comportamento con offset grandi su un file stream invece che su uno stream HGLOBAL: lo stream di memoria globale di Windows fa da solo il wrap-around sui seek oltre 4 GiB, quindi un test che lì passa non prova niente sulla tua aritmetica

Cosa nasconde un'implementazione AES auto-coerente

I file oggetto AES Win32 che la build Delphi linka sono OMF, e il linker Free Pascal non li può consumare, quindi il ramo FPC usa un'implementazione AES in Pascal. Delphi continua a linkare i file oggetto di sempre, il che tiene il binario rilasciato invariato per i clienti esistenti

Il requisito di verifica è la parte che vale la pena portarsi in qualsiasi progetto. Cifrare dati e decifrarli di nuovo con la stessa implementazione non prova assolutamente niente: un algoritmo simmetrico con uno schedule di chiave sbagliato, un ordine di blocchi sbagliato o un chaining sbagliato è perfettamente auto-coerente e rifà il round-trip del proprio output ogni volta. Solo i vettori known-answer lo beccano, controllando l'espansione della chiave, l'ordine dei blocchi e il chaining CBC contro valori pubblicati. Spedisci un'implementazione sbagliata ma auto-coerente e il sintomo compare la prima volta che un cliente apre il file in Excel

La compressione aveva un difetto di carattere diverso. Un backend inflate in Pascal può avere ancora output in sospeso dopo aver consumato tutto il proprio input compresso, quindi il chiamante deve continuare a chiamare finché lo stream non riporta la sua fine. Trattare l'input esaurito come fine dello stream tronca l'ultimo blocco. Peggio, trasforma un archivio danneggiato in uno accettato in silenzio, che è esattamente la modalità di guasto che il rafforzamento in validazione del record ZIP end-of-central-directory esiste per prevenire. La regola è che nessun progresso più non finito è un errore di troncamento, mai un EOF

Due trappole del build system che costano ore vere

I percorsi di ricerca LCL devono precedere i percorsi wildcard dei package FPC, altrimenti l'unit Menus di Free Vision fa ombra all'unit LCL omonima e ottieni un mismatch di checksum PPU che non dice nulla né dell'uno né dell'altra. Un'installazione di Lazarus spostata dopo l'installazione può anche lasciare percorsi stantii in fpc.cfg, quindi i punti di ingresso della build specificano percorsi di unit e binari esplicitamente invece di ereditare ciò che l'ambiente offre

La seconda trappola non c'entra niente con Pascal. Un file batch .cmd scritto con fine riga LF funziona finché il file cresce oltre la dimensione del buffer di lettura dell'interprete, a quel punto call :label fallisce sostenendo che l'etichetta batch non esiste, e il fallimento compare nel programma che per caso sta oltre il confine. Qualsiasi strumento che riscrive uno script batch deve riscrivere CRLF. E lazbuild --build-all svuota la directory di output delle unit dei package prima di compilare, quindi un file di opzioni parcheggiato in quella directory viene cancellato prima di poter essere letto: tienilo fuori, e ricorda che il percorso @ si risolve relativamente alla directory del package perché lazbuild invoca il compilatore da lì

Mappa dei due strati di pericolo dietro una compilazione Free Pascal di HotXLS pulita: la modalità DELPHIUNICODE che tiene String e Char in UTF-16, la via di fuga LX_FPC_ANSI per il decoder di byte PNG e gli override LCL, e le trappole di build dall'ombra di Menus di Free Vision, ai percorsi stantii di fpc.cfg, ai file batch solo LF e allo svuotamento dell'output di lazbuild
Una prima compilazione prova poco: la mappa delle modalità decide quali caratteri sopravvivono fino al writer, mentre le trappole del build system emergono come mismatch di checksum, etichette fantasma mancanti e file di opzioni cancellati prima di essere letti
// Export di griglia Lazarus: TGridToXLS è incluso nel package Lazarus, così
// lo stesso codice di export DB-grid funziona in un'applicazione LCL
var
  Exporter: TGridToXLS;
begin
  Exporter := TGridToXLS.Create(nil);
  try
    Exporter.DBGrid := GridOrders;
    Exporter.WorksheetName := 'Orders';
    Exporter.ExportHeader := True;
    Exporter.SetColumnsWidth := True;
    Exporter.ExportDBGrid;
    Exporter.SaveAs('orders.xls');
  finally
    Exporter.Free;
  end;
end;

Quanto vale un warning del compilatore

Free Pascal riporta le variabili locali non inizializzate che Delphi non riporta, e girare la build FPC ha trasformato quella differenza in due difetti veri nell'unit di calcolo. Una funzione leggeva una variabile contatore mai assegnata prima dell'uso, e un'altra usava due coordinate in un ramo prima che il codice che le calcolava girasse in un ramo diverso. Sotto Delphi entrambe si comportavano secondo ciò che lo stack per caso conteneva, che è la definizione di un bug che si riproduce su una macchina e non su un'altra

La conclusione pratica è che il secondo compilatore vale la pena tenerlo nel ciclo anche per un prodotto che viene spedito principalmente sul primo. Scandire periodicamente le classi di warning FPC è una passata di analisi statica a basso costo su una codebase Delphi, e trova una categoria di difetti che nessuna suite di test raggiunge in modo affidabile. La più ampia disciplina della matrice di versioni in cui questo sta è descritta in la matrice di build cross-compiler

Il supporto di Free Pascal e Lazarus per Windows viene spedito con il HotXLS Delphi spreadsheet component come package Lazarus accanto ai package Delphi e C++Builder, compilato dallo stesso albero sorgente invece che da un fork. Questo è il punto dell'esercizio: un motore, quattro toolchain, e le decisioni specifiche del compilatore isolate in file include leggibili in una seduta