Технічна стаття

HotXLS під Free Pascal: Unicode, COM-слоти та zlib

HotXLS збирається під Free Pascal і Lazarus на Windows, і порт звівся до чотирьох рішень, які не мають нічого спільного з синтаксисом Object Pascal: тримати ядро в режимі DELPHIUNICODE, декларувати інтерфейси OLE structured storage як CORBA-інтерфейси з ручним підрахунком посилань, замінити об'єктні файли Win32 AES на реалізацію на Паскалі і полагодити цикл inflate, який міг прийняти обрізаний ZIP як повний

Хто портував зрілу бібліотеку Delphi, той знає форму цієї роботи. Компілятор приймає майже все з першого проходу. Далі йде довгий хвіст поведінкових відмінностей, які компілюються чисто і продукують неправильні результати, а табличний двигун незвично оголений перед ними, бо торкається кодування тексту, COM structured storage, стиснення й криптографії в одному кодовому шляху

Чому ядро наполягає на DELPHIUNICODE, а не на простому DELPHI?

Бо формульний двигун залежить від String і Char, що несуть семантику UTF-16, а ANSI-альтернатива втрачає символи ще до того, як що-небудь досягне файлу. Спокусливо зібрати ядро в режимі FPC DELPHI, адже це перемикач сумісності, за який хватаються більшість портів, і код компілюється. Потім книга з китайськими іменами аркушів чи кириличними мітками робить рейс туди-назад крізь шлях обчислення, і символи зникають до того, як їх побачить райтер, без жодної помилки деінде

Режим не єдиний по всій бібліотеці, і це навмисно, а не недбалість. Байтовий декодер PNG і перевизначення LCL справді потребують ANSI-сигнатур, бо вони мають справу з байтами і з тим, що віддає їм widgetset. Ті юніти вмикають окремий перемикач LX_FPC_ANSI. Два режими в одній бібліотеці звучать як запах, поки не помітиш, що альтернатива — байтовий декодер, який трактує свій вхід як текст

Є супровідна деталь, яка ловить людей пізніше. DELPHIUNICODE не робить TFormatSettings.DecimalSeparator типу WideChar у рантаймі FPC. Вхід, що несе 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.

Юніт Interfaces не опційний і мусить іти першим. Саме він ініціалізує LCL widgetset і шар конвертації UTF-8, і HotXLS покладається на обидва, щойно шрифти, шляхи файлів чи текст перетинають межу RTL і LCL. Консольна програма, яка його пропустить, скомпілюється і буде поводитися неправильно на будь-якому не-ASCII шляху. Це також причина, чому успішна компіляція тут доводить так мало: порт був демонстративно робочим лише тоді, коли реальні документи з реальними іменами шрифтів і реальними шляхами зробили повний рейс туди й назад

Класова VMT — це не COM vtable

Free Pascal не дасть вам віддати класову VMT Windows як vtable COM-інтерфейсу, навіть коли декларація виглядає ідентичною до тієї, яку приймає Delphi. Розкладки відрізняються так, що виклик потрапляє в неправильний слот, що проявляється як падіння десь, ніяк не пов'язаному з місцем виклику. Structured storage має тут значення, бо класичний бінарний формат книги — це складений файл OLE, і читання чи запис одного означає реалізацію ILockBytes, у яку API сховища Windows буде телефонувати назад

Робоча схема — CORBA-інтерфейс із явно декларованими COM-слотами та AddRef і Release під ручним керуванням. Це означає відмову від автоматичного підрахунку посилань для цих типів і взяття відповідальності за час життя — чесний трейд за жменю інтерфейсів, що живуть в одному юніті. Конкретна пастка всередині тієї роботи — QueryInterface: він мусить повернути вказівник на інтерфейс, а не вказівник на об'єкт. Обидва компілюються. Один з них віддає Windows адресу, перша машинна комірка якої — не vtable

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

FPC-специфічні декларації живуть у lxOleInterfaces.inc, поруч із lxAESBackend.inc і lxZlibBackend.inc у каталозі FPC-сирців, тож компіляторно-специфічні вибори сидять в одному місці, а не розкидані крізь двигун. Сам формат і те, як бібліотека ним орієнтується, описані в читанні складених файлів OLE2 на Паскалі

Ще одна типова деталь належить до тієї ж сім'ї. LargeInt мусить розв'язуватися в Int64 у FPC-гілці, і класифікація Comp компілятором відрізняється між двома тулчейнами достатньо, щоб розв'язання перевантажень могло обрати іншого кандидата. Тестуйте поведінку великих зсувів файловим потоком, а не потоком HGLOBAL: потік глобальної пам'яті Windows сам завертається на seek повз 4 ГіБ, тож успішний тест там не доводить нічого про вашу власну арифметику

Що ховає самозгодна реалізація AES

Об'єктні файли Win32 AES, які лінкує збірка Delphi, — це OMF, і лінкер Free Pascal не може їх спожити, тож FPC-гілка використовує натомість Pascal-реалізацію AES. Delphi продовжує лінкувати ті самі об'єктні файли, що й завжди, що тримає випущений бінарник незмінним для наявних клієнтів

Вимога верифікації — та частина, яку варто нести в будь-який проєкт. Зашифрувати дані й розшифрувати їх знову тією ж реалізацією не доводить нічого: симетричний алгоритм із неправильною схемою ключа, неправильним порядком блоків чи неправильним зчепленням ідеально самозгодний і щоразу робитиме рейс туди-назад власного виводу. Ловлять це лише known-answer вектори, що перевіряють розширення ключа, порядок блоків і CBC-зчеплення проти опублікованих значень. Випустіть самозгодну неправильну реалізацію — і симптом з'явиться щоразу, коли клієнт уперше відкриє файл в Excel

Стиснення мало дефект іншого характеру. Pascal-бекенд inflate може мати вивід у запасі після того, як спожив увесь свій стиснутий вхід, тож викликаючий мусить продовжувати викликати, доки потік не повідомить свій кінець. Трактування вичерпаного входу як кінця потоку обрізає останній блок. Гірше — воно перетворює пошкоджений архів на мовчки прийнятий, і це рівно той режим відмови, щоб запобігти якому існує зміцнення з валідації запису ZIP end-of-central-directory. Правило: немає поступу плюс не завершено — це помилка обрізання, ніколи не EOF

Дві пастки системи збірки, що коштували реальних годин

Пошукові шляхи LCL мусять передувати шляхам wildcard пакетів FPC, інакше юніт Menus з Free Vision затіняє юніт LCL з тим самим ім'ям, і ви отримуєте невідповідність контрольної суми PPU, яка не каже нічого ні про те, ні про те. Встановлення Lazarus, яке пересунули після встановлення, також може лишити застарілі шляхи в fpc.cfg, тож точки входу збірки вказують шляхи юнітів і бінарників явно, замість того щоб успадковувати те, що запропонує середовище

Друга пастка не має нічого спільного з Паскалем. Пакетний файл .cmd, написаний із завершеннями рядків LF, працює, доки файл не виросте за розмір буфера читання інтерпретатора, у який момент call :label падає із заявою, що мітка пакетного файлу не існує, і відмова з'являється на тій програмі, яка випадково сидить за межею. Будь-який інструмент, який переписує пакетний скрипт, мусить писати CRLF назад. І lazbuild --build-all чистить каталог виводу юнітів пакету перед компіляцією, тож файл опцій, запаркований у тому каталозі, видаляється до того, як його можна прочитати: тримайте його зовні, і пам'ятайте, що шлях @ розв'язується відносно каталогу пакету, бо lazbuild викликає компілятор звідти

Карта двох шарів небезпек за чистою компіляцією HotXLS під Free Pascal: режим DELPHIUNICODE, що тримає String і Char у UTF-16, аварійний вихід LX_FPC_ANSI для байтового декодера PNG і перевизначень LCL, а також пастки збірки від затінення Menus з Free Vision, застарілих шляхів fpc.cfg, пакетних файлів лише-LF і витирання виводу lazbuild
Перша компіляція доводить мало: карта режимів вирішує, які символи виживуть до райтера, тоді як пастки системи збірки проявляються як невідповідності контрольних сум, фантомні відсутні мітки й файли опцій, видалені до того, як їх прочитають
// Експорт сітки Lazarus: TGridToXLS постачається в пакеті Lazarus, тож
// той самий код експорту DB-grid працює в 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-збірки перетворив ту різницю на два реальні дефекти в юніті обчислення. Одна функція читала змінну лічильника, яка ніколи не була присвоєна до використання, а інша використовувала дві координати в одній гілці до того, як код, який їх обчислював, виконався в іншій гілці. Під Delphi обидві поводилися за тим, що випадково лежало на стеку, — це й є визначення бага, який відтворюється на одній машині й не відтворюється на іншій

Практичний висновок: другий компілятор варто тримати в циклі навіть для продукту, що постачається переважно на першому. Періодичний скан класів попереджень FPC — дешевий прохід статичного аналізу над кодовою базою Delphi, і він знаходить категорію дефектів, до якої жодна тестова сюїта надійно не дотягується. Ширша дисципліна версійної матриці, всередині якої це сидить, описана в крос-компіляторній матриці збірки

Підтримка Free Pascal і Lazarus для Windows постачається з HotXLS Delphi spreadsheet component як пакет Lazarus поруч із пакетами Delphi та C++Builder, зібраний з того самого дерева сирців, а не з форку. У цьому і сенс вправи: один двигун, чотири тулчейни, і компіляторно-специфічні рішення, ізольовані в include-файлах, де їх можна прочитати за один присід