Articol tehnic

Free Pascal Win32: ornamentarea simbolurilor C în HotPDF

Free Pascal pe Win32 adaugă automat un underscore în fața fiecărui import cdecl; external, în timp ce public name exportă șirul pe care l-ați scris, caracter cu caracter. HotPDF trebuie să satisfacă ambele convenții în același arbore de surse, pentru că build-ul Delphi livrează deja declarații de import care își scriu underscore-ul de mână. A greși această asimetrie produce erori de linkare care numesc un simbol pe care nu l-a scris nimeni

Extinderea unei biblioteci Delphi către Free Pascal e descrisă de obicei ca o problemă de portabilitate, iar pe Win64 asta este, în cea mai mare parte. Win32 e altfel. ABI-ul Windows x86 pe 32 de biți poartă treizeci de ani de convenție acumulată despre cum se scriu simbolurile C, cine curăță stiva și ce helperi privați de compilator are voie să presupună o unitate de traducere, iar fiecare dintre acestea e un loc în care doi compilatori Pascal de acord asupra limbajului pot fi încă în dezacord asupra fișierului obiect

De ce același simbol se rezolvă pe Win64 și pică pe Win32?

Pentru că prefixul underscore e o convenție pe 32 de biți pe care Free Pascal o aplică importurilor, dar nu și exporturilor. Declarați function deflate(...): Integer; cdecl; external; și FPC caută _deflate în fișierul obiect pe Win32, și deflate pe Win64. E comportament corect și se potrivește cu ce emite un compilator C. Capcana e pe partea cealaltă a punții: o rutină marcată public name 'deflate' exportă întocmai deflate pe ambele ținte, fără niciun prefix adăugat

Acum adăugați detaliul istoric care face totul concret. Build-ul Delphi declară deja o parte dintre aceste puncte de intrare cu underscore-ul scris în nume, pentru că așa arată propriile sale fișiere obiect. Dați aceeași declarație lui FPC pe Win32 și compilatorul o prefixează cu conștiință din nou, deci linker-ul vânează __deflate, un simbol pe care nu îl exportă nimic. Remedierea intuitivă, câte un underscore peste tot, strică importurile care erau deja scrise corect

Ce funcționează e o pereche de constante de prefix, nu una singură. HPDFFPCZLib și HPDFFPCCodecStubs folosesc un prefix pentru importurile C obișnuite și altul pentru importurile care poartă deja un prefix din partea Delphi, iar pe Win64 ambele constante sunt goale, deci numele de linkare existente supraviețuiesc neatinse. Două constante în loc de una e întreaga corecție, și devine evidentă doar după ce ați despărțit regula de import de regula de export

Aceleași declarații de simboluri C rezolvate de Free Pascal și Delphi pe Win64 și Win32: importurile cdecl câștigă un underscore doar pe ținta pe 32 de biți, o declarație Delphi deja scrisă cu underscore devine __deflate și nu mai leagă, în timp ce exporturile public name rămân întocmai pe ambele arhitecturi
Constanta de prefix nu poate servi ambele reguli: importurile cdecl obișnuite și importurile care poartă deja underscore-ul Delphi se decorează diferit sub FPC pe Win32, deci HotPDF ține două și le lasă pe ambele goale pe Win64
// Doi prefixe, nu unul singur: importurile C obișnuite și importurile care
// poartă deja un prefix Delphi scris de mână se decorează diferit sub FPC/Win32
const
{$IF DEFINED(FPC) and DEFINED(CPU32)}
  CPrefix     = '_';   // FPC adaugă el însuși pentru cdecl external
  DelphiCName = '';    // deja scris cu underscore în sursă
{$ELSE}
  CPrefix     = '';
  DelphiCName = '';
{$IFEND}

// Partea de export: 'public name' e întocmai pe fiecare țintă
procedure hpdf_codec_free(P: Pointer); cdecl;
  public name 'hpdf_codec_free';

WIN32 îți spune arhitectura, nu ABI-ul

Aceasta e greșeala de compilație condiționată cu cea mai lungă coadă de depanare și merită enunțată simplu: WIN32 și WIN64 descriu arhitectura țintă și nu spun nimic despre ce helperi privați de runtime ai compilatorului există. Free Pascal definește ambele simboluri pe țintele Windows corespunzătoare, exact ca Delphi. O gardă scrisă {$IFDEF WIN32} în jurul codului care apelează un helper de runtime Delphi se compilează deci sub FPC și pică la linkare

Concret, trei familii de cod cad în capcana asta. Trambulinele Delphi pentru întregi pe 64 de biți, atinse prin helperii System.@_ll, rutinele de suport în asamblare MSVC Win32 și sloturile de import care vin cu ele există toate pentru a servi obiectele C precompilate pe care build-ul Delphi le leagă. Free Pascal nu leagă acele obiecte, deci nu are nevoie de nicio mașinărie din ele, iar fiecare referință la ea trebuie să dispară. Subtilitatea e că declarația și implementarea trebuie excluse împreună. Excludeți doar una și compilatorul raportează ceva neajutător despre un identificator pe care nu îl poate potrivi cu nimic

Regula care rezultă e scurtă. Puneți garda pe compilator când întrebarea e despre ABI sau suport de runtime, pe arhitectură când întrebarea e despre lățimea pointerului sau numărul de registre și nu lăsați niciodată una să țină locul celeilalte

Protejarea declarațiilor și implementărilor împreună

Un bloc condiționat în secțiunea de interfață e ușor de pățit fără să observi, iar mesajul de eroare rezultat arată oriunde, numai la cauză nu. Adăugați o declarație de metodă interfeței unei clase și locul natural e lângă metodele înrudite, ceea ce merge foarte bine până în momentul în care vecinii aceia stau de fapt într-un bloc {$IFDEF} existent. Directivele condiționate nu sunt indentate, deci un bloc deschis cu patruzeci de linii mai sus e practic invizibil în timp ce citiți declarațiile din jur

Ce urmează e o compilație care reușește pe un toolchain și produce o cascadă pe altul. Dacă garda din jur e o verificare de versiune Delphi pe care Free Pascal nu o satisface, declarația dispare pentru FPC în timp ce implementarea necondiționată rămâne, iar compilatorul raportează o listă lungă de plângeri despre identificatori de metode pe care îi aștepta și nu i-a găsit. Niciun mesaj nu pomenește blocul condiționat care a cauzat totul

Două obiceiuri previn întreaga clasă de eșecuri. Înainte de a insera într-o secțiune de interfață, priviți în sus după cea mai apropiată condiționată deschisă, în loc să aveți încredere în gruparea vizuală. Și tratați o suită de test Delphi verde ca dovadă doar despre Delphi: build-ul de bibliotecă Free Pascal e o poartă separată, iar singurul mod de a ști că trece e să rulați build-Win32-Lib-FPC.cmd și build-Win64-Lib-FPC.cmd ca parte a aceleiași modificări

Ce se rupe în codul aritmetic pe 32 de biți

O restricție de limbaj apare exact în codul cel mai puțin dispus să se schimbe: Free Pascal pe 32 de biți nu acceptă un UInt64 ca variabilă de control a unei bucle for. În unitățile de curbe eliptice care poartă X25519 și X448, buclele care străbat vectorii de limbs au fost scrise cu contoare pe 64 de biți pur și simplu pentru că tot restul fișierului e pe 64 de biți

Corecția trebuie să fie chirurgicală, pentru că în aritmetica pe corp finit lățimea unei variabile face parte din argumentul de corectitudine. Indecșii de buclă devin Integer, întrucât un vector de limbs are o mână de elemente și niciun index nu se apropie de intervalul pe 32 de biți. Tot ce participă la aritmetică — limbs în sine, propagarea carry-ului și măștile — rămâne UInt64, pentru că îngustarea oricăreia dintre ele schimbă tăcut rezultatul modulo numărul prim al corpului

// FPC pe 32 de biți respinge o variabilă de buclă UInt64. Îngustează doar indexul;
// limbs, măști și carry își păstrează lățimea, altfel se schimbă matematica corpului finit
var
  I: Integer;                 // era 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;

Verificarea pentru o astfel de schimbare nu poate fi un test de du-te-vino. Criptarea și decriptarea cu aceeași implementare stricată se de acord perfect cu sine, iar asta e exact motivul pentru care vectorii known-answer nu sunt negociabili aici: rulați vectorii de test publicați pentru X25519 și X448 și comparați octeții de ieșire exacti. E singura verificare care distinge o implementare corectă de una greșită, dar auto-consecventă, și se aplicază la fel primitivelor simetrice discutate în granițele codec-urilor deflate și AES în Free Pascal

Cele două puncte de rupere ale unui build HotPDF Win32 Free Pascal: o gardă {$IFDEF WIN32} în jurul helperilor de runtime Delphi care se compilează dar pică la linkare dacă declarația și implementarea nu sunt excluse împreună, și variabila de buclă UInt64 din străbaterile de limbs X25519 și X448 îngustată la Integer, în timp ce limbs, carry și măști își păstrează lățimea
Puneți garda pe compilator când întrebarea e despre ABI sau suport de runtime și pe arhitectură când e despre lățimea pointerului, apoi dovediți schimbările aritmetice contra vectorilor known-answer publicați, nu prin teste de du-te-vino

Ce valorează un build Win32 Free Pascal

Beneficiul practic e că o aplicație Lazarus țintită pe Windows pe 32 de biți primește același motor de documente ca omologul ei Delphi, fără un contract binar separat de întreținut. Contează cel mai mult pentru implementările despre care oamenii vorbesc rar: controlere industriale, terminale de punct de vânzare și softuri de business longeve în care runtime-ul pe 32 de biți nu e o alegere de moștenire, ci o constrângere de hardware

Povestea Win64 a venit prima și e descrisă în suportul Free Pascal și Lazarus pe Win64. Win32 nu e o repetare a ei. Win64 are o singură convenție de apel, nicio ornamentare de nume și niciun helper de întregi privat Delphi de ocolit, deci aproape tot ce e în articolul acesta e specific țintei pe 32 de biți. Unitățile aritmetice care au avut nevoie de schimbarea variabilei de buclă sunt aceleași descrise în aritmetica Montgomery peste curbele NIST, unde disciplina lățimii e explicată mai în profunzime

Lecția generală e că munca de portabilitate între compilatoare nu ține în primul rând de trăsăturile limbajului. Ambele compilatoare acceptă aici același Object Pascal. Ce diferă e fișierul obiect: cum se scriu simbolurile, ce rutine helper se presupune că furnizează runtime-ul și ce obiecte precompilate stau în linkare. HotPDF livrează pachetele Free Pascal și Lazarus alături de cele Delphi și C++Builder în HotPDF Delphi PDF component, astfel încât același arbore de surse hrănește fiecare toolchain, în loc să se bifurce per compilator