Tady je problém, který se objeví ve chvíli, kdy knihovna PDF opustí svůj domovský jazyk. Máte vazbu, která z C# na Windows funguje bezchybně. Potřebujete stejná volání z Pythonu na macOS, tak zkopírujete deklarační soubor pro Windows, vyměníte název binárky a spustíte to. Všechny symboly se najdou. První volání vrátí nesmysl, druhé spadne na porušení přístupu, a přitom se váš kód pro PDF vůbec nezměnil. Chyba leží o vrstvu níž, než je PDF: exporty pro Windows používají konvenci Stdcall, dylib pro macOS exportuje tytéž funkce jako Cdecl s podtržítkem na začátku, a deklarace cizí funkce, která kterýkoli z těch detailů popíše špatně, poškodí zásobník dřív, než se otevře jediný dokument
Celá tahle třída selhání pramení z jednoho návrhového rozhodnutí, které se vyplatí pochopit hned na začátku. PDF Library for Delphi, engine PDF se zpřístupněnými zdrojovými kódy od losLab pro Delphi a C++Builder, obaluje celý svůj objektový model do jediné ploché fasádní třídy TPDFlib a tuto fasádu pak dodává ve třech binárních podobách: jako DLL pro Windows se zhruba 1 250 exportovanými funkcemi, jako automatizační objekt COM/ActiveX a jako dylib pro macOS. Sémantika PDF je ve všech třech totožná. To, co vás kousne, sídlí v ABI pod nimi: konvence volání, kódování řetězců, vlastnictví handlů a otázka, která strana smí uvolnit který buffer
Jedna fasáda, tři binární podoby
Každá veřejná funkce třídy TPDFlib má plochý protějšek pojmenovaný jako DL plus název metody. Z LoadFromFile se stane DLLoadFromFile, z Encrypt se stane DLEncrypt, z NewSignProcessFromFile se stane DLNewSignProcessFromFile. Prvním parametrem téměř každého exportu je InstanceID vrácené funkcí DLCreateLibrary, které zastupuje objektovou referenci, již by jinak volající v Delphi držel. Toto mapování si osvojte co nejdřív. Znamená totiž, že referenční příručka rozhraní Delphi slouží zároveň jako dokumentace pro každý další jazyk: co umí třída, umí i DLL pod předvídatelným názvem, a z hlavičky metody v Pascalu vyčtete volání, které potřebujete z Pythonu nebo C#
Sestavení pro Windows vytváří PDFlibDLL32.dll a PDFlibDLL64.dll; vyberte tu, která odpovídá bitové šířce vašeho hostitelského procesu, protože 64bitový proces Javy nebo .NET nedokáže načíst 32bitovou knihovnu, ať už deklarace vypadá jakkoli
Windows: instance přes Stdcall a dvojice funkcí W/A
Každý export přijímající řetězec existuje dvakrát. Široká varianta bere PWideChar (UTF-16, přirozená volba pro .NET, Javu i pythonovský c_wchar_p), varianta s příponou A bere PAnsiChar. Obě mají totožnou sémantiku a liší se pouze kódováním, což je přesně to, proč se jejich smíchání tak obtížně dohledává: nic nevyhodí výjimku, nic nevrátí chybový kód, prostě dostanete mojibake v metadatech nebo falešné „soubor nenalezen“ u každé cesty se znakem mimo prosté ASCII. První chyba kódování, na kterou tým takto narazí, obvykle stojí celé odpoledne, protože příznak ukazuje na data a příčina sedí v deklaraci
// vazba pro Windows (PDFlibDLL64.dll): Stdcall, prosté názvy exportů
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';
// vazba pro macOS: tatáž funkce, Cdecl a podtržítko před názvem exportu
function DLCreateLibrary: Integer; cdecl;
external 'PDFlibDylib.dylib' name '_DLCreateLibrary';
Zvolte pro každého hostitele jednu šířku znaku a zakotvěte ji v generátoru vazeb. Praktické pravidlo: má-li hostitelský jazyk nativní řetězce UTF-16, navažte všude varianty W a rodiny A se už nikdy nedotýkejte
macOS: stejné názvy, jiné ABI
Dylib exportuje tutéž sadu funkcí DL se dvěma systematickými změnami. Konvence volání je Cdecl místo Stdcall a každý název exportu nese úvodní podtržítko (_DLCreateLibrary, _DLLoadFromFile a tak dále). Obě změny jsou čistě mechanické, což z nich dělá ideální materiál pro generovanou vazbu a nebezpečnou past pro ručně upravenou kopii souboru pro Windows. Udržujte jediný kanonický seznam funkcí a nechte z něj, pokud to vaše nástroje umožní, vygenerovat deklarace pro jednotlivé platformy. Přeskočte to a získáte přesně to poškození zásobníku popsané na začátku této stránky, reprodukovatelné jen na platformě, kterou vaše CI zrovna prověřuje nejméně
Hostitelé COM a ActiveX: Safecall a data v Olevariant
Pro VB.NET, C#, VBScript a starší automatizační hostitele obaluje sestavení OCX tutéž fasádu do automatizačního objektu IDispatch jménem IPDFlibrary, jehož každá metoda je deklarovaná jako Safecall. Tato konvence mění způsob, jakým se k vám dostávají chyby. Safecall převádí vnitřní selhání na HRESULT podle COM, takže volající z C# zachytí výjimku tam, kde by plochá DLL vrátila tiché celé číslo, na jehož kontrolu si volající musel vzpomenout. Tatáž operace, dva idiomy selhání, podle toho, kterou binárku jste načetli
Binární data se řídí druhým pravidlem specifickým pro COM. Automatizační rozhraní nemá vůbec žádné ukazatelové parametry. Cokoli binárního, ať jde o vstupní bajty obrázku nebo výstupní bajty PDF, překračuje hranici jako Olevariant přes metody typu AddImageFromVariant a AppendToVariant. Zabalit pole bajtů do variantu je v .NET jediný řádek. Zkuste místo toho předat surový ukazatel s odůvodněním, že jde stejně o tentýž proces, a dispečerská vrstva volání odmítne nebo zkomolí. Nasazení podráží nohy ještě jeden registrační detail: registrace COM je vázaná na bitovou šířku, takže OCX zaregistrované 32bitovým regsvr32 je pro 64bitového hostitele neviditelné. Tento nesoulad se projeví proslulým a naprosto nenápomocným hlášením „class not registered“ na zákaznickém stroji, dlouho poté, co od vás software odešel
Disciplína handlů: instance vlastní dokumenty
Ploché API běží na celočíselných handlech. DLCreateLibrary vrátí instanci. Načtení souboru vrátí ID dokumentu uvnitř této instance. Podepisovací procesy, seznamy řetězců i soubory v režimu přímého přístupu vracejí každý svůj vlastní celočíselný handle, všechny v rámci téže instance. Životní cyklus vypadá z libovolného hostitele FFI stejně; tady je ukázán v Pascalu, protože se dobře čte:
var
Inst, Doc: Integer;
begin
Inst := DLCreateLibrary; // jedna instance na každé pracovní vlákno
try
Doc := DLLoadFromFile(Inst, 'in.pdf', ''); // vrací DocumentID, při selhání 0
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); // uvolní každý dokument, který instance vlastní
end;
end;
Z toho vlastnického stromu plynou dvě věci. DLReleaseLibrary je jediné úklidové volání, které nezbytně potřebujete, protože jednou ranou zboří každý dokument i procesní handle pod danou instancí. V krátkém skriptu to stačí. V dlouho běžící službě se z toho stane pomalý únik paměti s dodatečným obřadem, takže dokumenty uvolňujte, jakmile s nimi skončíte, místo abyste je nechali kupit až do zániku instance. Instance je také přirozenou jednotkou izolace vláken. Dejte každému pracovnímu vláknu vlastní InstanceID a nikdy jednu instanci nesdílejte mezi vlákny bez vnějšího zamykání, ze stejného důvodu, z jakého byste nikdy nesdíleli jediný objekt TPDFlib mezi vlákny
Vrácené řetězce jsou zapůjčené, ne vaše
Funkce vracející text, například DLGetPageText, vracejí PWideChar nebo PAnsiChar, který ukazuje do bufferu vlastněného a recyklovaného instancí knihovny. Kontrakt zní: okamžitě zkopírovat, nikdy neuvolňovat
var
P: PWideChar;
PageText: string;
begin
P := DLGetPageText(Inst, 7); // ukazatel do bufferu vlastněného knihovnou
PageText := P; // kopírujte hned; pozdější volání může buffer znovu použít
end;
V C# to znamená převést IntPtr na spravovaný řetězec ještě před dalším voláním knihovny. V pythonovském ctypes to znamená okamžitě vyříznout široký řetězec z ukazatele. Podržte si surový ukazatel napříč voláními a máte napsanou chybu, která projde každým jednotkovým testem a pak selže poprvé ve chvíli, kdy se v produkci překryjí dva požadavky, protože druhé volání recyklovalo buffer, z něhož první ještě četlo. Totéž pravidlo vlastnictví platí opačným směrem pro callbacky registrované přes DLSetProgressCallback. Jakýkoli ukazatel, který knihovna předá do vašeho callbacku, je platný pouze po dobu těla toho callbacku a samotný objekt callbacku musí zůstat naživu (v hostiteli se správcem paměti připnutý) tak dlouho, dokud jej instance ještě může vyvolat. Delegát uklizený uprostřed úlohy je učebnicovým zdrojem „náhodného“ porušení přístupu, jaké se objeví ve vazbě pro .NET, která měsíce běžela bez jediné chyby
Zabudujte přímo do vazby kouřový test a spusťte jej dřív, než jakákoli vygenerovaná sada deklarací opustí dům. Projděte po jednom volání z každé kategorie, která chyby ABI obvykle odhalí: bezparametrovou funkci jako DLCreateLibrary, aby se prokázala správná konvence, funkci přijímající řetězec, které předáte cestu se znaky mimo ASCII, aby se prokázalo správné kódování, funkci vracející řetězec, aby se prokázalo správné zacházení se zapůjčeným bufferem, a jednu operaci, která selže záměrně, abyste viděli, jak se chyba dostane k vašemu hostiteli. To je patnáct minut práce a odchytí to chyby v konvenci volání i v kódování, jež by jinak dorazily o měsíce později v podobě zákaznického crash dumpu
Konkrétní případ: ctypes v Pythonu
Pythonovské ctypes je vazba, kterou vidím ručně psanou nejčastěji, a hezky se na ní ukazuje rozdělení mezi platformami. Na Windows načtěte knihovnu přes ctypes.WinDLL, aby ctypes použilo Stdcall, navažte funkce W bez přípony a každý řetězcový parametr deklarujte jako c_wchar_p. Na macOS ji načtěte přes ctypes.CDLL kvůli Cdecl, ponechte totožný seznam funkcí a názvy hledejte bez úvodního podtržítka. Většina vrstev FFI včetně ctypes si konvenci s podtržítkem na macOS doplní za vás, ale právě tenhle předpoklad si potvrďte jediným úspěšně vyřešeným voláním dřív, než nad ním vygenerujete stovky deklarací
Za prací na vazbě se táhnou dvě otázky nasazení a obě mají jasnou odpověď. Prostá DLL nepotřebuje žádnou registraci: regsvr32 se týká pouze sestavení ActiveX a DLL se distribuuje kopírováním souboru, což je hlavní důvod, proč ji upřednostnit pro služby a kontejnery na Windows, kde se raději registru vůbec nedotknete. Bezpečnost vůči vláknům se smrskne na pravidlo, které už výše platí: jedna instance na vlákno. Handle instance drží každý kousek proměnlivého stavu, který engine sleduje, tedy vybraný dokument, volby vykreslování i nastavení extrakce, takže dvě vlákna sdílející jednu instanci si navzájem prokládají stav i tehdy, když každé jednotlivé volání vrátí úspěch
Jakmile je vazba pevná, operace na její druhé straně jsou přesně ty, jimž se do hloubky věnují články o Delphi, včetně toho, jak nasadit a auditovat šifrování PDF nebo extrahovat text a obrázky z existujících dokumentů
Binární balíčky pro všechny tři integrační vrstvy se dodávají spolu s knihovnou; edice a licencování najdete na produktové stránce PDF Library for Delphi