Technical Article

PDFlibPas DLL, ActiveX i dylib povezivanja: pozivanje jednog PDF mehanizma iz bilo kojeg jezika

Evo problema koji se pojavljuje u trenutku kada PDF knjižnica napusti svoj matični jezik. Imate povezivanje koje savršeno radi iz C#-a na Windowsima. Trebate iste pozive iz Pythona na macOS-u, pa kopirate Windows deklaracijsku datoteku, zamijenite naziv binarne datoteke i pokrenete je. Svaki se simbol razrešava. Prvi poziv vraća smeće, drugi se ruši s povredom pristupa, a ništa se u vašem PDF kodu nije promijenilo. Pogreška je jedan sloj ispod PDF-a: Windows izvozi koriste Stdcall konvenciju, macOS dylib izvozi iste funkcije kao Cdecl s vodećom podcrtom, a vanjska deklaracija funkcije koja pogriješi u bilo kojem od tih detalja kvari stog prije nego što se otvori ijedan dokument.

Cijela ta klasa neuspjeha proizlazi iz jedne odluke o dizajnu koju vrijedi razumjeti na samom početku. PDFlibPas, losLab-ov PDF mehanizam s dostupnim izvornim kodom za Delphi i C++Builder, omotava svoj cjelokupni objektni model u jednu ravnu klasu fasade, TPDFlib, a zatim isporučuje tu fasadu u tri binarna oblika: Windows DLL s otprilike 1250 izvezenih funkcija, COM/ActiveX automatizacijski objekt i macOS dylib. Semantika PDF-a je identična u sva tri oblika. Dio koji vas može ugristi živi u ABI-ju ispod: konvencije pozivanja, kodiranja nizova, vlasništvo nad ručkama i koja strana smije osloboditi koji spremnik.

Jedna fasada, tri binarna oblika

Svaka javna funkcija u TPDFlib ima svoj ravni ekvivalent s nazivom DL plus naziv metode. LoadFromFile postaje DLLoadFromFile, Encrypt postaje DLEncrypt, NewSignProcessFromFile postaje DLNewSignProcessFromFile. Prvi parametar gotovo svakog izvoza je InstanceID koji vraća DLCreateLibrary, zamjenjujući referencu objekta koju bi inače držao Delphi pozivatelj. Rano usvojite to mapiranje. To znači da se Delphi API referenca može koristiti i kao dokumentacija za svaki drugi jezik: što god klasa može učiniti, DLL može učiniti pod predvidljivim nazivom, a možete pročitati potpis Pascal metode kako biste saznali poziv koji vam je potreban iz Pythona ili C#-a.

Windows verzija proizvodi PDFlibDLL32.dll i PDFlibDLL64.dll; odaberite onaj koji odgovara bitnosti vašeg procesa domaćina, budući da 64-bitni Java ili .NET proces ne može učitati 32-bitnu knjižnicu bez obzira na to kako deklaracija izgleda.

Windows: Stdcall instance i W/A parovi funkcija

Svaki izvoz koji prima niz znakova postoji dvaput. Široka verzija prima PWideChar (UTF-16, što prirodno odgovara .NET-u, Javi i Pythonovom c_wchar_p), a verzija sa sufiksom A prima PAnsiChar. Obje imaju identičnu semantiku i razlikuju se samo u kodiranju, što je upravo ono što čini njihovo miješanje teškim za pronalaženje: ništa ne podiže iznimku, ništa ne vraća kôd pogreške, jednostavno dobijete iskrivljene znakove (mojibake) u metapodacima ili lažnu poruku "datoteka nije pronađena" za bilo koju stazu sa znakom izvan običnog ASCII-ja. Prva pogreška u kodiranju na koju tim naiđe na ovaj način obično košta cijelo poslijepodne, jer simptom upućuje na podatke, a uzrok je u 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';

Odaberite jednu širinu znakova po domaćinu i kodirajte je u generatoru povezivanja. Praktično pravilo: ako jezik domaćina ima izvorne UTF-16 nizove znakova, povežite W verzije posvuda i više nikada ne dirajte obitelj A.

macOS: isti nazivi, različit ABI

Dylib izvozi isti skup DL funkcija uz dvije sustavne promjene. Konvencija pozivanja je Cdecl umjesto Stdcall, a svaki izvozni naziv nosi vodeću podcrtu (_DLCreateLibrary, _DLLoadFromFile itd.). Obje su promjene čisto mehaničke, što ih čini idealnim za generirano povezivanje i opasnim za ručno uređenu kopiju Windows datoteke. Držite jedan kanonski popis funkcija i emitirajte deklaracije po platformi iz njega ako vam to alati omogućuju. Preskočite to i dobit ćete točno kvarenje stoga opisano na vrhu ove stranice, koje se reproducira samo na platformi koju vaš CI sustav najrjeđe koristi.

COM i ActiveX domaćini: Safecall i Olevariant sadržaji

Za VB.NET, C#, VBScript i stare automatizacijske domaćine, OCX verzija omotava istu fasadu u IDispatch automatizacijski objekt, IPDFlibrary, pri čemu je svaka metoda deklarirana kao Safecall. Ta konvencija mijenja način na koji pogreške dolaze do vas. Safecall prevodi unutarnji neuspjeh u COM HRESULT, pa C# pozivatelj hvata iznimku tamo gdje bi ravni DLL vratio tihi cijeli broj koji je pozivatelj morao zapamtiti provjeriti. Isti rad, dva načina neuspjeha, ovisno o tome koju ste binarnu datoteku učitali.

Binarni podaci slijede drugo pravilo specifično za COM. Automatizacijsko sučelje uopće nema pokazivačke parametre. Sve binarno, bilo bajtovi slike koji ulaze ili bajtovi PDF-a koji izlaze, prelazi granicu kao Olevariant kroz metode kao što su AddImageFromVariant i AppendToVariant. Pretvaranje polja bajtova u varijantu je jedna linija u .NET-u. Pokušajte mu umjesto toga predati sirovi pokazivač, pod pretpostavkom da se ionako radi o istom procesu, i dispečerski sloj će odbiti ili uništiti poziv. Još jedan detalj registracije ometa implementacije: COM registracija ovisi o bitnosti, tako da je OCX registriran s 32-bitnim regsvr32 nevidljiv 64-bitnom domaćinu. Taj se nesklad pojavljuje kao poznata nekorisna pogreška "klasa nije registrirana" na stroju korisnika, dugo nakon što je napustila vaš.

Disciplina ručki: instance posjeduju dokumente

Ravni API radi na cjelobrojnim ručkama. DLCreateLibrary vraća instancu. Učitavanje datoteke vraća ID dokumenta unutar te instance. Procesi potpisivanja, popisi nizova i datoteke s izravnim pristupom vraćaju vlastite cjelobrojne ručke, sve unutar iste instance. Životni vijek izgleda isto sa svakog FFI domaćina, prikazan ovdje u Pascalu jer se čita č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;

Dvije stvari proizlaze iz tog stabla vlasništva. DLReleaseLibrary je jedini poziv za čišćenje koji vam je strogo potreban, budući da u jednom potezu oslobađa svaki dokument i procesnu ručku pod instancom. U kratkoj skripti to je dovoljno. In a long-running service it becomes a slow leak with extra ceremony, so release documents as you finish with them rather than letting them pile up until the instance dies. Instanca je također prirodna jedinica izolacije dretvi. Dajte svakoj radnoj dretvi vlastiti InstanceID i nikada nemojte dijeliti jedan među dretvama bez vanjskog zaključavanja, iz istog razloga iz kojeg nikada ne biste dijelili jedan objekt TPDFlib između dretvi.

Vraćeni nizovi znakova su posuđeni, a ne u vlasništvu

Funkcije koje vraćaju tekst, kao što je DLGetPageText, vraćaju PWideChar ili PAnsiChar koji pokazuje na spremnik koji posjeduje i reciklira instanca knjižnice. Ugovor glasi: kopirajte odmah, nikada ne oslobađajte.

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;

U C#-u to znači pretvaranje IntPtr-a u upravljani niz znakova prije sljedećeg poziva knjižnice. U Python ctypes to znači izdvajanje širokog niza znakova iz pokazivača odmah. Držite sirovi pokazivač kroz pozive i napisali ste pogrešku koja prolazi svaki jedinični test, a zatim ne uspijeva čim se dva zahtjeva preklapaju u produkciji, jer je drugi poziv reciklirao spremnik koji je prvi još uvijek čitao. Isto pravilo vlasništva vrijedi u drugom smjeru za povratne pozive registrirane putem DLSetProgressCallback. Bilo koji pokazivač koji knjižnica preda u vaš povratni poziv vrijedi samo za tijelo tog povratnog poziva, a sam objekt povratnog poziva mora ostati živ (prikvačen, u domaćinu s čišćenjem memorije) dokle god ga instanca može pozvati. Delegat prikupljen usred posla klasični je izvor "nasumične" povrede pristupa koja se pojavljuje u .NET povezivanju koje je mjesecima radilo čisto.

Ugradite brzi test (smoke test) u samo povezivanje i pokrenite ga prije slanja bilo kojeg generiranog skupa deklaracija. Isprobajte po jedan poziv iz svake kategorije koja obično izlaže ABI pogreške: funkciju bez parametara kao što je DLCreateLibrary kako biste dokazali da je konvencija ispravna, funkciju koja prima niz znakova s ne-ASCII stazom kako biste dokazali da je kodiranje ispravno, funkciju koja vraća niz znakova kako biste dokazali da je rukovanje posuđenim spremnikom ispravno i jednu operaciju koja namjerno ne uspijeva kako biste mogli vidjeti kako pogreška stiže do vašeg domaćina. To je petnaest minuta rada, a hvata greške u konvenciji pozivanja i kodiranju koje bi se inače pojavile mjesecima kasnije kao korisnički ispis rušenja.

Slučaj Python ctypes, konkretno

Python ctypes je povezivanje koje najčešće vidim ručno izrađeno, i olakšava demonstraciju međuplatformskog razdvajanja. Na Windowsima učitajte knjižnicu s ctypes.WinDLL kako bi ctypes primijenio Stdcall, povežite W funkcije bez sufiksa i deklarirajte svaki parametar niza znakova kao c_wchar_p. Na macOS-u učitajte je s ctypes.CDLL za Cdecl, zadržite isti popis funkcija i razriješite nazive bez vodeće podcrte. Većina FFI slojeva, uključujući ctypes, automatski rješava konvencije podcrte za vas na macOS-u, ali to je pretpostavka koju treba potvrditi s jednim riješenim pozivom prije nego što na tome generirate stotine deklaracija.

Dva pitanja o implementaciji prate rad na povezivanju i imaju jasne odgovore. Obični DLL ne treba registraciju: regsvr32 se odnosi samo na ActiveX verziju, a DLL se isporučuje kopiranjem datoteke, što je glavni razlog zašto mu se daje prednost za Windows usluge i spremnike gdje radije uopće ne biste dirali registar. Sigurnost dretvi svodi se na pravilo koje je već na snazi iznad: jedna instanca po dretvi. Ručka instance drži svaki dio promjenjivog stanja koje mehanizam prati – odabrani dokument, opcije iscrtavanja, postavke ekstrakcije – pa dvije dretve koje dijele instancu isprepliću stanje jedna druge čak i kada svaki pojedini poziv vraća uspjeh.

Nakon što je povezivanje stabilno, operacije s druge strane su upravo one koje Delphi članci detaljno pokrivaju, uključujući primjenu i reviziju šifriranja PDF-a i ekstrakciju teksta i slika iz postojećih dokumenata.

Binarna preuzimanja za sva tri integracijska sloja dolaze s knjižnicom; pogledajte PDFlibPas stranicu proizvoda za izdanja i licenciranje.