Техническа статия

HotXLS под Free Pascal и Lazarus: Unicode и COM ABI

HotXLS се компилира под Free Pascal и Lazarus на Windows, а портът се въртеше около четири решения, които нямат нищо общо с Object Pascal синтаксиса: ядрото остава в DELPHIUNICODE режим, OLE structured-storage интерфейсите се обявяват като CORBA интерфейси с ръчно управляван reference counting, Win32 AES обектните файлове се заменят с Pascal имплементация, и inflate цикъл, способен да приеме отрязан ZIP за завършен, се поправя

Всеки, портирал зряла Delphi библиотека, познава формата на тази работа. Компилаторът приема почти всичко на първия пас. Следва дълга опашка от поведенчески разлики, които се компилират чисто и произвеждат грешни резултати, а spreadsheet двигател е необичайно изложен на тях, защото пипа текстово кодиране, COM structured storage, компресия и криптография в един кодов път

Защо ядрото настоява за DELPHIUNICODE, а не за обикновен DELPHI?

Защото формулният двигател разчита String и Char да носят UTF-16 семантика, а ANSI алтернативата губи символи, преди нещо да стигне файла. Изкушаващо е да построиш ядрото в FPC DELPHI режим, тъй като това е compatibility превключвателят, към който повечето портове посягат, и кодът се компилира. После workbook с китайски имена на sheets или кирилоски етикети мине през изчислителния път и символите са изчезнали, докато writer-ът ги види, без нито една грешка някъде

Режимът не е еднакъв из цялата библиотека и това е нарочно, а не разхвърляно. PNG байтовият декодер и LCL override-ите истински се нуждаят от ANSI сигнатури, защото се занимават с байтове и с това, което widgetset-ът им подава. Тези unit-и включват отделен LX_FPC_ANSI ключ. Два режима в една библиотека звучи като code smell, докато не забележите, че алтернативата е байтов декодер, третиращ входа си като текст

Има придружаващ детайл, който хваща хората после. DELPHIUNICODE не прави TFormatSettings.DecimalSeparator на WideChar в FPC runtime-а. Вход, носещ Unicode десетичен разделител, трябва да бъде нормализиран към ASCII разделител в Unicode низа първо, а вход, чийто разделител не пасва на очаквания, трябва да бъде отхвърлян, вместо безшумно подрязан на символа, който парсерът не е разпознал

program ExportReport;
{$MODE DELPHI}
uses
  Interfaces,          // трябва да е пръв: инициализира LCL widgetset-а
  SysUtils, lxHandle;  // и 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 не е по избор и трябва да е пръв. Той инициализира LCL widgetset-а и UTF-8 конверсионния слой, а HotXLS разчита на и двата щом шрифтове, файлови пътища или текст пресекат границата RTL-LCL. Конзолна програма, прескочила го, ще се компилира и ще се държи зле на всеки не-ASCII път. Това е и причината успешна компилация да доказва толкова малко тук: портът демонстративно работеше едва щом истински документи с истински имена на шрифтове и истински пътища направиха пълен кръг

Class VMT не е COM vtable

Free Pascal няма да ви позволи да подадете class VMT на Windows като COM interface vtable, дори когато декларацията изглежда идентична с тази, която Delphi приема. Подредбите се различават по начини, произвеждащи извикване в грешен слот, което се проявява като краш някъде, несвързан с мястото на извикването. Structured storage има значение тук, защото класическият binary workbook формат е OLE compound file, а четенето или писането на такъв значи имплементиране на ILockBytes, в който Windows storage API ще се обажда обратно

Работната уредба е CORBA интерфейс с COM слотовете обявени изрично и AddRef и Release управлявани на ръка. Това значи да се откажете от автоматичния reference counting за тези типове и да поемете отговорността за живота им, което е честен компромис за шепа интерфейси, живеещи в един unit. Конкретният капан в тази работа е QueryInterface: трябва да върне интерфейсен указател, не указател към обекта. И двете се компилират. Едното подава на Windows адрес, чиято първа машинна дума не е vtable

Диаграма, сравняваща Free Pascal class VMT с COM interface vtable, която HotXLS трябва да представи на Windows structured storage API: различни подредби на слотове за една и съща Pascal декларация, плюс капанът QueryInterface, където връщането на указател към обекта вместо интерфейсен указател праща ILockBytes извикване в class слот и крашва далеч от мястото на извикването
Free Pascal отказва да сервира class VMT като COM vtable, затова HotXLS обявява CORBA интерфейси с изрични COM слотове и ръчно управляеми AddRef и Release, а QueryInterface връща интерфейсния указател, който Windows може да разреши

FPC-специфичните декларации живеят в lxOleInterfaces.inc, до lxAESBackend.inc и lxZlibBackend.inc в FPC source директорията, така че специфичните за компилатора избори седят на едно място, а не са разпръснати из двигателя. Самият формат и как библиотеката се движи из него са описани в четене на OLE2 compound файлове в Pascal

Още един типове детайл принадлежи на същото семейство. LargeInt трябва да се разреши до Int64 в FPC клона, а класификацията на Comp от компилатора се различава достатъчно между двата toolchain-а, че overload разрешаването може да избере различен кандидат. Тествайте поведението при големи офсети с file stream, а не с HGLOBAL stream: Windows global-memory stream-ът се превърта сам на seek отвъд 4 GiB, така че минаващ тест там не доказва нищо за вашата аритметика

Какво крие самосъгласувана AES имплементация

Win32 AES обектните файлове, които Delphi build-ът свързва, са OMF, а Free Pascal linker-ът не може да ги смълче, така че FPC клонът ползва Pascal AES имплементация. Delphi продължава да свързва обектните файлове, както винаги, което пази пуснатия binary непроменен за съществуващите клиенти

Изискването за верификация е частта, която си струва да понесете във всеки проект. Кодиране на данни и декодиране им отново с една и съща имплементация не доказва нищо: симетричен алгоритъм с грешен key schedule, грешен блоков ред или грешно свързване е перфектно самосъгласуван и ще мине кръг-обратно собствения си изход всеки път. Само known-answer векторите го хващат, проверявайки key разширението, блоковия ред и CBC свързването срещу публикувани стойности. Пуснете самосъгласувана грешна имплементация и симптомът се появява първия път, когато клиент отвори файла в Excel

Компресията имаше дефект с друг характер. Pascal inflate backend може още да има изход в изчакване, след като е изсмукал целия си компресиран вход, така че викащият трябва да продължава да вика, докато stream-ът докладва края си. Третирането на свършен вход като край на stream подрязва последния блок. По-лошо – превръща повреден архив в тихо приет, което е точно повредовият модел, който закалението в валидиране на ZIP end-of-central-directory записа съществува да пази. Правилото е, че никакъв прогрес плюс не-завършено е грешка от подрязване, никога EOF

Два build-system капана, струващи истински часове

LCL search пътищата трябва да предхождат wildcard пътищата на FPC пакетите, иначе Free Vision Menus unit-ът засенчва LCL unit-а със същото име и получавате PPU checksum несъответствие, което не казва нищо за нито едната. Инсталация на Lazarus, преместена след инсталирането, може и да остави застояли пътища в fpc.cfg, така че build входните точки задават unit и binary пътища изрично, вместо да наследяват каквото средата предложи

Вторият капан няма нищо общо с Pascal. .cmd batch файл, писан с LF краища на редове, работи, докато файлът порасне отвъд размера на read буфера на интерпретатора, в който момент call :label се проваля с твърдение, че batch label-ът не съществува, а провалът се появява при което и да е място, седящо отвъд границата. Всеки инструмент, пренаписващ batch скрипт, трябва да записва CRLF обратно. И lazbuild --build-all изчиства output директорията на package unit-ите преди компилация, така че options файл, паркиран в тази директория, се изтрива преди да бъде прочетен: държете го извън нея и помнете, че @ пътят се разрешава относително спрямо package директорията, защото lazbuild вика компилатора оттам

Карта на двата слоя опасности зад чиста HotXLS Free Pascal компилация: DELPHIUNICODE режимът, пазещ String и Char в UTF-16, бегството LX_FPC_ANSI за PNG байтовия декодер и LCL override-ите, и build капаните от засенчването на Free Vision Menus, застояли fpc.cfg пътища, batch файлове само с LF и изчистването на lazbuild output
Първата компилация доказва малко: картата на режими решава кои символи оцеляват до writer-а, докато build-system капаните излизат наяве като checksum несъответствия, фантомни липсващи label-и и options файлове, изтрити преди да бъдат прочетени
// Lazarus grid export: TGridToXLS ship-ва в Lazarus пакета, така че
// същият DB-grid export код работи в 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;

Колко струва предупреждение от компилатора

Free Pascal докладва неинициализирани локални променливи, които Delphi не докладва, и пускането на FPC build-а превърна тази разлика в два реални дефекта в изчислителния unit. Една функция четеше броячна променлива, никога не присвоявана преди употреба, а друга ползваше две координати в един клон преди кодът, смятащ ги, да е тръгнал в друг клон. Под Delphi и двете се държаха според каквото стекът случайно държеше, което е дефиницията на бъг, репродуциращ се на една машина, но не на друга

Практическото заключение е, че вторият компилатор си заслужава да остане в цикъла дори за продукт, ship-ващ преди всичко на първия. Скан на FPC warning класовете периодично е евтин static analysis пас върху Delphi кодова база, и намира категория дефекти, до които никаква test suite надеждно не стига. По-широката дисциплина на version matrix, в която това седи, е описана в cross-компилаторната build матрица

Free Pascal и Lazarus поддръжката за Windows ship-ва с HotXLS Delphi spreadsheet компонента като Lazarus пакет редом с Delphi и C++Builder пакетите, build-нат от същото source дърво, а не от fork. Това е смисълът на упражнението: един двигател, четири toolchain-а, и специфичните за компилатора решения, изолирани в include файлове, които могат да бъдат прочетени на едно заседание