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

Free Pascal Win32: декорирование C-символов в HotPDF

Free Pascal на Win32 автоматически добавляет подчёркивание к каждому импорту cdecl; external, тогда как public name экспортирует ровно ту строку, которую вы написали, символ в символ. HotPDF приходится удовлетворять обе конвенции в одном дереве исходников, потому что Delphi-сборка уже поставляет объявления импортов, где подчёркивание выписано вручную. Ошибка в этой асимметрии даёт ошибки линковки, называющие символ, который никто не писал

Расширение Delphi-библиотеки до Free Pascal обычно описывают как проблему переносимости, и на Win64 это по большей части так и есть. Win32 — другой случай. 32-битный x86 Windows ABI несёт тридцать лет накопленных конвенций о том, как пишутся C-символы, кто чистит стек и какие компиляторо-зависимые хелперы трансляционная единица вправе предполагать, и каждое из этих мест — точка, где два Pascal-компилятора, согласных по языку, могут разойтись по объектному файлу

Почему один и тот же символ резолвится на Win64 и проваливается на Win32?

Потому что подчёркивание — 32-битная конвенция, которую Free Pascal применяет к импортам, но не к экспортам. Объявите function deflate(...): Integer; cdecl; external;, и FPC на Win32 ищет в объектном файле _deflate, а на Win64 — deflate. Это корректное поведение, совпадающее с тем, что выдаёт C-компилятор. Ловушка на другой стороне моста: подпрограмма с public name 'deflate' экспортирует ровно deflate на обоих таргетах, без префикса

Теперь добавьте историческую деталь, делающую всё конкретным. Delphi-сборка уже объявляет часть этих точек входа с подчёркиванием, вписанным в имя, потому что именно так выглядят её собственные объектные файлы. Скормите то же объявление FPC на Win32, и компилятор добросовестно добавит префикс ещё раз, так что линковщик охотится за __deflate — символом, который не экспортирует никто. Интуитивный фикс, добавить по одному подчёркиванию везде, ломает импорты, которые уже были написаны правильно

Работает пара префиксных констант, а не одна. HPDFFPCZLib и HPDFFPCCodecStubs используют один префикс для обычных C-импортов и другой для импортов, уже несущих Delphi-префикс, а на Win64 обе константы пусты, так что существующие имена для линковки выживают нетронутыми. Две константы вместо одной — вот и весь фикс, и он очевиден только после того, как вы разделили правило импорта и правило экспорта

Одни и те же объявления C-символов, разрешаемые Free Pascal и Delphi на Win64 и Win32: cdecl-импорты получают подчёркивание только на 32-битном таргете, объявление Delphi, уже написанное с подчёркиванием, превращается в __deflate и не линкуется, а экспорты public name остаются литеральными на обеих архитектурах
Одна префиксная константа не может обслужить оба правила: обычные cdecl-импорты и импорты, уже несущие Delphi-подчёркивание, декорируются по-разному под FPC на Win32, поэтому HotPDF держит две и оставляет обе пустыми на Win64
// Два префикса, а не один: обычные C-импорты и импорты, которые уже несут
// рукописный Delphi-префикс, декорируются по-разному под FPC/Win32
const
{$IF DEFINED(FPC) and DEFINED(CPU32)}
  CPrefix     = '_';   // FPC добавляет это сам для cdecl external
  DelphiCName = '';    // подчёркивание уже выписано в исходнике
{$ELSE}
  CPrefix     = '';
  DelphiCName = '';
{$IFEND}

// Сторона экспорта: 'public name' литерален на каждом таргете
procedure hpdf_codec_free(P: Pointer); cdecl;
  public name 'hpdf_codec_free';

WIN32 говорит об архитектуре, а не об ABI

Это ошибка условной компиляции с самым длинным хвостом отладки, и её стоит сформулировать прямо: WIN32 и WIN64 описывают целевую архитектуру и не говорят ничего о том, какие компиляторо-зависимые runtime-хелперы существуют. Free Pascal определяет оба символа на соответствующих Windows-таргетах, ровно как Delphi. Охрана {$IFDEF WIN32} вокруг кода, вызывающего runtime-хелпер Delphi, поэтому компилируется под FPC и падает на линковке

Конкретно в эту ловушку попадают три семейства кода. Трамплины 64-битных целочисленных операций Delphi, доступные через хелперы System.@_ll, ассемблерные подпрограммы поддержки Win32 от MSVC и идущие с ними слоты импортов — всё это существует ради прекомпилированных C-объектов, которые линкует Delphi-сборка. Free Pascal эти объекты не линкует, так что ему не нужна ни одна из этих машин, и каждая ссылка на них должна исчезнуть. Тонкость в том, что объявление и реализацию надо исключать вместе. Исключите только одно, и компилятор сообщит что-то бесполезное про идентификатор, который не может ни с чем сопоставить

Правило, которое отсюда следует, короткое. Охраняйтесь по компилятору, когда вопрос про ABI или поддержку runtime, и по архитектуре, когда вопрос про ширину указателя или число регистров, и никогда не позволяйте одному подменять другое

Охранять объявления и реализации вместе

В условный блок секции interface легко попасть, не заметив, и полученное сообщение об ошибке указывает куда угодно, только не на причину. Добавить объявление метода в интерфейс класса — естественное место рядом с родственными методами, и всё хорошо ровно до момента, когда эти соседи оказываются внутри существующего блока {$IFDEF}. Условные директивы не отступают, поэтому блок, открытый сорока строками выше, при чтении окружающих объявлений практически невидим

Дальше происходит сборка, успешная на одном тулчейне и дающая лавину на другом. Если окружающая охрана — проверка версии Delphi, которой Free Pascal не удовлетворяет, объявление исчезает для FPC, а безусловная реализация остаётся, и компилятор выдаёт длинный список претензий к идентификаторам методов, которых он ожидал и не нашёл. Ни одно из сообщений не упоминает условный блок, который всё вызвал

Две привычки предотвращают весь этот класс отказов. Перед вставкой в секцию interface ищите глазами ближайший открытый conditional, а не доверяйте визуальной группировке. И считайте зелёный тестовый прогон Delphi доказательством только про Delphi: сборка библиотеки под Free Pascal — отдельный гейт, и единственный способ узнать, что он проходит, — запускать build-Win32-Lib-FPC.cmd и build-Win64-Lib-FPC.cmd в рамках того же изменения

Что ломается в 32-битном арифметическом коде

Одно языковое ограничение проявляется ровно в том коде, который меньше всего готов меняться: 32-битный Free Pascal не примет UInt64 как управляющую переменную цикла for. В юнитах эллиптических кривых с X25519 и X448 циклы, обходящие массивы лимбов, были написаны с 64-битными счётчиками просто потому, что всё остальное в файле 64-битное

Фикс обязан быть хирургическим, потому что в полевой арифметике ширина переменной — часть аргумента корректности. Индексы циклов становятся Integer: у массива лимбов горстка элементов, и ни один индекс не приближается к 32-битному диапазону. Всё, что участвует в арифметике, — сами лимбы, распространение переноса и маски — остаётся UInt64, потому что сужение любого из них молча меняет результат по модулю простого поля

// 32-битный FPC отвергает переменную цикла UInt64. Сужаем только индекс;
// лимбы, маски и переносы сохраняют ширину, иначе меняется математика поля
var
  I: Integer;                 // было UInt64
  Carry, Mask: UInt64;
begin
  Carry := 0;
  for I := 0 to High(Limbs) do
  begin
    Limbs[I] := Limbs[I] + Carry;
    Carry := Limbs[I] shr 51;
    Limbs[I] := Limbs[I] and Mask;
  end;
end;

Верификацией для такого изменения не может быть round-trip-тест. Шифрование и расшифровка одной и той же сломанной реализацией идеально согласуются друг с другом, поэтому known-answer векторы здесь не обсуждаются: прогоняйте опубликованные тестовые векторы X25519 и X448 и сравнивайте точные выходные байты. Это единственная проверка, отличающая корректную реализацию от самосогласованной неправильной, и она в равной мере относится к симметричным примитивам из границ codec deflate и AES в Free Pascal

Две точки поломки сборки HotPDF Win32 под Free Pascal: охрана {$IFDEF WIN32} вокруг runtime-хелперов Delphi, которая компилируется, но падает на линковке, если объявление и реализация не исключены вместе, и переменная цикла UInt64 в обходах лимбов X25519 и X448, суженная до Integer, тогда как лимбы, переносы и маски сохраняют ширину
Охраняйтесь по компилятору, когда вопрос про ABI или поддержку runtime, и по архитектуре, когда про ширину указателя, а изменения арифметики доказывайте на опубликованных known-answer векторах, а не round-trip-тестами

Чего стоит сборка Win32 под Free Pascal

Практическая выгода: Lazarus-приложение под 32-битную Windows получает тот же движок документов, что и его Delphi-собрат, без отдельного бинарного контракта в поддержке. Это важнее всего для деплоев, о которых редко говорят: промышленные контроллеры, POS-терминалы и долгоживущее линейно-бизнес-ПО, где 32-битный runtime — не легаси-выбор, а аппаратное ограничение

История Win64 случилась раньше и описана в поддержке Free Pascal и Lazarus на Win64. Win32 — не её повторение. У Win64 одно соглашение о вызовах, никакого декорирования имён и никаких Delphi-приватных целочисленных хелперов, которые надо обходить, так что почти всё в этой статье специфично для 32-битного таргета. Арифметические юниты, потребовавшие изменения переменной цикла, — те же, что описаны в арифметике Монтгомери над кривыми NIST, где дисциплина ширины разобрана глубже

Общий урок: работа по межкомпиляторной переносимости — это прежде всего не про языковые возможности. Оба компилятора принимают здесь один и тот же Object Pascal. Различие в объектном файле: как пишутся символы, какие подпрограммы поддержки runtime вправе предполагать и какие прекомпилированные объекты идут в линковку. HotPDF поставляет пакеты Free Pascal и Lazarus рядом с пакетами Delphi и C++Builder в Delphi PDF-компоненте HotPDF, так что одно дерево исходников кормит каждый тулчейн, а не форкается по компиляторам