Iată o problemă care apare chiar în momentul în care o bibliotecă PDF își părăsește limbajul de origine. Aveți un binding care funcționează perfect din C# pe Windows. Aveți nevoie de aceleași apeluri din Python pe macOS, așa că copiați fișierul de declarații pentru Windows, schimbați numele binarului și îl rulați. Fiecare simbol se rezolvă. Primul apel returnează date corupte, al doilea se blochează cu o încălcare de acces (access violation), iar codul dvs. PDF nu s-a schimbat deloc. Defectul se află cu un nivel sub PDF: exporturile Windows folosesc convenția Stdcall, dylib-ul de macOS exportă aceleași funcții ca Cdecl cu o liniuță de subliniere (underscore) la început, iar o declarație de funcție externă (foreign-function) care greșește oricare dintre aceste detalii corupe stiva înainte ca vreun document să fie deschis
Întreaga această clasă de erori provine dintr-o decizie de design care merită înțeleasă din start. PDF Library for Delphi, motorul PDF cu sursă disponibilă (source-available) al losLab pentru Delphi și C++Builder, învelește întregul model de obiecte într-o singură clasă-fațadă plată, TPDFlib, apoi livrează această fațadă în trei forme binare: un DLL pentru Windows cu aproximativ 1.250 de funcții exportate, un obiect de automatizare COM/ActiveX și un dylib pentru macOS. Semantica PDF este identică în toate cele trei cazuri. Partea care vă poate mușca se află în ABI-ul de dedesubt: convențiile de apel, codificările de șiruri, proprietatea handle-urilor și care parte are voie să elibereze care buffer
O fațadă, trei forme binare
Fiecare funcție publică din TPDFlib are un corespondent plat, numit DL plus numele metodei. LoadFromFile devine DLLoadFromFile, Encrypt devine DLEncrypt, NewSignProcessFromFile devine DLNewSignProcessFromFile. Primul parametru al aproape fiecărui export este un InstanceID returnat de DLCreateLibrary, care ține locul referinței de obiect pe care un apelant Delphi ar deține-o altfel. Interiorizați această corespondență din start. Asta înseamnă că referința API pentru Delphi funcționează totodată ca documentație pentru orice alt limbaj: tot ce poate face clasa poate face și DLL-ul, sub un nume previzibil, iar dvs. puteți citi semnătura unei metode Pascal ca să aflați apelul de care aveți nevoie din Python sau C#
Build-ul pentru Windows produce PDFlibDLL32.dll și PDFlibDLL64.dll; alegeți-l pe cel care corespunde arhitecturii (32/64 de biți) procesului gazdă, întrucât un proces Java sau .NET pe 64 de biți nu poate încărca biblioteca pe 32 de biți indiferent cum arată declarația
Windows: instanțe Stdcall și perechile de funcții W/A
Fiecare export care primește un șir de caractere există de două ori. O versiune wide primește PWideChar (UTF-16, potrivirea naturală pentru .NET, Java și c_wchar_p din Python), iar o versiune cu sufixul A primește PAnsiChar. Cele două au semantică identică și diferă doar prin codificare, ceea ce face amestecarea lor atât de dureros de depistat: nimic nu aruncă o excepție, nimic nu returnează un cod de eroare, pur și simplu obțineți mojibake în metadate sau un mesaj fals „file not found” pentru orice cale cu un caracter dincolo de ASCII simplu. Primul bug de codificare pe care o echipă îl întâlnește astfel costă de obicei o după-amiază, pentru că simptomul indică spre date, iar cauza se află în declarație
// Binding pentru Windows (PDFlibDLL64.dll): Stdcall, nume de export simple
function DLCreateLibrary: Integer; stdcall;
external 'PDFlibDLL64.dll' name 'DLCreateLibrary';
function DLReleaseLibrary(InstanceID: Integer): Integer; stdcall;
external 'PDFlibDLL64.dll' name 'DLReleaseLibrary';
function DLLoadFromFile(InstanceID: Integer;
FileName, Password: PWideChar): Integer; stdcall;
external 'PDFlibDLL64.dll' name 'DLLoadFromFile';
// Binding pentru macOS: aceeași funcție, Cdecl, și un prefix underscore pe export
function DLCreateLibrary: Integer; cdecl;
external 'PDFlibDylib.dylib' name '_DLCreateLibrary';
Alegeți o singură lățime de caracter per gazdă și codificați-o în generatorul de binding-uri. O regulă practică: dacă limbajul gazdă are șiruri UTF-16 native, legați versiunile W peste tot și nu mai atingeți niciodată familia A
macOS: aceleași nume, ABI diferit
Dylib-ul exportă același set de funcții DL cu două modificări sistematice. Convenția de apel este Cdecl în loc de Stdcall, iar fiecare nume de export poartă o liniuță de subliniere la început (_DLCreateLibrary, _DLLoadFromFile și așa mai departe). Ambele modificări sunt pur mecanice, ceea ce le face ideale pentru un binding generat automat și periculoase pentru o copie editată manual a fișierului Windows. Păstrați o listă canonică de funcții și generați din ea declarațiile specifice fiecărei platforme, dacă instrumentele dvs. o permit. Săriți peste acest pas și obțineți exact coruperea stivei descrisă la începutul acestei pagini, reproductibilă doar pe platforma pe care CI-ul dvs. o testează cel mai puțin
Gazde COM și ActiveX: convenția Safecall și payload-urile Olevariant
Pentru VB.NET, C#, VBScript și gazdele de automatizare mai vechi, build-ul OCX învelește aceeași fațadă într-un obiect de automatizare IDispatch, IPDFlibrary, cu fiecare metodă declarată Safecall. Această convenție schimbă modul în care erorile ajung la dvs. Safecall traduce un eșec intern într-un HRESULT COM, astfel încât un apelant C# prinde o excepție acolo unde DLL-ul plat ar fi returnat un simplu întreg pe care apelantul trebuia să-și amintească să-l verifice. Aceeași operație, două idiomuri de eșec, în funcție de binarul încărcat
Datele binare urmează o a doua regulă specifică COM. Interfața de automatizare nu are deloc parametri de tip pointer. Orice e binar, fie octeți de imagine care intră, fie octeți PDF care ies, traversează granița ca un Olevariant, prin metode precum AddImageFromVariant și AppendToVariant. Maparea (marshaling) unui array de octeți într-un variant este o singură linie în .NET. Încercați să-i dați în schimb un pointer brut, pe motivul că oricum e același proces, iar stratul de dispatch respinge sau alterează apelul. Încă un detaliu de înregistrare complică desfășurările: înregistrarea COM se face per arhitectură, așa că un OCX înregistrat cu regsvr32 pe 32 de biți este invizibil pentru o gazdă pe 64 de biți. Această nepotrivire iese la suprafață ca faimosul, deloc de ajutor, mesaj „class not registered” pe mașina clientului, mult după ce a părăsit-o pe a dvs
Disciplina handle-urilor: instanțele dețin documentele
API-ul plat funcționează pe bază de handle-uri întregi. DLCreateLibrary returnează o instanță. Încărcarea unui fișier returnează un ID de document în interiorul acelei instanțe. Procesele de semnare, listele de șiruri și fișierele cu acces direct returnează fiecare propriile handle-uri întregi, toate delimitate la aceeași instanță. Ciclul de viață arată identic din orice gazdă FFI, prezentat aici în Pascal pentru că se citește curat:
var
Inst, Doc: Integer;
begin
Inst := DLCreateLibrary; // o instanță per thread de lucru
try
Doc := DLLoadFromFile(Inst, 'in.pdf', ''); // returnează un DocumentID, 0 la eșec
if Doc <> 0 then
begin
DLEncrypt(Inst, 'owner-secret', 'user-secret', 3,
DLEncodePermissions(Inst, 1, 0, 0, 0, 0, 0, 0, 1));
DLSaveToFile(Inst, 'out.pdf');
end;
finally
DLReleaseLibrary(Inst); // eliberează fiecare document deținut de instanță
end;
end;
Din acest arbore de proprietate decurg două lucruri. DLReleaseLibrary este singurul apel de curățare de care aveți strict nevoie, întrucât dărâmă dintr-o singură lovitură fiecare handle de document și de proces aflat sub instanță. Într-un script scurt, atât e suficient. Într-un serviciu care rulează îndelung devine o scurgere lentă cu ceremonie în plus, așa că eliberați documentele pe măsură ce terminați cu ele, în loc să le lăsați să se acumuleze până moare instanța. Instanța este totodată unitatea naturală de izolare pe thread-uri. Dați fiecărui thread de lucru propriul InstanceID și nu partajați niciodată unul între thread-uri fără blocare externă, din același motiv pentru care nu ați partaja niciodată un singur obiect TPDFlib între thread-uri
Șirurile returnate sunt împrumutate, nu deținute
Funcțiile care returnează text, precum DLGetPageText, dau înapoi un PWideChar sau PAnsiChar care indică spre un buffer deținut și reciclat de instanța bibliotecii. Contractul este: copiați imediat, nu eliberați niciodată
var
P: PWideChar;
PageText: string;
begin
P := DLGetPageText(Inst, 7); // pointer către un buffer deținut de bibliotecă
PageText := P; // copiați acum; un apel ulterior poate refolosi bufferul
end;
În C# asta înseamnă maparea IntPtr-ului într-un șir gestionat (managed string) înainte de următorul apel către bibliotecă. În Python ctypes, înseamnă extragerea imediată a șirului wide din pointer. Păstrați pointerul brut peste mai multe apeluri și ați scris un bug care trece de fiecare test unitar și apoi eșuează prima dată când două cereri se suprapun în producție, pentru că al doilea apel a reciclat bufferul pe care primul îl mai citea încă. Aceeași regulă de proprietate funcționează și în sens invers pentru callback-urile înregistrate prin DLSetProgressCallback. Orice pointer pe care biblioteca îl transmite în callback-ul dvs. este valid doar pentru corpul acelui callback, iar obiectul callback însuși trebuie să rămână în viață (fixat/pinned, într-o gazdă cu colectare de gunoi) atâta timp cât instanța l-ar mai putea invoca. Un delegat colectat la jumătatea unei operațiuni este sursa de manual a violării de acces „aleatorii” care apare într-un binding .NET ce funcționase curat luni de zile
Construiți un smoke test chiar în binding și rulați-l înainte ca orice set de declarații generate să fie livrat. Exersați câte un apel din fiecare categorie care tinde să expună greșeli de ABI: o funcție fără parametri, precum DLCreateLibrary, pentru a dovedi că respectă convenția, o funcție cu șir de intrare alimentată cu o cale ce conține caractere non-ASCII pentru a dovedi că respectă codificarea, o funcție cu șir de ieșire pentru a dovedi că respectă gestionarea bufferului împrumutat, și o operație care eșuează intenționat, ca să vedeți cum ajunge o eroare la gazda dvs. Sunt cincisprezece minute de muncă, iar ele prind erorile de convenție de apel și de codificare care altfel ar ajunge peste luni de zile sub forma unui crash dump la un client
Cazul Python ctypes, concret
Python ctypes este binding-ul pe care îl văd construit manual cel mai des, și el face ușor de demonstrat separarea între platforme. Pe Windows, încărcați biblioteca cu ctypes.WinDLL, astfel încât ctypes să aplice Stdcall, legați funcțiile W fără sufix și declarați fiecare parametru de tip șir ca c_wchar_p. Pe macOS, încărcați-o cu ctypes.CDLL pentru Cdecl, păstrați aceeași listă de funcții identică și rezolvați numele fără liniuța de subliniere de la început. Majoritatea straturilor FFI, ctypes inclusiv, reintroduc automat convenția underscore-ului pe macOS, dar aceasta este exact presupunerea pe care merită să o confirmați cu un singur apel rezolvat înainte să generați sute de declarații peste ea
Două întrebări de desfășurare urmează munca de binding și au răspunsuri clare. DLL-ul simplu nu are nevoie de nicio înregistrare: regsvr32 se aplică doar build-ului ActiveX, iar DLL-ul se livrează prin simpla copiere a fișierului, motiv principal pentru care este preferat în servicii Windows și containere unde preferați să nu atingeți deloc registrul. Siguranța la nivel de thread se reduce la regula deja în vigoare mai sus, o instanță per thread. Handle-ul instanței păstrează fiecare fragment de stare mutabilă pe care motorul îl urmărește, documentul selectat, opțiunile de randare, setările de extragere, astfel încât două thread-uri care partajează o instanță își întrepătrund reciproc starea, chiar dacă fiecare apel individual returnează succes
Odată ce un binding este solid, operațiile de dincolo de el sunt exact cele pe care articolele despre Delphi le acoperă în profunzime, inclusiv aplicarea și auditarea criptării PDF și extragerea textului și a imaginilor din documente existente
Descărcările binare pentru toate cele trei straturi de integrare se livrează împreună cu biblioteca; consultați pagina de produs PDF Library for Delphi pentru ediții și licențiere