Í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
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
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
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