PDFlibPas se compilează sub Free Pascal pentru Windows pe 32 de biți, iar partea grea n-a fost niciodată Pascal-ul. Au fost fișierele obiect: obiectele AES și OpenJPEG pe care build-ul Delphi le leagă sunt OMF, linker-ul intern al Free Pascal cere COFF, iar conversia dintre cele două produce nume de secțiuni și simboluri de definiție a secțiunilor care fac linker-ul să crape cu erori interne în loc de mesaje de diagnostic
Oricine a legat obiecte C într-o bibliotecă Pascal cunoaște teritoriul ăsta. Win64 e comparativ civilizat: un singur format de obiect, o singură convenție de apel și niciun ornament de nume. Win32 păstrează fiecare strat de istorie pe care platforma l-a acumulat, iar o bibliotecă care leagă static cod C de la terți se lovește de toate deodată
Directorul compilatorului nu vă spune ținta
Începeți cu punctul de intrare al build-ului, pentru că a greși aici pierde ore înainte să fie vorba de vreun fișier obiect. Un nume de director de instalare Free Pascal identifică unde locuiește compilatorul principal, nu ce produce. Un compilator gazdă pe 32 de biți poate invoca un cross-compiler aflat lângă el și poate emite cod pe 64 de biți când îi treceți switch-urile de țintă potrivite, deci a deduce ținta dintr-o cale e o ghicire care merge până când cineva își rearanjează toolchain-ul
Abordarea sigură e să întrebați compilatorul. Interogați procesorul-țintă efectiv și sistemul de operare prin switch-urile proprii de informare ale compilatorului și acceptați ambele aranjamente uzuale de instalare, directorul binar plat și cel imbricat pe versiuni, pentru că instalatori și administratori de toolchain diferiți produc forme diferite. Un script de build cu aranjament hard-codat merge pe exact o singură mașină
De ce rupe un fișier obiect convertit linker-ul intern?
Pentru că conversia păstrează convenția OMF de numire a secțiunilor și sintetizează simboluri de definiție a secțiunilor care nu seamănă cu ce așteaptă linker-ul COFF. Conversia obiectelor OMF în COFF e necesară și insuficientă: fișierele rezultate poartă numele clasice de secțiune _TEXT, _DATA și _BSS, plus numele de simboluri de definiție a secțiunilor derivate din ele, iar dacă îi dați asta linker-ului intern Free Pascal, obțineți erori interne de compilator în locul unui mesaj despre numirea secțiunilor
O eroare internă e cel mai rău mod de a eșua pentru o problemă de build, pentru că nu spune nimic despre ce era în neregulă cu intrarea. Remedierea e o trecere de normalizare post-conversie peste fișierul COFF: rescrieți numele secțiunilor în forma așteptată și rescrieți simbolurile de definiție a secțiunilor corespunzătoare ca să se potrivească, lăsând neatinse indexul de simboluri, octeții de cod și relocațiile. Ultima constrângere e întreaga dificultate. O rescriere care renumerotează simboluri sau deplasează offseturi produce un obiect care se leagă și apoi crapă
Există un pas preliminar pentru unul dintre cele două seturi de obiecte. Obiectele OpenJPEG construite de compilatorul C++ clasic pe 32 de biți depind de rutine private Delphi pentru întregi pe 64 de biți, pe care Free Pascal nu le furnizează, deci nicio cantitate de conversie de format nu le face utilizabile. Acelea se reconstruiesc mai întâi cu compilatorul bazat pe Clang, care nu emite acele dependențe, și se convertesc după aceea
// Obiectele pentru ținta FPC stau în directorul lor propriu. Ele nu înlocuiesc
// setul de obiecte Delphi, pentru că ambele toolchain-uri compilează din
// același arbore de surse și fiecare are nevoie de propriile intrări de linkare
//
// Lib\thirdparty\Win32 obiecte Delphi OMF, nemodificate
// Lib\thirdparty\Win32f obiecte FPC COFF, convertite și normalizate
//
// Puncte de intrare ale build-ului:
// build-Win32-Lib-FPC.cmd
// build-Win64-Lib-FPC.cmd
Helperii privați ai compilatorului nu sunt portabili, iar convențiile lor nici atât
Runtime-ul Delphi furnizează trambuline în asamblare pentru operațiile cu întregi pe 64 de biți pe x86-ul pe 32 de biți, iar obiectele C precompilate construite pentru Delphi apelează spre ele. Free Pascal are propriul aranjament, deci acele referințe trebuie satisfăcute altfel, nu redirecționate. Detaliul care face redirecționarea imposibilă e convenția de apel: helper-ul de timing folosit de codul de imagistică are argumentul său de patru octeți curățat de callee, pe când helper-ul de împărțire pe 64 de biți curăță șaisprezece octeți și își întoarce rezultatul în perechea clasică de registre. Doi helperi, două convenții, iar o trambulină scrisă pentru unul corupe tăcut stiva pentru celălalt
Ornamentarea numelor aduce a doua jumătate a problemei. Pe Win32, Free Pascal prefixează automat cu underscore importurile C externe, în timp ce declarațiile public name le exportă întocmai, deci partea de import și partea de export ale aceleiași punți urmează reguli diferite. Puntea către runtime-ul C de care are nevoie OpenJPEG trebuie deci să exporte numele exacte de simboluri C, iar punctele de intrare variadice au nevoie de un salt indirect pe 32 de biți, nu unul direct. Nimic exotic odată enunțat. Totul eșuează ca eroare de linkare care numește un simbol pe care nu l-a scris nimeni
Ce făcea un executabil Win32 să moară înainte de main?
Un DLL pe 64 de biți aflat pe calea de căutare, atins pentru că unitatea zlib Free Pascal se leagă dinamic în loc să lege static. Simptomul era o ieșire imediată cu codul de stare de imagine invalidă, înainte să ruleze vreun cod Pascal din program, ceea ce vă trimite să priviți programul pe care tocmai l-ați compilat când vina e a loader-ului care rezolvă un import contra arhitecturii greșite
Lecția e despre presupuneri, nu despre zlib. O unitate numită după o bibliotecă de compresie nu conține neapărat una; poate fi o legătură care așteaptă o bibliotecă partajată la rulare, iar o dependență dinamică pe care nu ați vrut-o e o răspundere de deploy chiar și când se rezolvă din întâmplare. Trecerea la implementarea în Pascal pur a stream-ului dă ambelor ținte o cale de compresie inclusă static, fără nicio dependență externă, ceea ce ar trebui să aibă oricum o bibliotecă încorporată în aplicația altcuiva
Același instinct se aplică și backend-ului extern de encodare JBIG2. Pe ținta pe 32 de biți encoder-ul extern nu e legat, deci cererile cad pe encoder-ul Pascal integrat, iar testul care verifică asta trebuie să consulte starea de înregistrare a țintei curente, nu să trateze un encode reușit ca dovadă a prezenței backend-ului extern. Un fallback care funcționează e exact lucrul care ascunde o dependență lipsă, iar acest tipar de eșec e examinat în diagnosticarea eșecurilor tăcute de stub. Lucrarea de linkare statică pe 64 de biți e acoperită în linkarea statică jbig2enc sub FPC
Aritmetică pe 32 de biți peste un memory stream
Codul care manipulează dimensiunile de buffer cu aritmetică nesemnată pe lățimea pointerului e corect pe Win64 și la o imagine mare distanță de overflow pe Win32. Stream-ul din memorie care hrănește codec-ul JPEG 2000 crește prin dublare și avansează prin adunare, iar pe o țintă pe 32 de biți ambele operații se pot înfășura la intrări mari, dar pe deplin legitime
Fiecare scriere, salt, seek și alocare inițială verifică deci înainte să calculeze, iar plafonul de capacitate e valoarea maximă semnată pe lățimea pointerului, aleasă să se potrivească cu ce pot exprima rutina de mutare a blocurilor și valorile returnate de callback. Cerința comportamentală când o cerere e refuzată e ușor de greșit: refuzul nu trebuie să schimbe poziția stream-ului nici lungimea lui. O mutație parțială urmată de o eroare lasă stream-ul într-o stare în care apelantul nu poate raționa, iar operația următoare agravează lucrurile
// Verificați înainte să calculați. Pe Win32 ambele se înfășoară la
// intrările pe care le produce legitim o imagine JPEG 2000 mare
if Needed > NativeUInt(High(NativeInt)) - FPosition then
Exit(False); // refuză, lasă poziția și dimensiunea neatinse
NewCapacity := FCapacity;
while (NewCapacity < FPosition + Needed) do
begin
if NewCapacity > NativeUInt(High(NativeInt)) shr 1 then
Exit(False); // dublarea ar da overflow
NewCapacity := NewCapacity shl 1;
end;
Două capcane de output de build care supraviețuiesc portării
Separarea executabilelor de test și de exemplu după arhitectura țintă în directoare de output per-țintă e evident corectă și strică imediat orice își găsea datele de test numărând niveluri de director în sus. Remedierea e să căutați în sus directorul de resurse în loc să presupuneți o adâncime fixă, cu o restricție deliberată: exemplul de semnare acceptă un certificat de rezervă doar din propriul său director de proiect, niciodată dintr-un strămoș arbitrar, pentru că un certificat cu același nume aflat mai sus în arbore e o surpriză de securitate, nu o comoditate
A doua capcană supraviețuiește fiecărei portări și merită dusă în orice proiect FPC. După un upgrade de compilator, nu e de ajuns ca compilatorul să respingă fișierele PPU perimate, pentru că linker-ul preferă în continuare fișierele obiect rămase în calea de căutare a unităților chiar și când PPU-ul încărcat a venit din directorul corect, iar adăugarea unei căi explicite de output de obiecte nu întrece această preferință. Singurul răspuns sigur e un director temporar de unități, proaspăt la fiecare rundă de build. Orice mai puțin produce un binar legat din două versiuni de compilator, care eșuează în feluri care arată a bug-uri de sursă
Condiționalele de platformă sunt ultima bucată, iar alegerea axei potrivite contează mai mult decât pare. Întrebarea corectă e de obicei dacă codul e specific Windows, nu dacă o anumită bibliotecă de widget-uri e prezentă, așa cum a arătat lucrarea de conversie metafile din importul vectorial EMF și condiționalele de platformă: mutarea acelei gărzi de la o condiție de bibliotecă de controale la o condiție de platformă a transformat o presupusă rescriere într-o schimbare de o directivă. Suportul Free Pascal și Lazarus pentru ambele ținte Windows vine cu PDFlibPas, biblioteca PDF pentru Delphi, construită din aceleași surse ca pachetele Delphi și C++Builder