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