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

Free Pascal Win32: прикрашання C-символів у HotPDF

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-символів, розв'язані 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 описують цільову архітектуру й не кажуть нічого про те, які приватні для компілятора рантайм-хелпери існують. 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

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

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