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-импорты и импорты, которые уже несут
// рукописный 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
Чего стоит сборка 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, так что одно дерево исходников кормит каждый тулчейн, а не форкается по компиляторам