HotXLS builduje pod Free Pascalem a Lazarusem na Windows a port se točil kolem čtyř rozhodnutí, která s Object Pascal syntaxí nemají nic společného: udržet jádro v módu DELPHIUNICODE, deklarovat OLE structured-storage interface jako CORBA interface s ručně spravovaným reference countingem, nahradit Win32 AES objektové soubory Pascal implementací a opravit inflate smyčku, která mohla vzít useknutý ZIP za kompletní
Kdokoli někdy portoval vyzrálou Delphi knihovnu, zná podobu téhle práce. Kompilátor v prvním průchodu přijme skoro všechno. Následuje dlouhý ocas behaviorálních rozdílů, které se zkompilují čistě a produkují špatné výsledky, a spreadsheet engine je na ně neobvykle vystavený, protože v jediné kódové cestě sahá na text encoding, COM structured storage, kompresi i kryptografii
Proč jádro trvá na DELPHIUNICODE a ne na prostém DELPHI?
Protože formula engine závisí na tom, že String a Char nesou UTF-16 sémantiku, a ANSI alternativa ztrácí znaky dřív, než cokoliv dorazí do souboru. Láká postavit jádro v FPC módu DELPHI, protože to je compatibility switch, po kterém sáhne většina portů, a kód se zkompiluje. Pak sešit s čínskými jmény listů nebo azbukou v popiscích projde výpočetní cestou tam a zpět a znaky jsou pryž, už v okamžiku, kdy je uvidí writer, a žádná chyba nikde
Mód není napříč knihovnou jednotný a to je záměr, ne neúhled. PNG byte decoder a LCL override skutečně potřebují ANSI signatury, protože pracují s bajty a s tím, co jim widgetset podá. Tyhle unity zapínají samostatný switch LX_FPC_ANSI. Dva módy v jedné knihovně zní jako code smell, dokud si nevšimnete, že alternativou je byte decoder, který svůj vstup bere za text
Je tu doprovodný detail, který později chytá lidi. DELPHIUNICODE nedělá z TFormatSettings.DecimalSeparator WideChar v FPC runtime. Vstup nesoucí Unicode desetinný separátor se musí nejdřív normalizovat na ASCII separátor uvnitř Unicode stringu a každý vstup, jehož separátor neodpovídá očekávanému, musí být odmítnut, ne tiše useknut na znaku, který parser nerozpoznal
program ExportReport;
{$MODE DELPHI}
uses
Interfaces, // musí být první: inicializuje LCL widgetset
SysUtils, lxHandle; // a vrstvu konverze 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.
Unit Interfaces není volitelná a musí být první. To ona inicializuje LCL widgetset a vrstvu konverze UTF-8 a HotXLS se na obě spoléhá, jakmile fonty, cesty souborů nebo text překročí hranici RTL a LCL. Console program, který ji vynechá, se zkompiluje a na jakékoli ne-ASCII cestě se zblázní. To je taky důvod, proč úspěšný compile tady nedokazuje tak málo: port byl prokazatelně funkční, až když reálné dokumenty s reálnými jmény fontů a reálnými cestami udělaly kompletní cestu tam a zpět
Class VMT není COM vtable
Free Pascal vám nedovolí podstrčit class VMT Windows jako COM interface vtable, i když deklarace vypadá identicky s tou, kterou Delphi přijme. Layouty se liší způsobem, který produkuje volání do špatného slotu, a projeví se to jako crash někde úplně mimo místo volání. Structured storage je tu relevantní, protože klasický binární formát sešitu je OLE compound file a číst ho nebo zapisovat znamená implementovat ILockBytes, do nichž se Windows storage API bude vracet callbacky
Funkční úprava je CORBA interface s explicitně deklarovanými COM sloty a ručně spravovanými AddRef a Release. To znamená vzdát se automatického reference countingu pro tyhle typy a převzít odpovědnost za životnost, což je fér obchod za hrstku interface, které žijí uvnitř jedné unity. Konkrétní past uvnitř téhle práce je QueryInterface: musí vracet interface pointer, ne object pointer. Obojí se zkompiluje. Jedno z nich podá Windows adresu, jejíž první machine word není vtable
Deklarace specifické pro FPC bydlí v lxOleInterfaces.inc, vedle lxAESBackend.inc a lxZlibBackend.inc ve zdrojovém adresáři FPC, takže rozhodnutí vázaná na kompilátor sedí na jednom místě místo aby se sypala enginem. Formát sám a to, jak se jím knihovna prodírá, popisuje článek čtení OLE2 compound files v Pascalu
Ještě jeden typový detail patří do stejné rodiny. LargeInt se musí ve FPC větvi resolve na Int64 a klasifikace typu Comp kompilátorem se mezi oběma toolchainy liší dost na to, aby overload resolution vybral jiného kandidáta. Velké offsety testujte na file streamu, ne na HGLOBAL streamu: Windows stream globální paměti se při seeku za 4 GiB sám zabalí zpět, takže prošlý test tady o vaší aritmetice nedokáže nic
Co skrývá self-consistent AES implementace
Win32 AES objektové soubory, které Delphi build linkuje, jsou OMF a Free Pascal linker je nedokáže skousnout, takže FPC větev používá místo nich Pascal AES implementaci. Delphi dál linkuje objektové soubory, které vždycky, což ponechává vydanou binárku beze změny pro stávající zákazníky
Ověřovací požadavek je ta část, kterou stojí za to nést do jakéhokoli projektu. Zašifrovat data a dešifrovat je znovu stejnou implementací nedokazuje vůbec nic: symetrický algoritmus se špatným key schedule, špatným pořadím bloků nebo špatným chainingem je dokonale self-consistent a svůj vlastní výstup projede tam a zpět pokaždé. Chytí ho jen known-answer vektory, které kontrolují key expansion, pořadí bloků a CBC chaining proti publikovaným hodnotám. Vyplatíte self-consistent špatnou implementaci a symptom se objeví poprvé, kdy zákazník otevře soubor v Excelu
Komprese měla defekt jiného ražení. Pascal inflate backend může mít výstup ve frontě ještě poté, co spotřeboval celý svůj komprimovaný vstup, takže volající musí volat dál, dokud stream nenahlásí svůj konec. Brát vyčerpaný vstup za konec streamu usekne poslední blok. Hůř, to z poškozeného archivu udělá tiše přijatý, což je přesně ten failure mód, kterému má bránit hardening v článku validace ZIP end-of-central-directory záznamu. Pravidlo zní: žádný progres plus nedokončeno je truncation error, nikdy EOF
Dvě pasti build systému, které stály reálné hodiny
LCL search paths musejí předcházet wildcard cestám FPC balíčků, jinak unit Menus z Free Vision zastíní stejně jmenovanou LCL unit a dostanete PPU checksum mismatch, který o obou neřekne nic. Instalace Lazarusu přemístěná po instalaci může navíc zanechat v fpc.cfg zastaralé cesty, takže build vstupní body specifikují cesty unit i binárek explicitně, místo aby dědily, co zrovna nabídne prostředí
Druhá past s Pascalem nemá nic společného. Batch soubor .cmd psaný s LF koncemi řádků funguje, dokud soubor nepřeroste velikost read bufferu interpretu, a pak call :label selže s tvrzením, že batch label neexistuje, a selhání se ukáže na tom programu, který zrovna sedí za hranicí. Jakýkoli nástroj, který batch script přepisuje, musí zapsat zpět CRLF. A lazbuild --build-all před kompilací vyčistí výstupní adresář unit balíčku, takže options soubor zaparkovaný v tom adresáři se smaže, než ho vůbec někdo přečte: držte ho jinde a pamatujte, že @ cesta se resolve relativně k adresáři balíčku, protože lazbuild spouští kompilátor odtamtud
// Export gridu v Lazarusu: TGridToXLS se dodává v Lazarus balíčku, takže
// týž exportní kód DB-gridu funguje v LCL aplikaci
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;
Co je platný compiler warning
Free Pascal hlásí neinicializované lokální proměnné, které Delphi nehlásí, a spuštění FPC buildu proměnilo tenhle rozdíl ve dva reálné defekty ve výpočetní unitě. Jedna funkce četla count proměnnou, které nikdo nepřiřadil hodnotu před použitím, a druhá použila dvě souřadnice v jedné větvi dřív, než kód, který je spočítal, běžel v jiné větvi. Pod Delphi se obojí chovalo podle toho, co zrovna leželo na stacku, což je definice bugu, který se reprodukuje na jednom stroji a na jiném ne
Praktický závěr je, že se druhý kompilátor vyplatí držet ve hře i u produktu, který se primárně dodává na tom prvním. Pravidelné projíždění warning tříd FPC je levný static-analytický průchod přes Delphi codebase a najde kategorii defektů, na kterou žádná test suite spolehlivě nedosáhne. Širší disciplínu version matrice, do níž tohle spadá, popisuje článek cross-compiler build matice
Podpora Free Pascal a Lazarus pro Windows se dodává s HotXLS Delphi spreadsheet komponentou jako Lazarus balíček vedle balíčků pro Delphi a C++Builder, buildovaný ze stejného zdrojového stromu, ne z forku. V tom je pointa celého úkolu: jeden engine, čtyři toolchainy a rozhodnutí vázaná na kompilátor izolovaná v include souborech, kde je lze přečíst na jedno posezení