HotXLS собирается под Free Pascal и Lazarus на Windows, и порт свёлся к четырём решениям, которые не имеют отношения к синтаксису Object Pascal: держать ядро в режиме DELPHIUNICODE, объявить OLE structured-storage интерфейсы как CORBA-интерфейсы с ручным подсчётом ссылок, заменить Win32 объектные файлы AES Pascal-реализацией и починить цикл inflate, способный принять обрезанный ZIP за полный
Кто переносил зрелую Delphi-библиотеку, знает форму этой работы. Компилятор принимает почти всё с первого прохода. Дальше идёт длинный хвост поведенческих различий, которые чисто компилируются и дают неправильные результаты, и движок таблиц к ним необычно уязвим, потому что трогает кодировку текста, COM structured storage, сжатие и криптографию в одном пути кода
Почему ядро настаивает на DELPHIUNICODE, а не на простом DELPHI?
Потому что формульный движок зависит от String и Char с семантикой UTF-16, а ANSI-альтернатива теряет символы ещё до того, как что-то дойдёт до файла. Соблазнительно собрать ядро в FPC-режиме DELPHI: это переключатель совместимости, за которым тянется большинство портов, и код компилируется. Потом книга с китайскими именами листов или кириллическими подписями проходит туда-обратно через путь вычислений, и символы исчезают к моменту, когда их видит писатель, — без единой ошибки где-либо
Режим не единообразен по всей библиотеке, и это намеренно, а не неряшливо. Байтовый декодер PNG и оверрайды LCL честно нуждаются в ANSI-сигнатурах, потому что имеют дело с байтами и с тем, что им отдаёт widgetset. Те юниты включают отдельный переключатель LX_FPC_ANSI. Два режима в одной библиотеке звучат как code smell, пока не заметишь, что альтернатива — байтовый декодер, трактующий свой ввод как текст
Есть сопутствующая деталь, которая ловит людей позже. 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 compound file, и читать или писать его — значит реализовать ILockBytes, в который Windows storage API будет звонить
Рабочая схема — CORBA-интерфейс с явно объявленными COM-слотами и ручным управлением AddRef и Release. Это отказ от автоматического подсчёта ссылок для этих типов и принятие ответственности за время жизни — честная сделка для горстки интерфейсов, живущих внутри одного юнита. Конкретная ловушка внутри этой работы — QueryInterface: он обязан вернуть указатель на интерфейс, а не указатель на объект. Оба компилируются. Один из них вручает Windows адрес, чьё первое машинное слово — не vtable
FPC-специфичные объявления живут в lxOleInterfaces.inc, рядом с lxAESBackend.inc и lxZlibBackend.inc в каталоге FPC-исходников, так что привязанные к компилятору решения сидят в одном месте, а не раскиданы по движку. Сам формат и то, как библиотека по нему ходит, описаны в чтении OLE2 compound files в Pascal
Ещё одна типовая деталь из того же семейства. LargeInt обязан резолвиться в Int64 в FPC-ветке, а классификация Comp компилятором различается между тулчейнами достаточно, чтобы разрешение перегрузок выбрало другого кандидата. Тестируйте поведение с большими смещениями файловым стримом, а не HGLOBAL-стримом: стрим глобальной памяти Windows сам заворачивается на seek за 4 GiB, так что проходящий там тест не доказывает ничего про вашу арифметику
Что прячет самосогласованная AES-реализация
Win32 объектные файлы AES, которые линкует Delphi-сборка, — это OMF, и линковщик Free Pascal их не переваривает, поэтому FPC-ветка использует Pascal-реализацию AES. Delphi продолжает линковать те же объектные файлы, что и всегда, так что релизный бинарник для существующих клиентов не меняется
Требование верификации — та часть, что стоит унести в любой проект. Зашифровать данные и расшифровать их той же реализацией не доказывает ничего: симметричный алгоритм с неправильным key schedule, неправильным порядком блоков или неправильным сцеплением идеально самосогласован и каждый раз проходит свой же вывод туда-обратно. Ловят это только known-answer векторы, сверяющие расширение ключа, порядок блоков и CBC-сцепление с опубликованными значениями. Отгрузите самосогласованную неправильную реализацию — и симптом появится, когда клиент впервые откроет файл в Excel
У сжатия был дефект другого характера. Pascal-бэкенд inflate может иметь невыполненный вывод после того, как съел весь сжатый ввод, поэтому вызывающий обязан звать его, пока стрим не отрапортует конец. Считать исчерпание ввода концом стрима — значит обрезать последний блок. Хуже, это превращает повреждённый архив в тихо принятый, — ровно тот режим отказа, против которого существует харденинг из валидации записи ZIP end-of-central-directory. Правило: нет прогресса плюс не закончено — это ошибка усечения, никогда не EOF
Две ловушки системы сборки, стоившие реальных часов
Пути поиска LCL обязаны предшествовать wildcard-путям пакетов FPC, иначе юнит Menus из Free Vision затеняет одноимённый юнит LCL, и вы получаете несовпадение контрольной суммы PPU, не говорящее ничего ни о том, ни о другом. Установка Lazarus, которую двигали после установки, тоже может оставить протухшие пути в fpc.cfg, поэтому точки входа сборки задают пути юнитов и бинарников явно, а не наследуют то, что предлагает окружение
Вторая ловушка не имеет отношения к Pascal. Батник .cmd, написанный с LF-концами строк, работает, пока файл не вырастет за размер буфера чтения интерпретатора, после чего call :label падает с заявлением, что метки батника не существует, и отказ проявляется на той программе, которая случайно оказалась за границей. Любой инструмент, переписывающий батник, обязан писать CRLF обратно. И lazbuild --build-all чистит выходной каталог юнитов пакетов перед компиляцией, так что файл опций, припаркованный в этом каталоге, удаляется раньше, чем его прочитают: держите его снаружи и помните, что путь @ резолвится относительно каталога пакета, потому что 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 поставляется с Delphi spreadsheet-компонентом HotXLS в виде Lazarus-пакета рядом с пакетами Delphi и C++Builder, собираясь из того же дерева исходников, а не из форка. В этом смысл всего упражнения: один движок, четыре тулчейна, и компиляторо-зависимые решения изолированы в include-файлах, которые можно прочитать за один присест