Odborný článok

HotXLS na Free Pascal: Unicode, COM sloty a zlib

HotXLS sa zostavuje pod Free Pascal a Lazarus na Windows a port sa točil okolo štyroch rozhodnutí, ktoré nemajú nič so syntaxou Object Pascal: držať jadro v režime DELPHIUNICODE, deklarovať OLE structured-storage rozhrania ako CORBA rozhrania s ručne riadeným počítaním referencií, nahradiť Win32 objektové súbory AES Pascal implementáciou a opraviť inflate cyklus, ktorý mohol akceptovať orezaný ZIP ako kompletný

Ktokoľvek, kto už portoval zrelú Delphi knižnicu, pozná tvar tejto práce. Prekladač akceptuje takmer všetko na prvom prechode. Nasleduje za tým dlhý chvost behaviorálnych rozdielov, ktoré sa čisto skompilujú a produkujú zlé výsledky, a tabuľkový engine je voči nim neobyčajne vystavený, pretože sa v jednej kódovej ceste dotýka kódovania textu, COM structured storage, kompresie aj kryptografie

Prečo jadro trvá na DELPHIUNICODE a nie na obyčajnom DELPHI?

Pretože formula engine závisí na tom, že String a Char nesú UTF-16 sémantiku a ANSI alternatíva stratí znaky skôr, než sa čokoľvek dostane do súboru. Je lákavé stavať jadro v FPC režime DELPHI, keďže to je kompatibilný prepínač, po ktorom siahajú najviac portov, a kód sa skompiluje. Potom zošit s čínskymi menami hárkov alebo cyrilskými menovkami prejde tam a späť cez výpočtovú cestu a znaky sú preč v momente, keď ich vidí writer, bez jedinej chyby kdekoľvek

Režim nie je jednotný naprieč knižnicou a to je zámerné, nie neporiadok. PNG bajtový dekodér a LCL overridey reálne potrebujú ANSI signatúry, pretože obchodujú s bajtmi a s tým, čo im widgetset podá. Tieto unitá zapínajú osobitný prepínač LX_FPC_ANSI. Dva režimy v jednej knižnici znie ako code smell, dokiaľ si nevšimnete, že alternatívou je bajtový dekodér, ktorý berie svoj vstup ako text

Je tu sprievodný detail, ktorý ľudí chytí neskôr. DELPHIUNICODE nespraví z TFormatSettings.DecimalSeparator WideChar v FPC behovom prostredí. Vstup nesúci Unicode desatinný oddeľovač sa musí najprv normalizovať na ASCII oddeľovač vo vnútri Unicode reťazca a akýkoľvek vstup, ktorej oddeľovač sa nezhoduje s očakávaným, sa musí zamietnuť namiesto tichého orezania na znaku, ktorý parser nerozpoznal

program ExportReport;
{$MODE DELPHI}
uses
  Interfaces,          // musí byť prvé: inicializuje LCL widgetset
  SysUtils, lxHandle;  // a vrstvu konverzie 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.

Unita Interfaces nie je voliteľná a musí byť prvá. To ona inicializuje LCL widgetset a vrstvu konverzie UTF-8 a HotXLS sa na oboch spolieha v momente, keď fonty, cesty súborov alebo text prekročia hranicu RTL a LCL. Konzolový program, ktorý ju preskočí, sa skompiluje a bude sa správať zle na každej non-ASCII ceste. To je tiež dôvod, prečo úspešná kompilácia tu dokazuje tak málo: port bol dokázateľne funkčný až vtedy, keď reálne dokumenty s reálnymi menami fontov a reálnymi cestami prešli plný round trip

Class VMT nie je COM vtable

Free Pascal vám nedovolí podať class VMT Windows ako COM interface vtable, aj keď deklarácia vyzerá identicky s tou, ktorú Delphi akceptuje. Rozloženia sa líšia spôsobmi, ktoré produkujú volanie do zlého slotu, čo sa prejaví ako pád niekde nesúvisiaci s miestom volania. Structured storage tu záleží, pretože klasický binárny formát zošitu je OLE compound file a čítať ho alebo zapisovať znamená implementovať ILockBytes, do ktorých sa Windows storage API bude vraciať volaniami

Fungujúce usporiadanie je CORBA rozhranie s explicitne deklarovanými COM slotmi a AddRef a Release riadenými ručne. To znamená vzdať sa automatického počítania referencií pre tieto typy a prevziať zodpovednosť za životnosť, čo je férový obchod pre hrstku rozhraní bývajúcich v jednej unite. Špecifická pasca vnútri tej práce je QueryInterface: musí vrátiť interface pointer, nie objektový pointer. Obe sa skompilujú. Jedna z nich podá Windows adresu, ktorej prvé strojové slovo nie je vtable

Diagram porovnávajúci class VMT Free Pascal s COM interface vtable, ktorú HotXLS musí predložiť Windows structured storage API: odlišné poradia slotov pre tú istú Pascal deklaráciu, plus pasca QueryInterface, kde vrátenie objektového pointera namiesto interface pointera pošle volanie ILockBytes do class slotu a padne ďaleko od miesta volania
Free Pascal odmieta podávať class VMT ako COM vtable, takže HotXLS deklaruje CORBA rozhrania s explicitnými COM slotmi a ručne riadenými AddRef a Release a QueryInterface vracia interface pointer, ktorý Windows dokáže dereferencovať

Deklarácie špecifické pre FPC bývajú v lxOleInterfaces.inc, vedľa lxAESBackend.inc a lxZlibBackend.inc v FPC zdrojovom adresári, takže voľby špecifické pre prekladač sedia na jednom mieste namiesto toho, aby boli roztrúsené engineom. Samotný formát a to, ako sa ním knižnica orientuje, popisuje článok čítanie OLE2 compound súborov v Pascale

Do tej istej rodiny patrí ešte jeden typový detail. LargeInt sa musí resolveovať na Int64 vo vetve FPC a klasifikácia typu Comp prekladačom sa medzi toolchainmi líši dosť na to, aby overload resolution vybral iného kandidáta. Testujte správanie veľkých offsetov file streamom, nie HGLOBAL streamom: Windows global-memory stream sa pri seeks za 4 GiB sám zabalí, takže prechádzajúci test tam nedokáže nič o vlastnej aritmetike

Čo skrýva sebakonzistentná AES implementácia

Win32 objektové súbory AES, ktoré Delphi build linkuje, sú OMF a Free Pascal linker ich nedokáže skonzumovať, takže vetva FPC používa namiesto nich Pascal AES implementáciu. Delphi naďalej linkuje objektové súbory, ktoré vždy, čo necháva vydanú binárku nezmenenú pre existujúcich zákazníkov

Požiadavka overenia je tá časť, ktorá stojí za odnesenie do každého projektu. Zašifrovať dáta a znovu ich dešifrovať tou istou implementáciou nedokáže nič: symetrický algoritmus so zlým key schedule, zlým poradím blokov alebo zlým reťazením je dokonale sebakonzistentný a každýkrát prevedie vlastný výstup tam a späť. Chytia ho len known-answer vektory, ktoré kontrolujú rozšírenie kľúča, poradie blokov a CBC reťazenie proti publikovaným hodnotám. Vydať sebakonzistentnú zlú implementáciu znamená, že príznak sa objaví prvýkrát, keď zákazník otvorí súbor v Exceli

Kompresia mala defekt iného charakteru. Pascal inflate backend môže mať stále čakajúci výstup potom, čo skonzumoval celý komprimovaný vstup, takže volajúci musí volať ďalej, kým stream neohlási svoj koniec. Brať vyčerpaný vstup ako koniec streamu oreže posledný blok. Horšie je, že z poškodeného archívu urobí poticho akceptovaný, čo je presne režim zlyhania, ktorému má brániť hardening v článku validácia záznamu ZIP end-of-central-directory. Pravidlo znie: žiadny pokrok plus nedokončené je orezacia chyba, nikdy EOF

Dve pasce build systému, ktoré stoja reálne hodiny

Search paths LCL musia predchádzať wildcard cestám balíkov FPC, inak unita Menus z Free Vision zatieni LCL unitu rovnakého mena a dostanete PPU checksum mismatch, ktorý o ničom z nich nič nepovie. Inštalácia Lazarus presťahovaná po inštalácii môže navyše nechať zastarané cesty v fpc.cfg, takže vstupné body zostavenia špecifikujú unit aj binárne cesty explicitne namiesto dedičstva toho, čo prostredie ponúkne

Druhá pasca nemá s Pascalom nič. Batch súbor .cmd napísaný s LF koncami riadkov funguje, dokiaľ súbor nedorastie nad veľkosť čítacieho bufferu interpretu, v ktorom momente call :label zlyhá s tvrdením, že batch label neexistuje, a zlyhanie sa objaví pri ktoromkoľvek programe, ktorý sedí za hranicou. Každý nástroj, ktorý prepisuje batch skript, musí zapísať CRLF späť. A lazbuild --build-all vyčistí výstupný adresár unit balíka pred kompiláciou, takže options súbor zaparkovaný v tom adresári sa zmaže skôr, než sa dá prečítať: držte ho vonku a pamätajte, že cesta @ sa resolveuje relatívne voči adresáru balíka, pretože lazbuild volá prekladač odtiaľ

Mapa dvoch vrstiev nebezpečenstva za čistou kompiláciou HotXLS Free Pascal: režim DELPHIUNICODE, ktorý drží String a Char v UTF-16, únikový prepínač LX_FPC_ANSI pre PNG bajtový dekodér a LCL overridey a build pasce zo zatienenia Menus z Free Vision, zastaraných ciest fpc.cfg, batch súborov len s LF a mazania výstupu lazbuild
Prvá kompilácia nedokáže veľa: mapa režimov rozhoduje, ktoré znaky prežijú až k writerovi, kým pasce build systému sa vynoria ako checksum mismatches, fantomovo chýbajúce labely a options súbory zmazané skôr, než sa prečítajú
// Export gridu Lazarus: TGridToXLS prichádza v balíku Lazarus, takže
// ten istý exportný kód DB-gridu funguje v LCL aplikácii
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;

Čo je varovanie prekladača hodné

Free Pascal hlási neinicializované lokálne premenné, ktoré Delphi nehlási, a spustenie FPC buildu zmenilo ten rozdiel na dva reálne defekty vo výpočtovej unite. Jedna funkcia čítala počítadlovú premennú, ktorej nikto nepriradil pred použitím, a druhá použila dve súradnice v jednej vetve skôr, než kód, ktorý ich vypočítal, bežal v inej vetve. Pod Delphi sa oba správali podľa toho, čo náhoda držala na zásobníku, čo je definícia chyby, ktorá sa reprodukuje na jednom stroji a nie na inom

Praktický záver je, že druhý prekladač stojí za držanie v cykle aj pre produkt, ktorý sa vydáva predovšetkým na prvom. Pravidelné preletenie tried varovaní FPC je lacný prechod statickej analýzy nad Delphi kódovou základňou a nájde kategóriu defektov, ktorú žiadna testovacia sada nespoľahlivo nedosiahne. Širšia disciplína matice verzií, v ktorej to sedí, je popísaná v článku matica zostavenia pre rôzne prekladače

Podpora Free Pascal a Lazarus pre Windows prichádza s HotXLS Delphi tabuľkovým komponentom ako balík Lazarus vedľa balíkov Delphi a C++Builder, zostavená z toho istého zdrojového stromu, nie z forku. V tom je zmysel celého cvičenia: jeden engine, štyri toolchainy a rozhodnutia špecifické pre prekladač izolované v include súboroch, kde sa dajú prečítať na jedno posedenie