HotXLS se compilează sub Free Pascal și Lazarus pe Windows, iar portarea s-a jucat pe patru decizii care n-au nimic a face cu sintaxa Object Pascal: țineți nucleul în modul DELPHIUNICODE, declarați interfețele de structured-storage OLE ca interfețe CORBA cu counting de referințe gestionat de mână, înlocuiți fișierele obiect AES Win32 cu o implementare în Pascal și reparați o buclă inflate care putea accepta un ZIP trunchiat drept complet
Oricine a portat o bibliotecă Delphi matură cunoaște forma acestei munci. Compilatorul acceptă aproape tot la prima trecere. Urmează o coadă lungă de diferențe comportamentale care se compilează curat și produc rezultate greșite, iar un motor de foi de calcul e neobișnuit de expus la ele, pentru că atinge encodarea textului, structured storage COM, compresia și criptografia într-o singură cale de cod
De ce insistă nucleul pe DELPHIUNICODE, nu pe simplul DELPHI?
Pentru că motorul de formule depinde de String și Char cu semantică UTF-16, iar alternativa ANSI pierde caractere înainte ca ceva să ajungă în fișier. E tentant să compilezi nucleul în modul FPC DELPHI, fiindcă e comutatorul de compatibilitate la care ajung majoritatea portărilor, iar codul se compilează. Apoi un registru de lucru cu nume de foi chinezești sau etichete chirilice face un du-te-vino prin calea de calcul, iar caracterele au dispărut până când scriitorul le vede, fără nicio eroare nicăieri
Modul nu e uniform în toată biblioteca, iar asta e deliberat, nu dezordonat. Decoder-ul de octeți PNG și override-urile LCL chiar au nevoie de semnături ANSI, pentru că lucrează cu octeți și cu ce le dă widgetset-ul. Acele unități activează un comutator separat LX_FPC_ANSI. Două moduri într-o bibliotecă sună a miros până observi că alternativa e un decoder de octeți care își tratează intrarea ca pe text
Există un detaliu însoțitor care prinde oamenii mai târziu. DELPHIUNICODE nu face TFormatSettings.DecimalSeparator un WideChar în runtime-ul FPC. O intrare care poartă un separator zecimal Unicode trebuie normalizată la un separator ASCII în interiorul șirului Unicode mai întâi, iar orice intrare al cărei separator nu se potrivește cu cel așteptat trebuie respinsă, nu trunchiată pe furiș la caracterul pe care parser-ul nu l-a recunoscut
program ExportReport;
{$MODE DELPHI}
uses
Interfaces, // trebuie să vină primul: inițializează widgetset-ul LCL
SysUtils, lxHandle; // și stratul de conversie 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.
Unitatea Interfaces nu e opțională și trebuie să vină prima. Ea inițializează widgetset-ul LCL și stratul de conversie UTF-8, iar HotXLS se bazează pe ambele imediat ce fonturi, căi de fișiere sau text traversează granița RTL-LCL. Un program de consolă care o sări se compilează și se poartă rău pe orice cale non-ASCII. Acesta e și motivul pentru care o compilație reușită dovedește atât de puțin aici: portarea a fost demonstrabil funcțională doar odată ce documente reale, cu nume de fonturi reale și căi reale, au făcut un du-te-vino complet
Un VMT de clasă nu e un vtable COM
Free Pascal nu vă lasă să predați un VMT de clasă către Windows drept vtable de interfață COM, nici măcar când declarația arată identic cu cea pe care Delphi o acceptă. Aranjamentele diferă în feluri care produc un apel către slotul greșit, manifestat ca un crash undeva fără legătură cu locul apelului. Structured storage contează aici pentru că formatul clasic binar de registru de lucru e un fișier compus OLE, iar citirea sau scrierea unuia înseamnă a implementa ILockBytes în care API-ul de stocare Windows va apela înapoi
Aranjamentul care funcționează e o interfață CORBA cu sloturile COM declarate explicit și AddRef și Release gestionate de mână. Înseamnă a renunța la counting-ul automat de referințe pentru aceste tipuri și a-ți asuma responsabilitatea ciclului de viață, un schimb drept pentru o mână de interfețe care trăiesc într-o singură unitate. Capcana specifică din munca aceea e QueryInterface: trebuie să întoarcă un pointer de interfață, nu pointerul obiectului. Ambele se compilează. Una dintre ele îi dă Windowsului o adresă al cărei prim cuvânt de mașină nu e un vtable
Declarațiile specifice FPC stau în lxOleInterfaces.inc, lângă lxAESBackend.inc și lxZlibBackend.inc în directorul de surse FPC, astfel încât alegerile specifice compilatorului stau într-un singur loc, nu risipite prin motor. Formatul în sine și felul în care îl navighează biblioteca sunt descrise în citirea fișierelor compuse OLE2 în Pascal
Încă un detaliu de tip aparține aceleiași familii. LargeInt trebuie să se rezolve la Int64 în ramura FPC, iar clasificarea lui Comp de către compilator diferă suficient între cele două toolchain-uri încât rezolvarea overload-urilor poate alege alt candidat. Testați comportamentul la offseturi mari cu un file stream, nu cu un stream HGLOBAL: stream-ul de memorie globală Windows se întoarce singur peste seek-uri de peste 4 GiB, deci un test care trece acolo nu dovedește nimic despre aritmetica dumneavoastră
Ce ascunde o implementare AES auto-consecventă
Fișierele obiect AES Win32 pe care build-ul Delphi le leagă sunt OMF, iar linker-ul Free Pascal nu le poate consuma, deci ramura FPC folosește o implementare AES în Pascal. Delphi continuă să lege fișierele obiect pe care le-a legat mereu, ceea ce ține binarul lansat neschimbat pentru clienții existenți
Cerința de verificare e partea care merită dusă în orice proiect. Criptatul datelor și decriptatul lor înapoi cu aceeași implementare nu dovedește absolut nimic: un algoritm simetric cu un key schedule greșit, cu ordine de blocuri greșită sau cu înlănțuire greșită e perfect auto-consecvent și își face du-te-vino pe propria ieșire de fiecare dată. Doar vectorii known-answer îl prind, verificând extinderea cheii, ordinea blocurilor și înlănțuirea CBC contra valorilor publicate. Livrați o implementare greșită, dar auto-consecventă, iar simptomul apare prima dată când un client deschide fișierul în Excel
Compresia a avut un defect de altă factură. Un backend inflate în Pascal poate avea încă ieșire în așteptare după ce și-a consumat toată intrarea comprimată, deci apelantul trebuie să țină cont de apeluri până când stream-ul își raportează sfârșitul. A trata o intrare epuizată ca sfârșit de stream trunchiază ultimul bloc. Mai rău, transformă o arhivă deteriorată într-una acceptată tăcut, exact modul de eșec pe care întărirea din validarea înregistrării ZIP end-of-central-directory există să îl prevină. Regula e că niciun progres plus neterminat e o eroare de trunchiere, niciodată un EOF
Două capcane de sistem de build care costă ore reale
Căile de căutare LCL trebuie să preceadă căile wildcard de pachete FPC, altfel unitatea Menus din Free Vision acoperă unitatea LCL cu același nume și primiți un mismatch de checksum PPU care nu spune nimic despre niciunul. O instalare Lazarus mutată după instalare poate lăsa și ea căi perimate în fpc.cfg, deci punctele de intrare ale build-ului specifică explicit căile de unități și binare, în loc să moștenească ce oferă mediul
A doua capcană n-are nimic a face cu Pascal. Un fișier batch .cmd scris cu linii LF funcționează până când fișierul crește peste mărimea bufferului de citire al interpretorului, moment în care call :label eșuează cu susținerea că eticheta batch nu există, iar eșecul apare la programul care se întâmplă să stea peste graniță. Orice unealtă care rescrie un script batch trebuie să scrie CRLF înapoi. Iar lazbuild --build-all curăță directorul de ieșire al unităților de pachet înainte de compilație, deci un fișier de opțiuni parcat în directorul acela se șterge înainte să poată fi citit: țineți-l afară și rețineți că calea @ se rezolvă relativ la directorul pachetului, pentru că lazbuild invocă compilatorul de acolo
// Export de grilă Lazarus: TGridToXLS se livrează în pachetul Lazarus,
// deci același cod de export DB-grid funcționează într-o aplicație 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;
Ce valorează un avertisment de compilator
Free Pascal raportează variabilele locale neinițializate pe care Delphi nu le raportează, iar rularea build-ului FPC a transformat diferența aceea în două defecte reale în unitatea de calcul. O funcție citea o variabilă de contor căreia nu i s-a atribuit niciodată nimic înainte de folosire, iar alta folosea două coordonate într-o ramură înainte ca codul care le calcula să ruleze într-o altă ramură. Sub Delphi ambele se comportau după ce se întâmpla să țină stiva, adică definiția bug-ului care se reproduce pe o mașină și nu pe alta
Concluzia practică e că al doilea compilator merită ținut în buclă chiar și pentru un produs care se livrează în primul rând pe primul. Scanarea periodică a claselor de avertismente FPC e o trecere ieftină de analiză statică peste o bază de cod Delphi și găsește o categorie de defect la care nicio suită de test nu ajunge fiabil. Disciplina mai largă a matricei de versiuni în care asta stă e descrisă în matricea de build cross-compiler
Suportul Free Pascal și Lazarus pentru Windows se livrează cu HotXLS Delphi spreadsheet component ca un pachet Lazarus alături de pachetele Delphi și C++Builder, construit din același arbore de surse, nu dintr-un fork. Asta e rostul întregului efort: un motor, patru toolchain-uri, iar deciziile specifice compilatorului izolate în fișiere include pe care le poți citi într-o singură ședință