O legătură (binding) Pascal peste o bibliotecă C se citește ca un cod Pascal obișnuit. Apelați o metodă, primiți o înregistrare înapoi, eliberați ceea ce ați alocat. Problema este că PDFium este o bibliotecă C și C++ cu propria convenție de apelare, propriile lățimi de întregi și propriile reguli despre cine deține memoria și cine o eliberează. Nimic din toate acestea nu trece granița lingvistică de la sine. Fiecare dintre aceste contracte trebuie reafirmat manual în declarațiile Pascal, iar un singur cuvânt greșit transformă un apel aparent curat într-o corupere a stivei, un offset trunchiat sau o eliberare dublă (double free). Un audit v1.61.0 al unei legături PDFium VCL a scos la iveală câte un defect de fiecare tip. Merită parcurse deoarece nu sunt specifice acestei legături. Ele sunt pericolele permanente ale împachetării oricărui API C în Delphi sau Lazarus.
cdecl face parte din tipul funcției, nu este o decorație
PDFium este cod C compilat. Pe Win32, exporturile sale și, mai important, apelurile inverse (callbacks) pe care le invocă folosesc convenția de apelare cdecl. Sub cdecl, apelantul curăță stiva după ce apelul revine. Valoarea implicită nativă a Delphi este register, iar standardul Win32 C pentru apelurile inverse este stdcall în unele biblioteci, unde în schimb apelatul curăță stiva. Când o structură transmite PDFium un indicator de funcție și uitați directiva cdecl pe tipul acelui indicator, cele două părți nu se înțeleg cu privire la cine ajustează indicatorul de stivă (stack pointer). Ambii îl corectează, sau niciunul, iar indicatorul de stivă deviază cu dimensiunea argumentelor la fiecare invocare.
Motivul pentru care acest defect este greu de găsit este că dauna este non-locală. Apelul corupt returnează și pare în regulă. Alinierea greșită apare mai târziu, în unele funcții fără legătură al căror cadru (frame) se află acum pe un indicator de stivă care este decalat cu câțiva octeți și se manifestă ca o citire eronată, o adresă de returnare greșită sau un blocaj cu o urmărire inversă (backtrace) care nu indică nicăieri aproape de apelul invers pe care l-ați greșit de fapt. Completarea formularelor este locul clasic în care se întâmplă acest lucru, deoarece interfața de completare a formularelor este o înregistrare plină de apeluri inverse pe care PDFium le apelează. Unul dintre ele, FFI_OpenFile, transmite PDFium o funcție pe care o va apela pentru a deschide un fișier extern, declarată ca function(pThis: PFPDF_FORMFILLINFO; fileFlag: Integer; wsURL: FPDF_WIDESTRING; mode: PAnsiChar): PFPDF_FILEHANDLER; cdecl. Directiva finală cdecl este punctul care merită copiat. Renunțați la ea și codul se compilează în continuare, se conectează în continuare și rulează chiar până când PDFium apelează funcția. Convenția aparține tipului de funcție în sine. Nu este un element opțional, iar compilatorul nu vă va avertiza când lipsește, deoarece un tip de funcție simplu este un tip Pascal perfect legal. Singura apărare este de a trata convenția de apelare ca pe un câmp obligatoriu al fiecărei semnături importate și al fiecărui apel invers pe care îl transmiteți în exterior.
size_t are lățimea unui pointer, iar pe FPC Win64 aceasta înseamnă 64 de biți
Al doilea defect este o nepotrivire a lățimii întregilor care apare doar pe o singură platformă țintă. Tipul size_t din C este definit ca fiind suficient de larg pentru a reține orice dimensiune de obiect, ceea ce pe o platformă pe 64 de biți înseamnă un întreg fără semn pe 64 de biți. Interfețele de încărcare progresivă ale PDFium comunică în decalaje (offsets) de octeți de tip size_t. Înregistrarea furnizorului de disponibilitate FX_FILEAVAIL conține un apel invers IsDataAvail pe care PDFium îl apelează cu un offset și o dimensiune, iar apelul invers AddSegment al înregistrării FX_DOWNLOADHINTS primește aceleași elemente. Ambii parametri sunt de tip size_t.
IsDataAvail = function(
pThis : PFX_FILEAVAIL;
offset, size: size_t): FPDF_BOOL; cdecl;
AddSegment = procedure(
pThis : PFX_DOWNLOADHINTS;
offset, size: size_t); cdecl;
Dacă declarați acele offseturi ca fiind de tip pe 32 de biți, legătura funcționează pe Win32 și pe Delphi Win64, apoi se defectează în mod silențios pe FPC și Lazarus Win64. Cauza este subtilă. Pe FPC Win64, NativeUInt este un tip autentic de lățime a indicatorului pe 64 de biți, iar size_t este aliasat la acesta. Legătura conține un comentariu în secțiunea de tipuri care avertizează tocmai împotriva shadowing-ului NativeUInt pe FPC, deoarece redefinirea acestuia la un alias pe 32 de biți ar forța size_t la 32 de biți și ar corupe fiecare parametru size_t transmis sau scris de bibliotecă. Un offset de 64 de biți care ajunge la un parametru de 32 de biți își pierde jumătatea superioară. Pentru un fișier mic, fiecare offset se potrivește în 32 de biți și nimic nu este în neregulă. Pentru un fișier mare, în momentul în care un offset trece de linia de patru gigaocteți, valoarea trunchiată indică în altă parte complet, PDFium întreabă dacă intervalul de octeți greșit este disponibil, iar încărcarea progresivă se blochează sau citește date eronate. Defectul este invizibil până când fișierul este suficient de mare, iar ținta este cea pe care size_t s-a extins de fapt.
O excepție Pascal nu trebuie să traverseze niciodată un cadru C
A treia clasă se referă la modelul de excepții, pe care limbajul C nu îl are. Când PDFium apelează unul dintre apelurile inverse, codul Pascal rulează în interiorul unei stive de cadre C și C++ care nu știu nimic despre mecanismul de excepții al Delphi. Dacă apelul invers generează și permite propagarea excepției, aceasta se desfășoară prin cadre care nu au fost niciodată construite pentru a fi derulate. Curățarea internă a PDFium nu rulează, invarianții săi interni rămân parțial actualizați, iar procesul se află acum într-o stare pe care biblioteca nu a anticipat-o niciodată. Contractul pentru aceste apeluri inverse este un cod de returnare, nu o excepție.
Două apeluri inverse fac acest lucru concret. FPDF_FILEWRITE este receptorul în care PDFium scrie un document salvat, iar FPDF_FILEACCESS este sursa din care citește un document de intrare. Ambele sunt implementate aici peste un TStream Delphi și ambele pot eșua în modul în care eșuează orice flux: discul se umple, fluxul este închis pe neașteptate, o citire depășește sfârșitul. Apelul invers de scriere îmbracă scrierea în flux și transformă orice eșec în codul de eșec al PDFium, în loc să-l lase să scape.
function WriteBlock(
pThis: PFPDF_FILEWRITE;
pData: Pointer;
Size : LongWord): Integer; cdecl;
begin
// PDFium treats any non-1 return as a write failure. A Pascal exception
// must not unwind through this cdecl/C++ frame, so trap it and report
// failure instead.
Result := 0;
try
PPdfWrite(pThis).Stream.WriteBuffer(pData^, Size);
Result := 1;
except
end;
end;
Partea de citire face același lucru: o citire eșuată raportează zero pentru a se potrivi cu contractul FPDF_FILEACCESS, în loc să genereze o excepție peste granița ABI. Un bloc except simplu fără o re-generare pare greșit pentru un programator Pascal instruit să nu ignore niciodată excepțiile, iar în Pascal obișnuit este greșit. La o graniță ABI are forma corectă, deoarece singura valoare sigură care poate fi transmisă înapoi apelantului C este un cod de stare pe care acesta știe să-l interpreteze. Eșecul se propagă în continuare, doar că prin valoarea de returnare, iar codul de apelare de deasupra bibliotecii îl expune ca EPdfError odată ce controlul revine pe partea Pascal.
Eroarea double free se ascunde pe calea de tratare a erorilor
Al patrulea defect este deținerea (ownership). Un descriptor de document PDFium este deschis de bibliotecă și trebuie închis exact o singură dată, prin FPDF_CloseDocument. Pericolul este o cale de eroare care eliberează un descriptor pe care îl deține și o a doua curățare. Imaginați-vă o rutină care creează un obiect înveliș (wrapper), îi atribuie un descriptor de document proaspăt deschis și apoi face mai multe configurări care ar putea eșua. Dacă configurarea eșuează, un handler de returnare timpurie care apelează FPDF_CloseDocument pe descriptorul brut îl va închide, iar apoi propriul destructor al obiectului înveliș îl va închide din nou când obiectul este eliberat. Descriptorul este eliberat de două ori, ceea ce reprezintă un comportament nedefinit și un probabil blocaj al aplicației.
Auditul a constatat acest lucru pe o cale de import de tip impunere care construiește un TPdf în jurul unui descriptor deja deschis. Remedierea este de a face din transferul de proprietate singura sursă de adevăr. Odată ce descriptorul este atribuit câmpului învelișului, învelișul îl deține, iar singura curățare de pe calea de eroare este eliberarea învelișului. Destructorul învelișului apelează FPDF_CloseDocument în locul dvs., așa că o a doua închidere explicită ar elibera de două ori același document. Handlerul de eroare corectat eliberează obiectul și re-generează excepția, existând exact o singură cale către închidere.
Result := TPdf.Create(nil);
try
Result.FDocument := NewDoc; // Result now owns the handle
Result.InitializeFormFill;
Result.ReloadPage;
except
// Result.Free closes the handle. A second FPDF_CloseDocument(NewDoc)
// here would double-free the same PDFium document.
Result.Free;
raise;
end;
Înregistrările gestionate și o bibliotecă cu multe exporturi necesită eliberare explicită
Ultima clasă se referă la memoria pe care compilatorul o gestionează în numele dvs., pe care un obicei preluat din limbajul C o va corupe în mod silențios. Multe dintre funcțiile ajutătoare ale acestei legături returnează o înregistrare care conține un WideString sau o matrice dinamică. Acestea sunt câmpuri contorizate prin referință, iar compilatorul emite o evidență ascunsă pentru a le menține contorizările. Instinctul preluat din C este de a goli o înregistrare nouă cu FillChar(Result, SizeOf(Result), 0). Aceasta aplică zerouri peste referința gestionată din înregistrare fără a o decrementa mai întâi. Compilatorul reutilizează o variabilă temporară ascunsă pentru rezultatul unei funcții de-a lungul iterațiilor buclei, astfel încât la a doua iterație FillChar suprascrie un indicator de șir activ care nu a fost niciodată eliberat, iar șirul către care indica se pierde. Apelați funcția într-o buclă de peste o mie de adnotări și veți pierde o mie de șiruri de caractere.
Remedierea constă în a permite limbajului să golească înregistrarea în modul în care știe să o facă, cu Default(T), care eliberează orice câmp gestionat înainte de a-l aduce la valoarea zero.
// Default() instead of FillChar: the compiler reuses one hidden temp for
// the function result across loop iterations, so FillChar would zero live
// WideString pointers without releasing them.
Result := Default(TPdfAnnotation);
O problemă conexă de deținere se află la limita de încărcare a bibliotecii. Această legătură rezolvă câteva sute de indicatori de funcții din DLL-ul PDFium cu GetProcAddress după un LoadLibrary. Dacă un export solicitat lipsește, starea parțial legată este periculoasă: zeci de indicatori sunt valabili, restul sunt nil sau învechiți, iar orice apel ulterior prin unul dintre aceștia sare într-un modul care poate fi deja descărcat din memorie. Legătura gestionează acest lucru prin descărcarea bibliotecii și rularea unei proceduri complete ClearAllBindings care resetează fiecare indicator importat înapoi la nil ori de câte ori un export solicitat nu reușește să se rezolve. După aceea, niciun indicator de funcție nu mai atârnă într-un modul descărcat, iar un apel ulterior eșuează curat cu o verificare de tip indicator nil, în loc să ramifice în cod eliberat.
Niciunul dintre aceste cinci defecte nu este exotic. Ele sunt modurile previzibile de eșec ale unui strat subțire Pascal peste un API C și se grupează deoarece acel strat este exact locul unde trebuie re-declarate patru contracte separate. Convenția de apelare trebuie să fie scrisă cdecl la fiecare apel invers. Lățimea întregului trebuie să corespundă cu size_t pe singura țintă unde se extinde de fapt. Modelul de excepție trebuie convertit în coduri de returnare la fiecare apel invers care iese din Pascal. Proprietatea fiecărui descriptor și a fiecărui câmp gestionat trebuie declarată o singură dată și respectată pe fiecare cale, inclusiv pe căile de eroare pe care nimeni nu le exercită până în producție. Scăpați vreuna și veți obține un defect al cărui simptom apare departe de cauza sa, ceea ce face ca această categorie să fie costisitoare. Valoarea auditului a fost mai puțin în fiecare remediere individuală, cât în tratarea fiecăreia dintre acestea ca pe o disciplină proprie de verificat pe întreaga legătură.
Dacă doriți să vedeți legătura făcând treabă reală, în loc să-și protejeze marginile, tehnicile de cache de redare și zoom din nota noastră despre performanța cache-ului de redare și a zoom-ului arată calea de redare, iar ghidul cross-compilator din construirea unui vizualizator Lazarus și FPC este locul în care comportamentul Win64 size_t descris aici contează cu adevărat. Ambele se bazează pe aceeași activitate de siguranță a memoriei și ABI care este livrată în PDFium Component pentru Delphi, Lazarus și C++Builder, alături de API-urile de redare, extragere de text și formulare acoperite în alte părți ale acestui blog.