Tehnični članak

Vezave PDF Library for Delphi 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. PDF Library for Delphi, 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

Arhitekturni diagram ene fasade TPDFlib, izpostavljene kot DLL Windows s Stdcall, kot objekt avtomatizacije ActiveX s Safecall in kot dylib macOS s Cdecl
Vse tri binarne datoteke si delijo eno samo ravno fasado PDF, a se razlikujejo po klicni konvenciji, ravnanju z nizi in zahtevah registracije

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

// Windowsova vezava (PDFlibDLL64.dll): Stdcall, navadna izvozna imena
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';

// macOSova vezava: ista funkcija, Cdecl in predpona podčrtaja pri izvozu
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;                       // en primerek na delovno nit
  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);                      // sprosti vsak dokument, ki ga ima primerek
  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

Časovnica PDF Library for Delphi, ki protistavlja takojšnjemu kopiranju izposojenega kazalca DLGetPageText zadrževanju istega kazalca do recikliranja temeljnega medpomnilnika v knjižnici
Vrnjeni kazalci char si izposojajo pomnilnik, ki ga primerek reciklira, zato mora kopija nastati, preden pride do naslednjega klica knjižnice
var
  P: PWideChar;
  PageText: string;
begin
  P := DLGetPageText(Inst, 7);   // pointer into a library-owned buffer
  PageText := P;                 // kopiraj zdaj; poznejši klic lahko znova uporabi medpomnilnik
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

PDF Library for Delphi: mreža dva-krat-dva osnovnih preizkusov vezave, ki pokrivajo klicni dogovor, kodiranje nizov, izposojene medpomnilnike in javljanje napak
Štirje poceni preskusi ujamejo napake konvencije, kodiranja in lastništva, preden generirane deklaracije sploh pridejo do strankinega računalnika

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 PDF Library for Delphi za različice in licenciranje