HotXLS buduje się pod Free Pascalem i Lazarusem na Windows, a port rozstrzygnął się na czterech decyzjach, które nie mają nic wspólnego ze składnią Object Pascala: trzymać rdzeń w trybie DELPHIUNICODE, deklarować interfejsy OLE structured storage jako interfejsy CORBA z ręcznie prowadzonym zliczaniem referencji, zastąpić pliki obiektów AES Win32 implementacją w Pascalu i naprawić pętlę inflate, która mogła przyjąć ucięty ZIP za kompletny
Każdy, kto portował dojrzałą bibliotekę Delphi, zna kształt tej pracy. Kompilator przyjmuje prawie wszystko za pierwszym przejściem. Potem przychodzi długi ogon różnic zachowania, które kompilują się czysto, a dają złe wyniki, a silnik arkuszy jest na nie nieproporcjonalnie wystawiony, bo dotyka kodowania tekstu, COM structured storage, kompresji i kryptografii w jednej ścieżce kodu
Dlaczego rdzeń upiera się przy DELPHIUNICODE, a nie gołym DELPHI?
Bo silnik formuł zależy od tego, że String i Char niosą semantykę UTF-16, a alternatywa ANSI gubi znaki, zanim cokolwiek dotrze do pliku. Kusi, by zbudować rdzeń w trybie FPC DELPHI, bo to przełącznik zgodności, po który sięga większość portów, i kod się kompiluje. Potem skoroszyt z chińskimi nazwami arkuszy albo cyrylicznymi etykietami przechodzi przez ścieżkę obliczeń w obie strony i znaki są już nieobecne, gdy writer je widzi, bez żadnego błędu
Tryb nie jest jednolity w całej bibliotece i to jest świadome, a nie niechlujne. Dekoder bajtów PNG i nadpisania LCL naprawdę potrzebują sygnatur ANSI, bo operują na bajtach i na tym, co widgetset im wręcza. Te unity włączają osobny przełącznik LX_FPC_ANSI. Dwa tryby w jednej bibliotece brzmi jak code smell, dopóki nie zauważysz, że alternatywą jest dekoder bajtów traktujący swoje wejście jako tekst
Jest jeszcze detal towarzyszący, który łapie ludzi później. DELPHIUNICODE nie czyni TFormatSettings.DecimalSeparator WideCharem w runtime FPC. Wejście niosące Unicode'owy separator dziesiętny musi być najpierw znormalizowane do separatora ASCII wewnątrz ciągu Unicode, a każde wejście, którego separator nie pasuje do oczekiwanego, musi być odrzucone, a nie po cichu ucięte na znaku, którego parser nie rozpoznał
program ExportReport;
{$MODE DELPHI}
uses
Interfaces, // musi być pierwsze: inicjalizuje widgetset LCL
SysUtils, lxHandle; // i warstwę konwersji 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 nie jest opcjonalny i musi przyjść pierwszy. To on inicjalizuje widgetset LCL i warstwę konwersji UTF-8, a HotXLS na obu polega, skoro fonty, ścieżki plików albo tekst przekraczają granicę RTL i LCL. Program konsolowy, który to pominie, skompiluje się i będzie się źle zachowywał na każdej ścieżce nie-ASCII. To też powód, dla którego udana kompilacja dowodzi tu tak mało: port demonstracyjnie działał dopiero, gdy prawdziwe dokumenty z prawdziwymi nazwami fontów i prawdziwymi ścieżkami zrobiły pełną rundę w obie strony
VMT klasy to nie vtable COM
Free Pascal nie pozwoli ci podać VMT klasy Windowsowi jako vtable interfejsu COM, nawet gdy deklaracja wygląda identycznie jak ta, którą akceptuje Delphi. Układy różnią się w sposób produkujący wywołanie złego slotu, co manifestuje się crashem gdzieś niezwiązanym z miejscem wywołania. Structured storage ma tu znaczenie, bo klasyczny binarny format skoroszytu to plik złożony OLE, a jego czytanie albo pisanie znaczy implementację ILockBytes, w którą API przechowywania Windows będzie się odwoływać
Działający układ to interfejs CORBA z jawnie zadeklarowanymi slotami COM i ręcznie prowadzonymi AddRef i Release. To znaczy zrezygnować z automatycznego zliczania referencji dla tych typów i wziąć odpowiedzialność za czas życia, co jest uczciwym handlem dla garstki interfejsów mieszkających w jednym unicie. Konkretna pułapka w tej robocie to QueryInterface: musi zwracać wskaźnik interfejsu, nie wskaźnik obiektu. Oba się kompilują. Jeden z nich wręcza Windowsowi adres, którego pierwsze maszynowe słowo nie jest vtable
Deklaracje właściwe FPC mieszkają w lxOleInterfaces.inc, obok lxAESBackend.inc i lxZlibBackend.inc w katalogu źródeł FPC, więc wybory właściwe kompilatorowi siedzą w jednym miejscu, zamiast być rozsiane po silniku. Sam format i to, jak porusza się po nim biblioteka, opisuje czytanie plików złożonych OLE2 w Pascalu
Jeszcze jeden detal typowy należy do tej samej rodziny. LargeInt musi rozwiązywać się do Int64 w gałęzi FPC, a klasyfikacja Comp przez kompilator różni się między toolchainami na tyle, że rozwiązywanie przeciążeń może wybrać innego kandydata. Testuj zachowanie dużych offsetów strumieniem plikowym, a nie strumieniem HGLOBAL: strumień pamięci globalnej Windows sam zawija się przy seekach ponad 4 GiB, więc zdany tam test nie dowodzi niczego o twojej arytmetyce
Co skrywa samospójna implementacja AES
Pliki obiektów AES Win32, które linkuje build Delphi, są OMF i linker Free Pascala nie umie ich skonsumować, więc gałąź FPC używa zamiast nich implementacji AES w Pascalu. Delphi dalej linkuje pliki obiektów, które zawsze linkowała, co trzyma wydaną binarkę bez zmian dla istniejących klientów
Wymóg weryfikacji to część warta zabrania do każdego projektu. Zaszyfrowanie danych i odszyfrowanie ich tą samą implementacją nie dowodzi niczego: algorytm symetryczny ze złym harmonogramem klucza, złą kolejnością bloków albo złym chainowaniem jest doskonale samospójny i za każdym razem przepuści własne wyjście w obie strony. Łapią to wyłącznie wektory known-answer, sprawdzające rozszerzenie klucza, kolejność bloków i chainowanie CBC względem opublikowanych wartości. Wyślij samospójną, ale złą implementację, a objaw pokaże się, gdy klient pierwszy raz otworzy plik w Excelu
Kompresja miała defekt innego charakteru. Backend inflate w Pascalu może mieć jeszcze wyjście w kolejce po skonsumowaniu całego skompresowanego wejścia, więc wołający musi wołać dalej, aż strumień zgłosi swój koniec. Potraktowanie wyczerpanego wejścia jako końca strumienia ucina ostatni blok. Gorzej: zamienia uszkodzone archiwum w po cichu zaakceptowane, czyli dokładnie ten tryb awarii, któremu zapobiega utwardzenie opisane w walidacja rekordu ZIP end-of-central-directory. Reguła brzmi: brak postępu plus brak zakończenia to błąd ucięcia, nigdy EOF
Dwie pułapki systemu budowania, które kosztują realne godziny
Ścieżki wyszukiwania LCL muszą poprzedzać ścieżki wildcard pakietów FPC, bo inaczej unit Menus z Free Vision zasłoni unit LCL o tej samej nazwie, a ty dostaniesz niezgodność sumy kontrolnej PPU, która o niczym z nich nie mówi. Instalacja Lazarusa przeniesiona po zainstalowaniu potrafi też zostawić nieświeże ścieżki w fpc.cfg, więc punkty wejścia builda podają ścieżki unitów i binariów jawnie, zamiast dziedziczyć cokolwiek da środowisko
Druga pułapka nie ma nic wspólnego z Pascalem. Plik wsadowy .cmd napisany z końcami linii LF działa, dopóki plik nie urośnie ponad rozmiar bufora odczytu interpretera, w którym to momencie call :label wywala się z tezą, że etykieta batcha nie istnieje, a awaria pokazuje się w tym programie, który akurat siedzi za granicą. Każde narzędzie przepisujące skrypt wsadowy musi z powrotem zapisać CRLF. A lazbuild --build-all czyści katalog wyjściowy unitów pakietu przed kompilacją, więc plik opcji zaparkowany w tym katalogu zostaje usunięty, zanim da się go przeczytać: trzymaj go na zewnątrz i pamiętaj, że ścieżka @ jest rozwiązywana względem katalogu pakietu, bo lazbuild odpala kompilator stamtąd
// Eksport siatki Lazarus: TGridToXLS jest w pakiecie Lazarus, więc ten sam
// kod eksportu DB-grid działa w aplikacji 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;
Ile wart jest warning kompilatora
Free Pascal zgłasza niezainicjalizowane zmienne lokalne, których Delphi nie zgłasza, i odpalenie builda FPC zamieniło tę różnicę w dwa prawdziwe defekty w unicie obliczeń. Jedna funkcja czytała zmienną licznika, która nigdy nie została przypisana przed użyciem, a inna używała dwóch współrzędnych w jednej gałęzi, zanim kod je liczący przeszedł w innej gałęzi. Pod Delphi obie zachowywały się według tego, co akurat leżało na stosie, czyli definicja błędu, który reprodukuje się na jednej maszynie, a na innej nie
Praktyczny wniosek: drugi kompilator wart jest trzymania w pętli nawet dla produktu wydawanego głównie na pierwszym. Okresowe przeglądanie klas warningów FPC to tani przebieg analizy statycznej po kodzie Delphi i znajduje kategorię defektów, do której żadna suita testów nie sięga niezawodnie. Szerszą dyscyplinę macierzy wersji, w której to siedzi, opisuje macierz buildów międzykompilatorowych
Wsparcie Free Pascala i Lazarusa dla Windows jest częścią komponentu arkuszy HotXLS dla Delphi jako pakiet Lazarus obok pakietów Delphi i C++Builder, budowany z tego samego drzewa źródeł, a nie z forka. W tym sens ćwiczenia: jeden silnik, cztery toolchainy i decyzje właściwe kompilatorowi odizolowane w plikach include, które można przeczytać na jednym posiedzeniu