Tu je problém, ktorý sa objaví v momente, keď knižnica PDF opustí svoj domovský programovací jazyk. Máte prepojenie, ktoré funguje dokonale z C# v systéme Windows. Potrebujete rovnaké volania z Pythonu v systéme macOS, preto skopírujete súbor deklarácií pre Windows, zmeníte názov binárneho súboru a spustíte ho. Každý symbol sa úspešne vyrieši. Prvé volanie však vráti nezmyselné dáta, druhé zlyhá s narušením prístupu do pamäte (access violation) a pritom sa žiadny z vašich kódov pre PDF nezmenil. Chyba je o úroveň nižšie než samotné PDF: exporty pre Windows používajú volaciu konvenciu Stdcall, dylib pre macOS exportuje rovnaké funkcie ako Cdecl s predradeným podčiarkovníkom a akákoľvek deklarácia externých funkcií, ktorá urobí chybu v ktoromkoľvek z týchto detailov, poškodí zásobník skôr, než sa otvorí jediný dokument
Celá táto skupina zlyhaní vyplýva z jedného dizajnového rozhodnutia, ktoré je dôležité pochopiť hneď na začiatku. PDFlibPas, engine PDF s dostupnými zdrojovými kódmi pre Delphi a C++Builder od losLab, balí celý svoj objektový model do jedinej plochej triedy fasády, TPDFlib, a potom dodáva túto fasádu v troch binárnych formách: ako Windows DLL s približne 1 250 exportovanými funkciami, ako automatizačný objekt COM/ActiveX a ako dylib pre macOS. Sémantika PDF je vo všetkých troch rovnaká. Problémy však spôsobujú ABI pod nimi: volacie konvencie, kódovanie reťazcov, vlastníctvo popisovačov a to, ktorá strana smie uvoľniť ktorú vyrovnávaciu pamäť
Jedna fasáda, tri binárne formy
Každá verejná funkcia triedy TPDFlib má svoj plochý náprotivok pomenovaný predponou DL a názvom metódy. Metóda LoadFromFile sa mení na DLLoadFromFile, Encrypt na DLEncrypt, NewSignProcessFromFile na DLNewSignProcessFromFile. Prvým parametrom takmer každého exportu je InstanceID vrátený funkciou DLCreateLibrary, ktorý predstavuje referenciu na objekt, ktorú by inak držal volajúci v Delphi. Osvojte si toto mapovanie čo najskôr. Znamená to, že referenčná príručka API pre Delphi slúži zároveň ako dokumentácia pre každý iný jazyk: čokoľvek dokáže trieda, dokáže aj DLL pod predvídateľným názvom a podpis metódy v Pascale vám napovie, aké volanie potrebujete použiť z Pythonu alebo C#
Zostava pre Windows produkuje súbory PDFlibDLL32.dll a PDFlibDLL64.dll; vyberte si ten, ktorý zodpovedá bitovej architektúre vášho hostiteľského procesu, pretože 64-bitový proces Javy alebo .NET nedokáže načítať 32-bitovú knižnicu bez ohľadu na to, ako vyzerá jej deklarácia
Windows: Inštancie Stdcall a páry funkcií W/A
Každý export preberajúci reťazec existuje dvakrát. Široká verzia (W) preberá typ PWideChar (UTF-16, ktorý prirodzene pasuje pre .NET, Javu a c_wchar_p v Pythone) a verzia s príponou A preberá PAnsiChar. Obe nesú rovnakú sémantiku a líšia sa iba kódovaním, čo je presne to, prečo je hľadanie chýb pri ich zámene také náročné: nič nevyvolá výnimku, nič nevráti chybový kód, jednoducho dostanete poškodený text v metadátach alebo chybu „súbor sa nenašiel“ pri akejkoľvek ceste so znakom mimo základného ASCII. Prvá chyba kódovania, na ktorú tím takto narazí, zvyčajne stojí pol dňa hľadania, pretože príznak ukazuje na dáta, no príčina leží v deklarácii
// Windows binding (PDFlibDLL64.dll): Stdcall, plain export names
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';
// macOS binding: same function, Cdecl, and an underscore prefix on the export
function DLCreateLibrary: Integer; cdecl;
external 'PDFlibDylib.dylib' name '_DLCreateLibrary';
Zvoľte si jednu šírku znakov pre daného hostiteľa a zadefinujte ju v generátore prepojení. Praktické pravidlo: ak má hostiteľský jazyk natívne reťazce UTF-16, prepojte verzie W všade a rodiny A sa už nikdy nedotknite
macOS: rovnaké názvy, odlišné ABI
Knižnica dylib exportuje rovnakú sadu funkcií DL s dvoma systematickými zmenami. Volacia konvencia je Cdecl namiesto Stdcall a každý názov exportu obsahuje predradený podčiarkovník (_DLCreateLibrary, _DLLoadFromFile atď.). Obe zmeny sú čisto mechanické, čo z nich robí ideálnych kandidátov na vygenerovanie prepojenia a riziko pri ručnej úprave súboru pre Windows. Udržiavajte jeden kanonický zoznam funkcií a generujte z neho deklarácie pre jednotlivé platformy, ak to vaše nástroje umožňujú. Ak to urobil váš systém inak, dočkáte sa presne toho poškodenia zásobníka popísaného na začiatku tohto článku, ktoré sa bude prejavovať len na platforme, ktorú váš systém CI testuje najmenej
Hostitelia COM a ActiveX: Volania Safecall a dáta Olevariant
Pre VB.NET, C#, VBScript a starších hostiteľov automatizácie balí zostava OCX rovnakú fasádu do automatizačného objektu IDispatch s názvom IPDFlibrary, pričom každá metóda je deklarovaná ako Safecall. Táto konvencia mení spôsob, akým sa k vám dostávajú chyby. Safecall prekladá interné zlyhanie na COM HRESULT, takže volajúci z C# zachytí výnimku tam, kde by plochá DLL vrátila tiché celé číslo, ktorého kontrolu by musel vývojár zabezpečiť manuálne. Rovnaká operácia, dva rôzne spôsoby spracovania chýb podľa toho, ktorú binárku ste načítali
Binárne dáta podliehajú druhému pravidlu špecifickému pre COM. Automatizačné rozhranie nemá vôbec žiadne parametre ukazovateľov (pointers). Čokoľvek binárne, či už vstupujúce bajty obrázka alebo vystupujúce bajty PDF, prechádza cez hranicu ako Olevariant pomocou metód ako AddImageFromVariant a AppendToVariant. Prevedenie poľa bajtov na variant je v .NET záležitosťou jedného riadku. Pokúste sa mu namiesto toho odovzať surový ukazovateľ s odôvodnením, že ide o ten istý proces, a vrstva dispatch volanie odmietne alebo poškodí. Ďalší detail registrácie komplikuje nasadenie: registrácia COM závisí od bitovej verzie, takže OCX registrovaný pomocou 32-bitového regsvr32 je pre 64-bitového hostiteľa neviditeľný. Tento nesúlad sa na počítači zákazníka prejaví ako známa a neužitočná správa „trieda nie je registrovaná“ dlho po tom, čo opustil váš systém
Práca s popisovačmi: inštancie vlastnia dokumenty
Ploché API beží na celočíselných popisovačoch. DLCreateLibrary vracia inštanciu. Načítanie súboru vracia ID dokumentu v rámci danej inštancie. Podpisové procesy, zoznamy reťazcov a súbory s priamym prístupom vracajú vlastné celočíselné popisovače, všetky s platnosťou v rámci tej istej inštancie. Životný cyklus vyzerá rovnako z akéhokoľvek hostiteľa FFI, tu je zobrazený v Pascale kvôli čitateľnosti:
var
Inst, Doc: Integer;
begin
Inst := DLCreateLibrary; // one instance per worker thread
try
Doc := DLLoadFromFile(Inst, 'in.pdf', ''); // returns a DocumentID, 0 on failure
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); // frees every document the instance owns
end;
end;
Z tohto stromu vlastníctva vyplývajú dve veci. Jediným čistiacim volaním, ktoré striktne potrebujete, je DLReleaseLibrary, pretože naraz zruší každý dokument a popisovač procesu pod danou inštanciou. V krátkom skripte to stačí. V dlhodobo bežiacej službe to však môže viesť k pomalému úniku pamäte so zbytočnou réžiou, preto uvoľňujte dokumenty hneď, ako s nimi skončíte, a nenechávajte ich hromadiť sa až do zániku inštancie. Inštancia je tiež prirodzenou jednotkou izolácie vlákien. Priraďte každému pracovnému vláknu jeho vlastné InstanceID a nikdy ho nezdieľajte medzi vláknami bez externého zamykania, z rovnakého dôvodu, prečo by ste nikdy nezdieľali jeden objekt TPDFlib medzi vláknami
Vrátené reťazce sú vypožičané, nie vlastnené
Funkcie, ktoré vracajú text, ako napríklad DLGetPageText, vracajú PWideChar or PAnsiChar ukazujúci do vyrovnávacej pamäte, ktorú vlastní a recykluje inštancia knižnice. Pravidlo znie: okamžite skopírovať, nikdy neuvoľňovať
var
P: PWideChar;
PageText: string;
begin
P := DLGetPageText(Inst, 7); // pointer into a library-owned buffer
PageText := P; // copy now; a later call may reuse the buffer
end;
V C# to znamená previesť IntPtr na spravovaný reťazec pred ďalším volaním knižnice. V Python ctypes to znamená okamžite získať široký reťazec z ukazovateľa. Ak podržíte surový ukazovateľ medzi volaniami, vytvoríte chybu, ktorá prejde každým jednotkovým testom a potom zlyhá pri prvom prekrytí dvoch požiadaviek v produkcii, pretože druhé volanie zrecykluje vyrovnávaciu pamäť, ktorú prvé volanie ešte čítalo. Rovnaké pravidlo vlastníctva platí aj v opačnom smere pre spätné volania registrované cez DLSetProgressCallback. Akýkoľvek ukazovateľ, ktorý knižnica odovzdá do vášho spätného volania, je platný iba počas trvania tohto spätného volania a samotný objekt spätného volania musí zostať nažive (umiestnený v pamäti bez presunov u GC hostiteľov) tak dlho, kým ho inštancia môže vyvolať. Uvoľnenie delegáta garbage collectorom uprostred úlohy je klasickým zdrojom „náhodných“ chýb typu access violation v prepojení .NET, ktoré predtým bežalo bez problémov celé mesiace
Zabudujte rýchly orientačný test (smoke test) priamo do prepojenia a spustite ho pred odoslaním akejkoľvek sady vygenerovaných deklarácií. Otestujte jedno volanie z každej kategórie, ktorá zvykne odhaliť chyby ABI: funkciu bez parametrov ako DLCreateLibrary na overenie správnosti konvencie, funkciu prijímajúcu reťazec s cestou obsahujúcou znaky mimo ASCII na overenie kódovania, funkciu vracajúcu reťazec na overenie práce s vypožičanou pamäťou a jednu operáciu, ktorá zámerne zlyhá, aby ste videli, ako sa chyba prenesie k vášmu hostiteľovi. Je to pätnásť minút práce a zachytí to chyby volacej konvencie a kódovania, ktoré by sa inak prejavili o mesiace neskôr ako chybové výpisy u zákazníka
Prípad Python ctypes, konkrétne
Python ctypes je prepojenie, ktoré sa píše ručne najčastejšie, a uľahčuje demonštráciu rozdielov medzi platformami. V systéme Windows načítajte knižnicu pomocou ctypes.WinDLL, aby ctypes aplikoval Stdcall, prepojte nepríponové funkcie W a deklarujte každý parameter reťazca ako c_wchar_p. V systéme macOS ju načítajte pomocou ctypes.CDLL pre Cdecl, zakážte predvolený zoznam funkcií a vyriešte názvy bez predradeného podčiarkovníka. Väčšina vrstiev FFI, vrátane ctypes, vyrieši konvenciu s podčiarkovníkom v systéme macOS automaticky za vás, je to však predpoklad, ktorý si treba overiť jedným skúšobným volaním predtým, než nad ním vygenerujete stovky deklarácií
Dve otázky týkajúce sa nasadenia majú jasné odpovede. Samotná DLL nevyžaduje žiadnu registráciu: príkaz regsvr32 sa vzťahuje len na zostavu ActiveX a DLL sa dodáva jednoduchým kopírovaním súboru, čo je hlavný dôvod, prečo sa uprednostňuje pre služby a kontajnery Windows, kde nechcete vôbec zasahovať do registrov. Vláknová bezpečnosť sa redukuje na pravidlo spomenuté vyššie: jedna inštancia na jedno vlákno. Popisovač inštancie drží každú časť meniteľného stavu, ktorú engine sleduje – vybraný dokument, možnosti vykresľovania, nastavenia extrakcie – takže dve vlákna zdieľajúce inštanciu by si navzájom prepisovali stavy, aj keby každé jednotlivé volanie vrátilo úspech
Keď je prepojenie stabilné, operácie na jeho druhej strane sú presne tie, ktorým sa podrobne venujú články o Delphi, vrátane aplikovania a auditu šifrovania PDF a extrahovania textu a obrázkov z existujúcich dokumentov
Binárne súbory na stiahnutie pre všetky tri integračné vrstvy sa dodávajú s knižnicou; vydania a licencovanie nájdete na produktovej stránke PDFlibPas