Tehnički članak

HotXLS na Free Pascal-u: Unicode, COM slotovi i zlib

HotXLS se gradi pod Free Pascal-om i Lazarus-om na Windows-ima, i port je počivao na četiri odluke koje nemaju veze sa Object Pascal sintaksom: zadržati jezgro u DELPHIUNICODE režimu, deklarisati OLE structured-storage interfejse kao CORBA interfejse sa ručno vođenim brojanjem referenci, zameniti Win32 AES objektne datoteke Pascal implementacijom, i popraviti inflate petlju koja je mogla odsečeni ZIP da prihvati kao potpun

Svako ko je portovao zrelu Delphi biblioteku zna oblik ovog posla. Kompajler prihvata skoro sve u prvom prolazu. Sledi dug rep bihevioralnih razlika koje se čisto kompajliraju a proizvode pogrešne rezultate, i spreadsheet engine je neobično izložen njima jer dodiruje enkodiranje teksta, COM structured storage, kompresiju i kriptografiju u jednom kodnom putu

Zašto jezgro insistira na DELPHIUNICODE, a ne na čistom DELPHI?

Zato što formula engine zavisi od toga da String i Char nose UTF-16 semantiku, a ANSI alternativa gubi znakove pre nego što išta stigne do datoteke. Mamljivo je graditi jezgro u FPC DELPHI režimu, pošto je to kompatabilni switch kome većina portova poseže, i kod se kompajlira. Onda radna sveska sa kineskim imenima listova ili ćiriličnim etiketama ode kroz putanju računanja i znakovi su nestali do trenutka kad ih pisac vidi, bez ijedne greške bilo gde

Režim nije ujednačen kroz biblioteku, i to je namerno, a ne neuredno. PNG byte dekoder i LCL override-i stvarno trebaju ANSI potpise, jer se bave bajtovima i onim što im widgetset uruči. Ti unit-i uključuju poseban LX_FPC_ANSI switch. Dva režima u jednoj biblioteci zvuči kao smrad u kodu dok ne primetite da je alternativa byte dekoder koji svoj ulaz tretira kao tekst

Postoji i prateći detalj koji ljude uhvati kasnije. DELPHIUNICODE ne čini TFormatSettings.DecimalSeparator WideChar-om u FPC runtime-u. Ulaz koji nosi Unicode decimalni separator mora se prvo normalizovati na ASCII separator unutar Unicode stringa, i svaki ulaz čiji separator ne odgovara očekivanom mora se odbiti, a ne tiho odseći na znaku koji parser nije prepoznao

program ExportReport;
{$MODE DELPHI}
uses
  Interfaces,          // mora doći prvo: inicijalizuje LCL widgetset
  SysUtils, lxHandle;  // i sloj UTF-8 konverzije

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.

Interfaces unit nije opcion i mora doći prvi. On inicijalizuje LCL widgetset i sloj UTF-8 konverzije, i HotXLS se oslanja na oba čim fontovi, putanje datoteka ili tekst pređu RTL i LCL granicu. Konzolni program koji ga preskoči kompajliraće se i ponašaće pogrešno na svakoj ne-ASCII putanji. To je i razlog što uspešno kompajliranje ovde dokazuje tako malo: port je demonstrabilno radio tek kad su stvarni dokumenti sa stvarnim imenima fontova i stvarnim putanjama napravili pun povratni krug

Class VMT nije COM vtable

Free Pascal vam neće dozvoliti da Windows-u uručite class VMT kao COM interface vtable, čak i kad deklaracija izgleda identično onoj koju Delphi prihvata. Rasporedi se razlikuju na načine koji proizvode poziv u pogrešan slot, što se manifestuje kao pad negde nepovezan sa mestom poziva. Structured storage je ovde važan jer je klasični binarni format radne sveske OLE compound datoteka, i njeno čitanje ili pisanje znači implementirati ILockBytes u koje će Windows storage API pozivati nazad

Radni aranžman je CORBA interfejs sa eksplicitno deklarisanim COM slotovima i AddRef i Release vođenim ručno. To znači odreći se automatskog brojanja referenci za ove tipove i preuzeti odgovornost za životni vek, što je fer razmena za šaku interfejsa koji žive unutar jednog unit-a. Konkretna zamka unutar toga posla je QueryInterface: mora da vrati interface pointer, a ne object pointer. Obe varijante se kompajliraju. Jedna od njih uruči Windows-u adresu čija prva mašinska reč nije vtable

Dijagram koji poredi Free Pascal class VMT sa COM interface vtable koju HotXLS mora da ponudi Windows structured storage API-ju: različiti redosledi slotova za istu Pascal deklaraciju, plus QueryInterface zamka gde vraćanje object pointera umesto interface pointera šalje ILockBytes poziv u class slot i ruši daleko od mesta poziva
Free Pascal odbija da služi class VMT kao COM vtable, pa HotXLS deklariše CORBA interfejse sa eksplicitnim COM slotovima i ručno vođenim AddRef i Release, a QueryInterface vraća interface pointer koji Windows može da dereferencira

FPC-specifične deklaracije žive u lxOleInterfaces.inc, pored lxAESBackend.inc i lxZlibBackend.inc u FPC direktorijumu izvora, pa kompajlerski specifične odluke sede na jednom mestu umesto da su razasute kroz engine. Sam format i kako ga biblioteka obilazi opisani su u članku o čitanju OLE2 compound datoteka u Pascal-u

Još jedan tip detalj pripada istoj familiji. LargeInt mora da se razreši u Int64 u FPC grani, i kompajlerska klasifikacija Comp-a dovoljno se razlikuje između dve alatke da overload rezolucija može izabrati drugog kandidata. Testirajte ponašanje velikih ofseta file stream-om, a ne HGLOBAL stream-om: Windows global-memory stream sam se omotava pri seeks preko 4 GiB, pa položen test tamo ne dokazuje ništa o vašoj aritmetici

Šta samousaglašena AES implementacija krije

Win32 AES objektne datoteke koje Delphi build povezuje su OMF, i Free Pascal linker ne ume da ih koristi, pa FPC grana koristi Pascal AES implementaciju umesto njih. Delphi i dalje povezuje objektne datoteke koje je oduvek, što objavljeni binarni artefakt drži nepromenjenim za postojeće kupce

Zahtev verifikacije je deo vredan nošenja u svaki projekat. Šifrovanje podataka i dešifrovanje istom implementacijom ne dokazuje ništa: simetričan algoritam sa pogrešnim key schedule-om, pogrešnim redosledom blokova ili pogrešnim ulančavanjem savršeno je samousaglašen i svaki put će round-trip-ovati svoj izlaz. Samo known-answer vektori ga hvataju, proverom key ekspanzije, redosleda blokova i CBC ulančavanja naspram objavljenih vrednosti. Isporučite samousaglašenu pogrešnu implementaciju i simptom se pojavi prvi put kad kupac otvori datoteku u Excel-u

Kompresija je imala defekt drugačijeg karaktera. Pascal inflate backend može i dalje imati izlaz na čekanju pošto je potrošio sav komprimovani ulaz, pa pozivalac mora da nastavi da zove dok tok ne prijavi svoj kraj. Tretiranje istrošenog ulaza kao kraja toka odseca poslednji blok. Gore od toga, od oštećene arhive pravi tiho prihvaćenu, a to je baš režim otkaza kome očvršćavanje iz članka o validaciji ZIP end-of-central-directory zapisa postoji da ga spreči. Pravilo je da nema napretka plus nije gotovo jeste greška odsecanja, nikada EOF

Dve zamke build sistema koje koštaju stvarnih sati

LCL search putanje moraju prethoditi FPC wildcard putanjama paketa, ili Free Vision Menus unit zasenjuje LCL unit istog imena i dobijate PPU checksum neslaganje koje ne kaže ništa o nijednom od ta dva. Lazarus instalacija premeštena posle instaliranja može takođe ostaviti zastarele putanje u fpc.cfg, pa build ulazne tačke eksplicitno navode unit i binarne putanje umesto da naslede šta god okruženje ponudi

Druga zamka nema nikakve veze sa Pascal-om. .cmd batch datoteka pisana LF krajevima linija radi dok datoteka ne naraste preko veličine read bafera interpreter-a, kad call :label padne sa tvrdnjom da batch labela ne postoji, i otkaz se pojavi na bilo kom programu koji slučajno sedi preko granice. Svaka alatka koja prepisuje batch skriptu mora da upiše CRLF nazad. I lazbuild --build-all čisti direktorijum izlaza paketnih unit-a pre kompajliranja, pa datoteka sa opcijama parkirana u tom direktorijumu bude obrisana pre nego što se može pročitati: držite je van, i setite se da se @ putanja razrešava relativno na direktorijum paketa jer lazbuild poziva kompajler odatle

Mapa dva sloja opasnosti iza čistog HotXLS Free Pascal kompajliranja: DELPHIUNICODE režim koji drži String i Char u UTF-16, LX_FPC_ANSI beg za PNG byte dekoder i LCL override-e, i build zamke od Free Vision Menus senke, zastarelih fpc.cfg putanja, LF-only batch datoteka i lazbuild brisanja izlaza
Prvo kompajliranje dokazuje malo: mapa režima odlučuje koji znakovi prežive do pisca, dok se build zamke pokazuju kao checksum neslaganja, fantomski nestale labele i datoteke opcija obrisane pre nego što su pročitane
// Lazarus grid export: TGridToXLS stiže u Lazarus paketu, pa isti
// DB-grid export kod radi u LCL aplikaciji
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;

Koliko vredi upozorenje kompajlera

Free Pascal prijavljuje neinicijalizovane lokalne promenljive koje Delphi ne, i pokretanje FPC build-a pretvorilo je tu razliku u dva stvarna defekta u unit-u računanja. Jedna funkcija čitala je brojačku promenljivu koja nikada nije dodeljena pre upotrebe, a druga koristila je dve koordinate u jednoj grani pre nego što je kod koji ih je računao radio u drugoj grani. Pod Delphi-jem su se obe ponašale prema tome što je stek slučajno držao, a to je definicija baga koji se reprodukuje na jednoj mašini, a ne na drugoj

Praktičan zaključak je da drugi kompajler vredi držati u krugu čak i za proizvod koji se prvenstveno isporučuje na prvom. Pregledanje FPC klasa upozorenja periodično je jeftin prolaz statičke analize nad Delphi kodnom bazom, i nalazi kategoriju defekata do koje nijedan test suite pouzdano ne dopire. Šira disciplina verzione matrice unutar koje ovo sedi opisana je u članku o cross-kompajlerskoj build matrici

Podrška Free Pascal-a i Lazarus-a za Windows stiže uz HotXLS Delphi spreadsheet komponentu kao Lazarus paket uz Delphi i C++Builder pakete, građen iz istog stabla izvora, a ne iz forka. U tome je poenta vežbe: jedan engine, četiri alatke, i kompajlerski specifične odluke izolovane u include datotekama gde se mogu pročitati za jedno sedenje