Articol tehnic

HotXLS pe Free Pascal: Unicode, sloturi COM și zlib

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

Diagramă care compară VMT-ul de clasă Free Pascal cu vtable-ul de interfață COM pe care HotXLS trebuie să îl prezinte API-ului de structured storage Windows: ordine de sloturi diferite pentru aceeași declarație Pascal, plus capcana QueryInterface în care întoarcerea pointerului obiectului în locul pointerului de interfață trimite un apel ILockBytes către un slot de clasă și crapă departe de locul apelului
Free Pascal refuză să servească un VMT de clasă drept vtable COM, deci HotXLS declară interfețe CORBA cu sloturi COM explicite și AddRef și Release gestionate de mână, iar QueryInterface întoarce pointerul de interfață pe care Windows îl poate dereferenția

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

Harta celor două straturi de pericol din spatele unei compilații curate HotXLS Free Pascal: modul DELPHIUNICODE care ține String și Char în UTF-16, scăparea LX_FPC_ANSI pentru decoder-ul de octeți PNG și override-urile LCL și capcanele de build de la acoperirea Menus din Free Vision, căi perimate în fpc.cfg, fișiere batch doar-LF și ștergerea ieșirilor de către lazbuild
O primă compilație dovedește puțin: harta de moduri decide ce caractere supraviețuiesc până la scriitor, în timp ce capcanele de sistem de build ies la suprafață ca mismatch-uri de checksum, etichete fantomă lipsă și fișiere de opțiuni șterse înainte să fie citite
// 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ță