Free Pascal на Win32 автоматично додає початкове підкреслення до кожного імпорту cdecl; external, тоді як public name експортує той рядок, який ви написали, символ у символ. HotPDF мусить задовольнити обидві конвенції в одному дереві сирців, бо збірка Delphi вже постачає оголошення імпортів, де підкреслення виписане руками. Помилка в цій асиметрії дає помилки лінкування, які називають символ, що його ніхто не писав
Розширення бібліотеки Delphi на Free Pascal зазвичай описують як проблему переносимості, і на Win64 це переважно так і є. Win32 інший. 32-бітний ABI Windows на x86 несе тридцять років накопиченої конвенції про те, як пишуться C-символи, хто очищає стек і які приватні для компілятора хелпери трансляційна одиниця має право припускати, і кожен із цих пунктів — місце, де два паскалівські компілятори, згодні щодо мови, можуть розійтися в об'єктному файлі
Чому той самий символ розв'язується на Win64 і падає на Win32?
Бо префікс підкреслення — це 32-бітна конвенція, яку Free Pascal застосовує до імпортів, але не до експортів. Оголосіть function deflate(...): Integer; cdecl; external; — і FPC шукає _deflate в об'єктному файлі на Win32 і deflate на Win64. Це коректна поведінка, що збігається з тим, що продукує 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 описують цільову архітектуру й не кажуть нічого про те, які приватні для компілятора рантайм-хелпери існують. Free Pascal визначає обидва символи на відповідних цілях Windows, рівно як Delphi. Guard, написаний як {$IFDEF WIN32} навколо коду, що викликає рантайм-хелпер Delphi, отже, компілюється під FPC і падає під час лінкування
Конкретно, три сімейства коду падають у цю пастку. Трампліни 64-бітових цілочисельних операцій Delphi, досяжні через хелпери System.@_ll, асемблерні процедури підтримки Win32 від MSVC та імпортні слоти, які з ними йдуть, — усе це існує, щоб обслуговувати передзібрані C-об'єкти, які лінкує збірка Delphi. Free Pascal ці об'єкти не лінкує, тож йому не потрібен жоден з цього механізму, і кожне посилання на нього має зникнути. Тонкість у тому, що оголошення й реалізацію треба виключати разом. Виключите лише одне — і компілятор видасть щось некорисне про ідентифікатор, який не може ні з чим зіставити
Правило, яке звідси випливає, коротке. Guard на компілятор, коли питання про ABI чи рантайм-підтримку; guard на архітектуру, коли питання про ширину покажчика чи кількість регістрів; і ніколи не дозволяйте одному заступати іншого
Guard на оголошення та реалізації разом
В умовний блок у секції інтерфейсу легко впасти непомітно, і отримане повідомлення про помилку вказує будь-куди, тільки не на причину. Додаєте оголошення методу до класового інтерфейсу — і природне місце для нього поруч зі спорідненими методами, що добре аж до моменту, коли ці сусіди сидять у наявному блоці {$IFDEF}. Умовні директиви не мають відступів, тож блок, відкритий сорок рядків вище, фактично невидимий, поки ви читаєте навколишні оголошення
Далі стається компіляція, успішна на одному тулчейні і даюча каскад на іншому. Якщо навколишній guard — перевірка версії Delphi, яку Free Pascal не задовольняє, оголошення зникає для FPC, тоді як безумовна реалізація лишається, і компілятор видає довгий список скарг на ідентифікатори методів, які він очікував і не знайшов. Жодне з повідомлень не згадує умовний блок, що це спричинив
Дві звички запобігають усьому цьому класу відмов. Перед вставкою в секцію інтерфейсу подивіться вгору на найближчий відкритий умовний блок, замість того щоб довіряти візуальному групуванню. І вважайте зелену тестову сюїту Delphi доказом лише щодо Delphi: білд бібліотеки Free Pascal — окремі ворота, і єдиний спосіб знати, що вони пройдені, — запускати build-Win32-Lib-FPC.cmd і build-Win64-Lib-FPC.cmd у складі тієї самої зміни
Що ламається в 32-бітному арифметичному коді
Одне мовне обмеження виявляється рівно в тому коді, який найменше охоче змінюватися: 32-бітний Free Pascal не прийме UInt64 як керівну змінну циклу for. В юнітах еліптичних кривих, які несуть X25519 і X448, цикли, що проходять масиви limb, були написані з 64-бітними лічильниками просто тому, що все інше в файлі 64-бітне
Фікс мусить бути хірургічним, бо в польовій арифметиці ширина змінної — частина аргументу коректності. Індекси циклів стають Integer, адже масив limb має кілька елементів, і жоден індекс не підступає до 32-бітного діапазону. Усе, що бере участь в арифметиці, — самі limb, перенесення carry і маски — лишається UInt64, бо звуження будь-чого з цього мовчки змінює результат за модулем простого поля
// 32-бітний FPC відхиляє змінну циклу UInt64. Звужуйте лише індекс;
// limbs, маски й переноси зберігають ширину, інакше зміниться математика поля
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 і порівнюйте точні вихідні байти. Це єдина перевірка, що відрізняє коректну реалізацію від самозгодженої неправильної, і вона так само стосується симетричних примітивів, розглянутих у межах кодеків deflate і AES у Free Pascal
Чого вартий білд Win32 під Free Pascal
Практична вигода в тому, що застосунок Lazarus під 32-бітний Windows отримує той самий документний рушій, що й його побратим на Delphi, без окремого бінарного контракту для підтримки. Це найважливіше для деплоїв, про які рідко говорять: промислові контролери, POS-термінали й довговічне лінійно-бізнесове ПЗ, де 32-бітний рантайм — не легасі-вибір, а апаратне обмеження
Історія Win64 була першою і описана в підтримці Free Pascal і Lazarus на Win64. Win32 — не повторення цього. Win64 має одну конвенцію виклику, жодного прикрашання імен і жодних приватних для Delphi цілочисельних хелперів, які треба обходити, тож майже все в цій статті специфічне для 32-бітної цілі. Арифметичні юніти, яким потрібна була зміна змінної циклу, — ті самі, що описані в арифметиці Монтгомері над кривими NIST, де дисципліна ширини пояснена глибше
Загальний урок: робота з крос-компіляторною переносимістю — це передусім не мовні можливості. Обидва компілятори приймають той самий Object Pascal. Відрізняється об'єктний файл: як пишуться символи, які хелперні процедури рантайм вважається наданим, і які передзібрані об'єкти в лінкуванні. HotPDF постачає пакети Free Pascal і Lazarus поруч із пакетами Delphi та C++Builder у HotPDF Delphi PDF component, тож одне дерево сирців годує кожен тулчейн замість форкування на компілятор