Műszaki cikk

PDF DLL-, ActiveX- és dylib-kötések Delphiben

Íme egy probléma, amely abban a pillanatban előbukkan, amint egy PDF-könyvtár elhagyja a saját nyelvét. Van egy kötése, amely C#-ból Windowson tökéletesen működik. Ugyanezekre a hívásokra macOS-en, Pythonból is szüksége van, ezért lemásolja a windowsos deklarációs fájlt, kicseréli a bináris nevét, és lefuttatja. Minden szimbólum feloldódik. Az első hívás szemetet ad vissza, a második hozzáférési hibával összeomlik, és közben a PDF-kódjából semmi nem változott. A hiba egy réteggel a PDF alatt van: a windowsos exportok Stdcall konvenciót használnak, a macOS dylib ugyanezeket a függvényeket Cdecl konvencióval és vezető aláhúzással exportálja, és az a külsőfüggvény-deklaráció, amely bármelyik részletet elvéti, még egyetlen dokumentum megnyitása előtt megrongálja a vermet

Ez az egész hibaosztály egyetlen tervezési döntésből fakad, amelyet érdemes előre megérteni. A PDF Library for Delphi, a losLab forráshozzáférést adó PDF-motorja Delphihez és C++Builderhez, a teljes objektummodelljét egyetlen lapos facade osztályba, a TPDFlib osztályba csomagolja, majd ezt a facade-ot három bináris alakban szállítja: egy windowsos DLL-ként mintegy 1250 exportált függvénnyel, egy COM/ActiveX automatizálási objektumként és egy macOS dylibként. A PDF-szemantika mindhárom esetben azonos. Ami megharapja az embert, az alatta lévő ABI-ban lakik: hívási konvenciók, sztringkódolások, a leírók tulajdonlása, és hogy melyik oldal szabadíthat fel melyik puffert

Egy facade, három bináris alak

A TPDFlib minden nyilvános függvényének van egy lapos párja, amelynek neve a DL előtag és a metódusnév. A LoadFromFile hívásból DLLoadFromFile, az Encrypt hívásból DLEncrypt, a NewSignProcessFromFile hívásból DLNewSignProcessFromFile lesz. Csaknem minden export első paramétere egy InstanceID, amelyet a DLCreateLibrary ad vissza, és amely azt az objektumhivatkozást pótolja, amelyet egy Delphi-hívó egyébként a kezében tartana. Ezt a leképezést sajátítsa el korán. Azt jelenti ugyanis, hogy a Delphi API-referencia egyben minden más nyelv dokumentációja is: amit az osztály tud, azt a DLL is tudja kiszámítható néven, és egy Pascal metódusszignatúra elolvasásából megtanulja a Pythonból vagy C#-ból szükséges hívást

A windowsos build a PDFlibDLL32.dll és a PDFlibDLL64.dll fájlt állítja elő; azt válassza, amelyik a gazdafolyamat bitszélességéhez illik, mivel egy 64 bites Java- vagy .NET-folyamat semmilyen deklaráció mellett nem tudja betölteni a 32 bites könyvtárat

Architektúradiagram egyetlen TPDFlib facade-ról, amely Stdcall Windows DLL-ként Safecall ActiveX automatizálási objektumként és Cdecl macOS dylibként jelenik meg
Mindhárom bináris ugyanazon a lapos PDF facade-on osztozik mégis eltér a hívási konvencióban a sztringkezelésben és a regisztrációs igényben

Windows: Stdcall példányok és a W/A függvénypárok

Minden sztringet fogadó export kétszer létezik. A széles változat PWideChar értéket vesz át (UTF-16, ami természetesen illik a .NET, a Java és a Python c_wchar_p típusához), az A utótagú változat pedig PAnsiChar típust. A kettő szemantikája azonos, és csak a kódolásban tér el, és épp ez teszi olyan fájdalmassá a keveredésük felderítését: semmi nem dob kivételt, semmi nem ad vissza hibakódot, egyszerűen krix-kraxot kap a metaadatokban, vagy hamis „a fájl nem található” üzenetet minden olyan útvonalra, amelyben a sima ASCII-n túli karakter szerepel. Az első ilyen kódolási hiba egy csapatnak rendszerint egy délutánjába kerül, mert a tünet az adatra mutat, az ok viszont a deklarációban van

// Windows kötés (PDFlibDLL64.dll): Stdcall, egyszerű exportnevek
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 kötés: ugyanaz a függvény, Cdecl, és aláhúzás előtag az exportnéven
function DLCreateLibrary: Integer; cdecl;
  external 'PDFlibDylib.dylib' name '_DLCreateLibrary';

Gazdanyelvenként egyetlen karakterszélességet válasszon, és rögzítse a kötésgenerátorban. Gyakorlati szabály: ha a gazdanyelvnek natív UTF-16 sztringjei vannak, mindenhol a W változatokat kösse be, és soha többé ne nyúljon az A családhoz

macOS: azonos nevek, más ABI

A dylib ugyanazt a DL függvénykészletet exportálja két rendszerszerű változtatással. A hívási konvenció Stdcall helyett Cdecl, és minden exportnév vezető aláhúzást visel (_DLCreateLibrary, _DLLoadFromFile és így tovább). Mindkét változtatás tisztán gépies, ami eszményivé teszi őket egy generált kötéshez, és veszélyessé a windowsos fájl kézzel szerkesztett másolatában. Tartson egyetlen mérvadó függvénylistát, és abból bocsásson ki platformonkénti deklarációkat, ha az eszközkészlete engedi. Ezt kihagyva pontosan azt a veremrongálódást kapja, amelyet az oldal tetején leírtunk, méghozzá csak azon a platformon, amelyet a CI a legkevésbé gyakorol

COM- és ActiveX-gazdák: Safecall és Olevariant rakomány

A VB.NET, a C#, a VBScript és a régi automatizálási gazdák kedvéért az OCX build ugyanezt a facade-ot egy IDispatch automatizálási objektumba, az IPDFlibrary felületbe csomagolja, ahol minden metódus Safecall deklarációt kap. Ez a konvenció megváltoztatja, hogyan érnek el a hibák. A Safecall a belső hibát COM HRESULT értékké fordítja, tehát egy C#-hívó kivételt kap ott, ahol a lapos DLL egy csendes egész számot adott volna vissza, amelynek ellenőrzésére a hívónak emlékeznie kellett. Ugyanaz a művelet, két hibaidióma, attól függően, melyik binárist töltötte be

A bináris adat egy második, COM-specifikus szabályt követ. Az automatizálási felületnek egyáltalán nincsenek mutatóparaméterei. Bármi, ami bináris – bemenő képbájtok vagy kijövő PDF-bájtok –, Olevariant értékként lép át a határon, olyan metódusokon keresztül, mint az AddImageFromVariant és az AppendToVariant. Egy bájttömb variánsba marsallozása .NET-ben egyetlen sor. Próbáljon meg helyette nyers mutatót átadni azon az alapon, hogy úgyis ugyanaz a folyamat, és a diszpécserréteg elutasítja vagy elrontja a hívást. Még egy regisztrációs részlet buktat el telepítéseket: a COM-regisztráció bitszélességenkénti, tehát a 32 bites regsvr32 eszközzel regisztrált OCX láthatatlan egy 64 bites gazda számára. Ez az eltérés a hírhedten haszontalan „a class not registered” üzenet formájában bukkan fel az ügyfél gépén, jóval azután, hogy elhagyta az Önét

Leírófegyelem: a példányok birtokolják a dokumentumokat

A lapos API egész számú leírókon fut. A DLCreateLibrary egy példányt ad vissza. Egy fájl betöltése egy dokumentumazonosítót ad vissza ezen a példányon belül. Az aláírási folyamatok, a sztringlisták és a közvetlen hozzáférésű fájlok mind saját egész számú leírót adnak, és mind ugyanahhoz a példányhoz tartoznak. Az életciklus minden FFI-gazdából egyformán néz ki, itt Pascalban, mert így olvasható a legtisztábban:

var
  Inst, Doc: Integer;
begin
  Inst := DLCreateLibrary;                       // szálanként egy példány
  try
    Doc := DLLoadFromFile(Inst, 'in.pdf', '');   // DocumentID-t ad, hiba esetén 0-t
    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);                      // a példány minden dokumentumát felszabadítja
  end;
end;

Ebből a tulajdonlási fából két dolog következik. A DLReleaseLibrary az egyetlen takarítóhívás, amelyre szigorúan szüksége van, mivel egyetlen mozdulattal lebontja a példány alatti összes dokumentum- és folyamatleírót. Egy rövid szkriptben ez elég. Egy hosszan futó szolgáltatásban lassú szivárgássá válik, csak több szertartással, ezért a dokumentumokat akkor engedje el, amikor végzett velük, ahelyett hogy a példány haláláig gyűlnének. A példány egyben a szálelszigetelés természetes egysége is. Adjon minden munkaszálnak saját InstanceID azonosítót, és külső zárolás nélkül soha ne ossza meg egyiket sem a szálak között, ugyanabból az okból, amiért egyetlen TPDFlib objektumot sem osztana meg szálak között

A visszaadott sztringek kölcsönbe kapottak, nem sajátok

A szöveget visszaadó függvények, például a DLGetPageText, olyan PWideChar vagy PAnsiChar értéket adnak vissza, amely a könyvtárpéldány tulajdonában lévő és általa újrahasznosított pufferbe mutat. A szerződés így szól: azonnal másolja le, és soha ne szabadítsa fel

PDF Library for Delphi idővonal, amely szembeállítja a kölcsönkapott DLGetPageText mutató azonnali másolását azzal, ha addig tartjuk, amíg a könyvtár újrahasznosítja az alatta lévő puffert
A visszaadott karaktermutatók olyan tárterületet kölcsönöznek amelyet a példány újrahasznosít ezért a másolásnak a következő könyvtárhívás előtt kell megtörténnie
var
  P: PWideChar;
  PageText: string;
begin
  P := DLGetPageText(Inst, 7);   // mutató a könyvtár birtokolta pufferbe
  PageText := P;                 // másoljon most; későbbi hívás újrahasználhatja a puffert
end;

C#-ban ez azt jelenti, hogy az IntPtr értéket a következő könyvtárhívás előtt felügyelt sztringgé kell marsallozni. Python ctypes esetén azt, hogy a széles sztringet azonnal ki kell szeletelni a mutatóból. Ha a nyers mutatót hívásokon átívelően tartja meg, olyan hibát írt, amely minden egységteszten átmegy, majd az első alkalommal elbukik, amikor éles üzemben két kérés átfedi egymást, mert a második hívás újrahasznosította azt a puffert, amelyet az első még olvasott. Ugyanez a tulajdonlási szabály fordított irányban is fut a DLSetProgressCallback hívással regisztrált visszahívásokra. Minden mutató, amelyet a könyvtár a visszahívásába ad, csak a visszahívás törzsének idejére érvényes, és magának a visszahívási objektumnak életben kell maradnia (rögzítve, szemétgyűjtött gazdában) mindaddig, amíg a példány még meghívhatja. Egy munka közben begyűjtött delegált a tankönyvi forrása annak a „véletlenszerű” hozzáférési hibának, amely egy hónapokig tisztán futó .NET-kötésben bukkan fel

Építsen füstpróbát magába a kötésbe, és futtassa le, mielőtt bármilyen generált deklarációkészlet kiszállna. Gyakoroljon egy-egy hívást minden olyan kategóriából, amely az ABI-hibákat elő szokta hozni: egy paraméter nélküli függvényt, például a DLCreateLibrary hívást annak igazolására, hogy a konvenció jó; egy sztringet fogadó függvényt nem ASCII karaktereket tartalmazó útvonallal annak igazolására, hogy a kódolás jó; egy sztringet visszaadó függvényt annak igazolására, hogy a kölcsönpuffer kezelése jó; és egy szándékosan elbukó műveletet, hogy lássa, hogyan ér el egy hiba a gazdájáig. Ez tizenöt perc munka, és elkapja azokat a hívási konvenciós és kódolási hibákat, amelyek különben hónapokkal később, ügyféltől érkező összeomlási memóriaképként állítanának be

PDF Library for Delphi kettő szor kettes rácsa a kötések füstpróbáiról, amelyek a hívási konvenciót a sztringkódolást a kölcsönpuffereket és a hibák felszínre kerülését fedik le
Négy olcsó szonda elkapja a konvenció a kódolás és a tulajdonlás hibáit még mielőtt a generált deklarációk ügyfélgépre kerülnének

A Python ctypes esete, kézzelfoghatóan

A Python ctypes az a kötés, amelyet a leggyakrabban látok kézzel összeütve, és jól szemlélteti a platformok közti hasadást. Windowson a ctypes.WinDLL hívással töltse be a könyvtárat, hogy a ctypes Stdcall konvenciót alkalmazzon, kösse be az utótag nélküli W függvényeket, és minden sztringparamétert c_wchar_p típusúnak deklaráljon. macOS-en a ctypes.CDLL hívással töltse be a Cdecl kedvéért, tartsa meg az azonos függvénylistát, és a neveket vezető aláhúzás nélkül oldja fel. A legtöbb FFI-réteg, a ctypes is, macOS-en visszahelyezi Ön helyett az aláhúzásos konvenciót, de épp ez az az egy feltevés, amelyet egyetlen feloldott hívással érdemes megerősíteni, mielőtt több száz deklarációt épít rá

Két telepítési kérdés kíséri a kötési munkát, és mindkettőre éles válasz van. A sima DLL-nek nincs szüksége regisztrációra: a regsvr32 csak az ActiveX buildre vonatkozik, a DLL pedig fájlmásolással szállítható, és főként ezért érdemes ezt választani windowsos szolgáltatásokhoz és konténerekhez, ahol inkább hozzá sem nyúlna a rendszerleíró adatbázishoz. A szálbiztonság a fent már működő szabályra egyszerűsödik: szálanként egy példány. A példányleíró tartja a motor által követett összes változó állapotot, a kiválasztott dokumentumot, a rajzolási beállításokat, a kinyerési beállításokat, tehát két, egy példányon osztozó szál akkor is egymás állapotába gázol, ha minden egyes hívás sikert jelent

Amint a kötés szilárd, a túloldali műveletek pontosan azok, amelyeket a Delphi-cikkek részletesen tárgyalnak, köztük a PDF-titkosítás alkalmazása és auditálása, valamint a szöveg és a képek kinyerése meglévő dokumentumokból

Mindhárom integrációs réteg bináris letöltése a könyvtárral együtt érkezik; a kiadásokért és a licencelésért lásd a PDF Library for Delphi termékoldalt