Technical Article

Vezave PDFlibPas DLL, ActiveX in dylib: Klicanje enega pogona PDF iz katerega koli jezika

Tukaj je težava, ki se pojavi v trenutku, ko knjižnica za PDF zapusti svoj domači jezik. Imate vezavo, ki deluje popolnoma v C# na Windowsih. Potrebujete enake klicne funkcije v Pythonu na macOS, so kopirate deklaracijsko datoteko za Windows, zamenjate ime binarne datoteke in jo zaženete. Vsak simbol se razreši. Prvi klic vrne smeti, drugi se sesuje s kršitvijo dostopa (access violation), pri čemer se vaša koda PDF sploh ni spremenila. Napaka je eno raven pod PDF-jem: izvozi za Windows uporabljajo konvencijo Stdcall, dylib za macOS izvozi iste funkcije kot Cdecl z vodilnim podčrtajem, deklaracija tuje funkcije (FFI), ki zgreši katero koli od teh podrobnosti, pa pokvari sklad, še preden se odpre en sam dokument.

Ta celotni razred napak izhaja iz ene oblikovalske odločitve, ki jo je dobro razumeti vnaprej. PDFlibPas, losLabov pogon PDF z razpoložljivo izvorno kodo za Delphi in C++Builder, ovije celoten objektni model v en sam raven fasadni razred, TPDFlib, in nato to fasado pošlje v treh binarnih oblikah: Windows DLL s približno 1.250 izvoženimi funkcijami, COM/ActiveX avtomatizacijski objekt in dylib za macOS. Semantika PDF je pri vseh treh enaka. Del, ki vas lahko ugrizne, živi v spodnjem ABI-ju: klicne konvencije, kodiranja nizov, lastništvo ročajev in katera stran sme sprostiti kateri odložišče.

Ena fasada, tri binarne oblike

Vsaka javna funkcija TPDFlib ima ravnega dvojnika z imenom DL in imenom metode. LoadFromFile postane DLLoadFromFile, Encrypt postane DLEncrypt, NewSignProcessFromFile becomes DLNewSignProcessFromFile. Prvi parameter skoraj vsakega izvoza je InstanceID, ki ga vrne DLCreateLibrary in nadomešča referenco objekta, ki bi jo sicer imel klicatelj v Delphiju. To preslikavo ponotranjite zgodaj. Pomeni, da referenca API za Delphi služi kot dokumentacija za vsak drug jezik: karkoli razred zmore, DLL zmore pod predvidljivim imenom, vi pa lahko preberete podpis metode v Pascalu, da ugotovite klic, ki ga potrebujete iz Pythona ali C#.

Gradnja za Windows ustvari datoteki PDFlibDLL32.dll in PDFlibDLL64.dll; izberite tisto, ki ustreza bitnosti vašega gostiteljskega procesa, saj 64-bitni Java ali .NET proces ne more naložiti 32-bitne knjižnice, ne glede na to, kako je videti deklaracija.

Windows: Instance Stdcall in pari funkcij W/A

Vsak izvoz, ki sprejema nize, obstaja dvakrat. Različica Wide sprejema PWideChar (UTF-16, primerno za .NET, Javo in Pythonov c_wchar_p), različica s pripono A pa sprejema PAnsiChar. Obe imata enako semantiko in se razlikujeta le v kodiranju, kar je natanko tisto, zaradi česar je njuno mešanje tako težko izslediti: nič ne sproži izjeme, nič ne vrne kode napake, preprosto dobite popačeno besedilo (mojibake) v metapodatkih ali lažno napako "datoteka ni najdena" za vsako pot z znakom izven osnovnega ASCII. Prva napaka kodiranja, na katero ekipa naleti na ta način, običajno stane celo popoldne, saj simptom kaže na podatke, vzrok pa je v deklaraciji.

// 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';

Izberite eno širino znakov na gostitelja in jo kodirajte v generatorju vezav. Praktično pravilo: če ima gostiteljski jezik izvorne nize UTF-16, povsod povezajte različice W in se nikoli več ne dotikajte družine A.

macOS: ista imena, drugačen ABI

Dylib izvozi enak nabor funkcij DL z dvema sistematičnima spremembama. Klicna konvencija je Cdecl in ne Stdcall, vsako izvoženo ime pa nosi vodilni podčrtaj (_DLCreateLibrary, _DLLoadFromFile itd.). Obe spremembi sta povsem mehanski, zaradi česar sta idealni za generirano vezavo in nevarni za ročno urejeno kopijo datoteke za Windows. Ohranite en kanoničen seznam funkcij in iz njega oddajajte deklaracije za posamezne platforme, če vam to omogočajo vaša orodja. Če to izpustite, dobite natanko tisto popačenje sklada, opisano na vrhu te strani, ki se pojavi le na tisti platformi, ki jo vaš CI najmanj preverja.

COM in ActiveX gostitelji: Vsebina Safecall in Olevariant

Za VB.NET, C#, VBScript in starejše gostitelje avtomatizacije gradnja OCX ovije isto fasado v avtomatizacijski objekt IDispatch, IPDFlibrary, pri čemer je vsaka metoda deklarirana kot Safecall. Ta konvencija spreminja način, kako napake dosežejo vas. Safecall prevede notranjo napako v COM HRESULT, tako da klicatelj v C# ujame izjemo tam, kjer bi navaden DLL vrnil tiho celo število, ki bi ga morala klicatelj ročno preveriti. Isti postopek, dva načina sporočanja napak, odvisno od binarne datoteke, ki ste jo naložili.

Binarni podatki sledijo drugemu pravilu, značilnemu za COM. Avtomatizacijski vmesnik sploh nima kazalčnih parametrov. Vse, kar je binarno, bajti slike na vhodu ali bajti PDF na izhodu, prečka mejo kot Olevariant prek metod, kot sta AddImageFromVariant in AppendToVariant. Pretvorba bajtnega polja v različico variant je v .NET ena sama vrstica. Če poskusite namesto tega posredovati surovi kazalec, z razlago, da gre vseeno za isti proces, dispečerski sloj klic zavrne ali pohabi. Še ena podrobnost pri registraciji povzroča težave: registracija COM je odvisna od bitnosti, oso OCX, registriran z 32-bitnim regsvr32, neviden za 64-bitnega gostitelja. To neskladje se na strankinem računalniku pokaže kot znano neuporabno sporočilo "razred ni registriran", dolgo po tem, ko je zapustil vašega.

Disciplina ročajev: instance imajo v lasti dokumente

Ravni API deluje na celoštevilskih ročajih. DLCreateLibrary vrne instanco. Nalaganje datoteke vrne ID dokumenta znotraj te instance. Podpisovalni procesi, seznami nizov in datoteke z neposrednim dostopom vrnejo lastne celoštevilske ročaje, ki so vsi omejeni na isto instanco. Življenjski cikel je enak iz katerega koli gostitelja FFI, kar je tukaj prikazano v Pascalu, ker se bere čisto:

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;

Iz tega drevesa lastništva izhajata dve stvari. DLReleaseLibrary je edini klic za čiščenje, ki ga nujno potrebujete, saj v enem koraku uniči vsak dokument in ročaj procesa pod instanco. V kratkem skriptu to zadošča. V dolgotrajno delujoči storitvi pa to postane počasno puščanje pomnilnika, zato dokumente sprostite, ko jih zaključite, namesto da jih kopičite, dokler instanca ne umre. Instanca je tudi naravna enota izolacije niti. Vsaki delovni niti dodelite lasten InstanceID in ga nikoli ne delite med nitmi brez zunanjega zaklepanja, iz istega razloga, kot ne bi nikoli delili enega objekta TPDFlib med nitmi.

Vrnjeni nizi so izposojeni in ne v lasti

Funkcije, ki vračajo besedilo, kot je DLGetPageText, vrnejo PWideChar ali PAnsiChar, ki kaže v odložišče, ki je v lasti instance knjižnice in se ponovno uporablja. Pogodba se glasi: kopirajte takoj, nikoli ne sprostite.

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 pomeni pretvorbo (marshaling) IntPtr v upravljani niz pred naslednjim klicem knjižnice. V Python ctypes to pomeni takojšnje izrezovanje širokega niza iz kazalca. Če držite surovi kazalec med klici, ste ustvarili napako, ki opravi vsak test enote, nato pa spodleti ob prvem prekrivanju dveh zahtev v produkciji, saj je drugi klic ponovno uporabil odložišče, ki ga je prvi še bral. Enako pravilo o lastništvu velja v drugi smeri za povratne klice (callbacks), registrirane prek DLSetProgressCallback. Vsak kazalec, ki ga knjižnica posreduje v vaš povratni klic, je veljaven le za telo tega klica, sam objekt povratnega klica pa mora ostati živ (pripet v gostitelju s sproščanjem pomnilnika), dokler ga instanca lahko kliče. Delegat, sproščen sredi dela, je šolski primer "naključne" kršitve dostopa, ki se pojavi v vezavi .NET, ki je mesece delovala brez težav.

Vgradite test delovanja v samo vezavo in ga zaženite, preden pošljete kateri koli generirani nabor deklaracij. Izvedite po en klic iz vsake kategorije, ki pogosto razkrije napake ABI: funkcijo brez parametrov, kot je DLCreateLibrary, da dokažete pravilnost konvencije, funkcijo s string-in parametrom, ki ji posredujete pot z ne-ASCII znaki, da preverite kodiranje, funkcijo s string-out parametrom, da preverite obravnavo izposojenega odložišča, in en postopek, ki namenoma spodleti, da vidite, kako napaka doseže vašega gostitelja. To je petnajst minut dela, ki ujame napake klicnih konvencij in kodiranja, ki bi sicer mesece pozneje prispele kot strankino poročilo o sesutju.

Primer Python ctypes, konkretno

Python ctypes je vezava, ki jo najpogosteje vidim ročno napisano, in omogoča enostaven prikaz razdelitve med platformami. V sistemu Windows naložite knjižnico s ctypes.WinDLL, da ctypes uporabi Stdcall, povežite funkcije brez pripone W in deklarirajte vsak parameter niza kot c_wchar_p. V sistemu macOS jo naložite s ctypes.CDLL za Cdecl, obdržite enak seznam funkcij in razrešite imena brez vodilnega podčrtaja. Večina slojev FFI, vključno s ctypes, v macOS samodejno doda podčrtaj, vendar je to predpostavka, ki jo je treba potrditi z enim samim klicem, preden na njej zgradite na stotine deklaracij.

Dve namestitveni vprašanji spremljata delo z vezavami in imata jasna odgovora. Navaden DLL ne potrebuje registracije: regsvr32 velja le za gradnjo ActiveX, DLL pa se pošlje s kopiranjem datotek, kar je glavni razlog, da se mu daje prednost pri storitvah Windows in vsebnikih, kjer se sploh ne želite dotikati registra. Varnost niti se zmanjša na zgoraj omenjeno pravilo: ena instanca na nit. Ročaj instance drži vsak del spremenljivega stanja, ki ga pogon spremlja: izbrani dokument, možnosti upodabljanja, nastavitve izvoza, zato dve niti, ki si delita instanco, prepletata stanje druge druge, tudi če vsak posamezen klic vrne uspeh.

Ko je vezava stabilna, so postopki na drugi strani natanko tisti, ki jih podrobno pokrivajo članki za Delphi, vključno z uporabo in revizijo šifriranja PDF ter izvažanjem besedila in slik iz obstoječih dokumentov. Binarni prenosi za vse tri integracijske sloje so priloženi knjižnici; glejte produktno stran PDFlibPas za različice in licenciranje.