Teknisk artikel

PDFlibPas DLL, ActiveX och dylib-bindningar: Att anropa en PDF-motor från vilket språk som helst

Här är ett problem som dyker upp i samma ögonblick ett PDF-bibliotek lämnar sitt hemspråk (home language). Du har en bindning (binding) som fungerar perfekt från C# på Windows. Du behöver samma anrop från Python på macOS, så du kopierar Windows-deklarationsfilen, byter ut binärnamnet, och kör den. Varje symbol löses upp (resolves). Det första anropet returnerar skräp (garbage), det andra kraschar med en åtkomstöverträdelse (access violation), och ingen av din PDF-kod har ändrats. Felet ligger ett lager under PDF:en: Windows-exporterna använder Stdcall-konventionen, macOS-dylib:en exporterar samma funktioner som Cdecl med ett inledande understreck (leading underscore), och en foreign-function-deklaration som får endera detalj fel korrumperar stacken innan ett enda dokument har öppnats

Hela den klassen av misslyckanden (failures) kommer från ett designbeslut värt att förstå från början. PDFlibPas, losLabs PDF-motor med källkod tillgänglig för Delphi och C++Builder, omsluter hela sin objektmodell i en enda platt fasadklass, TPDFlib, och skeppar sedan den fasaden i tre binära former: en Windows DLL med ungefär 1 250 exporterade funktioner, ett COM/ActiveX-automationsobjekt, och en macOS dylib. PDF-semantiken är identisk tvärs över alla tre. Den del som biter dig lever i ABI:t därunder: anropskonventioner (calling conventions), strängkodningar (string encodings), handtagsägarskap (handle ownership), och vilken sida som tillåts frigöra (free) vilken buffert

En fasad, tre binära former

Varje publik funktion i TPDFlib har en platt motsvarighet med namnet DL plus metodnamnet. LoadFromFile blir DLLoadFromFile, Encrypt blir DLEncrypt, NewSignProcessFromFile blir DLNewSignProcessFromFile. Den första parametern i nästan varje export är ett InstanceID som returneras av DLCreateLibrary, som ersätter den objektreferens en Delphi-anropare annars skulle hålla. Internalisera (Internalize) den mappningen tidigt. Det innebär att Delphis API-referens dubblerar som dokumentation för alla andra språk: vad klassen än kan göra, kan DLL:en göra under ett förutsägbart namn, och du kan läsa en Pascal-metodsignatur för att lära dig det anrop du behöver från Python eller C#

Windows-bygget producerar PDFlibDLL32.dll och PDFlibDLL64.dll; välj den som matchar din värdprocess bitness (host process bitness), eftersom en 64-bitars Java- eller .NET-process inte kan ladda 32-bitarsbiblioteket oavsett hur deklarationen ser ut

Windows: Stdcall-instanser och W/A-funktionsparen

Varje strängtagande export (string-taking export) existerar två gånger. En bred version tar PWideChar (UTF-16, den naturliga passformen för .NET, Java och Pythons c_wchar_p), och en A-suffixerad version tar PAnsiChar. De två bär på identisk semantik och skiljer sig enbart åt i kodning, vilket är exakt vad som gör det så smärtsamt att spåra upp (track down) om man blandar dem: inget kastar (throws), inget returnerar en felkod, du får helt enkelt mojibake i metadata eller en falsk "filen hittades inte" ("file not found") för varje sökväg med ett tecken bortom ren ASCII. Den första kodningsbuggen (encoding bug) ett team stöter på (hits) på det här sättet kostar vanligtvis en eftermiddag, eftersom symptomet pekar på datan och orsaken finns i deklarationen

// 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';

Välj en teckenbredd (character width) per värd och kodifiera den i bindningsgeneratorn (binding generator). En praktisk regel: om värdspråket har inbyggda UTF-16-strängar, bind W-versionerna överallt och rör aldrig A-familjen igen

macOS: samma namn, annat ABI

Dylib:en exporterar samma DL-funktionsuppsättning med två systematiska ändringar. Anropskonventionen (calling convention) är Cdecl snarare än Stdcall, och varje exportnamn bär ett inledande understreck (_DLCreateLibrary, _DLLoadFromFile, och så vidare). Båda ändringarna är rent mekaniska, vilket gör dem idealiska för en genererad bindning och farliga för en handredigerad kopia av Windows-filen. Håll en kanonisk (canonical) funktionslista och generera per-plattform-deklarationer utifrån den om dina verktyg (tooling) tillåter det. Hoppa över det och du får exakt den stackkorruption som beskrivs högst upp på denna sida, reproducerbar endast på den plattform din CI råkar exekvera (exercise) minst

COM- och ActiveX-värdar: Safecall- och Olevariant-nyttolaster

För VB.NET, C#, VBScript och äldre automationsvärdar (legacy automation hosts), slår OCX-bygget in samma fasad i ett IDispatch-automationsobjekt, IPDFlibrary, med varje metod deklarerad Safecall. Den konventionen ändrar hur fel når dig. Safecall översätter ett internt fel till ett COM HRESULT, så en C#-anropare fångar (catches) ett undantag (exception) där den platta DLL:en skulle ha returnerat ett tyst heltal som anroparen var tvungen att komma ihåg att kontrollera. Samma operation, två felidiom (failure idioms), beroende på vilken binär du laddade

Binär data följer en andra COM-specifik regel. Automationsgränssnittet (automation interface) har inga pekarparametrar (pointer parameters) alls. Allt binärt, bildbytes in eller PDF-bytes ut, korsar gränsen (crosses the boundary) som en Olevariant genom metoder såsom AddImageFromVariant och AppendToVariant. Att marskalkera (marshaling) en bytearray in i en variant är en enda rad i .NET. Försök att räcka den en rå pekare (raw pointer) i stället, på resonemanget att det är samma process i alla fall, och dispatch-lagret (dispatch layer) avvisar eller förvanskar (mangles) anropet. Ännu en registreringsdetalj fäller (trips up) driftsättningar (deployments): COM-registrering är per-bitness, så en OCX registrerad med den 32-bitars regsvr32 är osynlig för en 64-bitarsvärd. Den obalansen yppar sig (surfaces) som det ökänt ohjälpsamma "klassen är inte registrerad" ("class not registered") på kundens maskin, långt efter att den lämnade din

Handtagsdisciplin (Handle discipline): instanser äger dokument

Det platta API:et körs på heltalshandtag (integer handles). DLCreateLibrary returnerar en instans. Att ladda en fil returnerar ett dokument-ID inuti den instansen. Signeringsprocesser, string-lists, och direktåtkomstfiler returnerar var och en sina egna heltalshandtag, alla med skop (scoped) till samma instans. Livscykeln (lifecycle) ser likadan ut från vilken FFI-värd som helst, visat här i Pascal för att den läses tydligt (cleanly):

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;

Två saker följer av det ägarskapsträdet (ownership tree). DLReleaseLibrary är det enda uppstädningsanrop (cleanup call) du strikt behöver, eftersom den river ner (tears down) varje dokument- och processhandtag under instansen i ett enda slag (in one shot). I ett kort skript är det tillräckligt. I en långkörande tjänst (long-running service) blir det en långsam läcka med extra ceremoni (ceremony), så frigör dokument allteftersom du blir färdig med dem snarare än att låta dem staplas på hög (pile up) tills instansen dör. Instansen är också den naturliga enheten för trådisolering (thread isolation). Ge varje arbetstråd (worker thread) sitt eget InstanceID, och dela aldrig ett tvärs över trådar utan extern låsning (external locking), av samma anledning som du aldrig skulle dela ett enda TPDFlib-objekt mellan trådar

Returnerade strängar är lånade, inte ägda

Funktioner som returnerar text, såsom DLGetPageText, lämnar tillbaka en PWideChar eller PAnsiChar som pekar in i en buffert ägd och återvunnen (recycled) av biblioteksinstansen. Kontraktet är: kopiera omedelbart, frigör aldrig (never free)

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;

I C# betyder det att man marskalkerar (marshaling) IntPtr in till en hanterad sträng (managed string) innan nästa biblioteksanrop. I Python ctypes, betyder det att man skivar ut (slicing out) den breda strängen ur pekaren direkt. Håll i den råa pekaren över anrop och du har skrivit en bugg som klarar varje enhetstest och sedan misslyckas första gången två begäranden (requests) överlappar (overlap) i produktion, eftersom det andra anropet återvann bufferten som det första fortfarande höll på att läsa. Samma äganderegel löper åt andra hållet för callbacks registrerade genom DLSetProgressCallback. Varje pekare biblioteket räcker in i din callback är giltig enbart för kroppen (body) av den callbacken, och själva callback-objektet måste hållas vid liv (pinned, i en garbage-collected värd) under så lång tid som instansen eventuellt fortfarande skulle kunna åkalla den. Ett delegat (delegate) insamlat (collected) mitt i ett jobb är lärobokskällan till den "slumpmässiga" ("random") åtkomstöverträdelsen (access violation) som uppträder i en .NET-bindning som körts rent i månader

Bygg in ett röktest (smoke test) i själva bindningen, och kör det innan någon genererad deklarationsuppsättning (declaration set) skeppas. Öva (Exercise) ett anrop från varje kategori som tenderar att exponera ABI-misstag: en parameterlös funktion såsom DLCreateLibrary för att bevisa att konventionen stämmer, en sträng-in-funktion (string-in) matad med en sökväg med icke-ASCII-tecken för att bevisa att kodningen stämmer, en sträng-ut-funktion (string-out) för att bevisa att den lånade buffert-hanteringen stämmer, och en operation som misslyckas med flit så att du kan iaktta (watch) hur ett fel når din värd. Det är en kvarts arbete, och det fångar anropskonventions- (calling-convention) och kodningsfelen (encoding faults) som annars skulle anlända månader senare som en kunds kraschdump (crash dump)

Fallet Python ctypes, konkret

Python ctypes är den bindning jag ser handrullad (hand-rolled) mest frekvent, och den gör tvärplattforms-delningen (cross-platform split) enkel att demonstrera. På Windows, ladda biblioteket med ctypes.WinDLL så att ctypes applicerar Stdcall, bind de osuffixerade W-funktionerna, och deklarera varje strängparameter som c_wchar_p. På macOS, ladda det med ctypes.CDLL för Cdecl, behåll den identiska funktionslistan, och lös upp namnen utan det inledande understrecket (leading underscore). De flesta FFI-lager, ctypes inräknat, viker tillbaka (folds back in) understreck-konventionen åt dig på macOS, men det är det antagandet man vill bekräfta med ett enda upplöst anrop (resolved call) innan du genererar hundratals deklarationer ovanpå det

Två driftsättningsfrågor följer i släptåg på (trail) bindningsarbetet och har skarpa (crisp) svar. Den rena (plain) DLL:en behöver ingen registrering: regsvr32 appliceras bara på ActiveX-bygget, och DLL:en skeppas via filkopiering, vilket är det huvudsakliga skälet att föredra den för Windows-tjänster (Windows services) och containers där man helst inte vill röra registret alls. Trådsäkerhet reduceras till den regel som redan är i spel ovan, en instans per tråd. Instanshandtaget (instance handle) håller varje bit muterbart tillstånd (mutable state) motorn spårar, det valda dokumentet, renderingsalternativen, extraheringsinställningarna, så två trådar som delar en instans varvar (interleave) varandras tillstånd även när varje individuellt anrop returnerar framgång

När en bindning väl är solid, är operationerna på andra sidan exakt de som Delphi-artiklarna täcker på djupet, inklusive applicering och revision av PDF-kryptering och att extrahera text och bilder från existerande dokument

Binära nedladdningar för alla tre integreringslager skeppas med biblioteket; se PDFlibPas produktsida för utgåvor och licensiering