Teknisk artikel

PDF Library for Delphi DLL-, ActiveX- og dylib-bindings: Kald én PDF-motor fra ethvert sprog

Her er et problem, der dukker op i det øjeblik, et PDF-bibliotek forlader sit hjemmesprog. Du har en binding, der virker perfekt fra C# på Windows. Du skal bruge de samme kald fra Python på macOS, så du kopierer Windows-deklarationsfilen, skifter binærnavnet ud og kører den. Alle symboler bliver fundet. Det første kald returnerer volapyk, det andet crasher med en access violation, og intet i din PDF-kode er ændret. Fejlen ligger ét lag under PDF'en: Windows-eksporterne bruger Stdcall-konventionen, macOS-dylib'en eksporterer de samme funktioner som Cdecl med en indledende understregning, og en foreign function-deklaration, der rammer bare en af de to detaljer forkert, ødelægger stakken, før et eneste dokument er åbnet

Hele den klasse af fejl udspringer af én designbeslutning, der er værd at forstå på forhånd. PDF Library for Delphi, losLabs PDF-motor med tilgængelig kildekode til Delphi og C++Builder, pakker hele sin objektmodel ind i én flad facadeklasse, TPDFlib, og leverer så den facade i tre binære skikkelser: en Windows-DLL med omtrent 1.250 eksporterede funktioner, et COM/ActiveX-automatiseringsobjekt og en macOS-dylib. PDF-semantikken er identisk på tværs af alle tre. Det, der bider dig, bor i ABI'en nedenunder: kaldekonventioner, strengkodninger, ejerskab af handles, og hvilken side der må frigive hvilken buffer

Én facade, tre binære skikkelser

Hver offentlig funktion i TPDFlib har en flad modpart, der hedder DL plus metodenavnet. LoadFromFile bliver til DLLoadFromFile, Encrypt bliver til DLEncrypt, NewSignProcessFromFile bliver til DLNewSignProcessFromFile. Den første parameter i næsten hver eksport er et InstanceID, som DLCreateLibrary returnerer, og som træder i stedet for den objektreference, en Delphi-kalder ellers ville holde på. Gør dig den afbildning fortrolig tidligt. Den betyder, at Delphi-API-referencen samtidig fungerer som dokumentation for ethvert andet sprog: hvad klassen kan, kan DLL'en også under et forudsigeligt navn, og du kan læse en Pascal-metodesignatur for at lære det kald, du skal bruge fra Python eller C#

Windows-builden producerer PDFlibDLL32.dll og PDFlibDLL64.dll; vælg den, der passer til din værtsproces' bitbredde, for en 64-bit Java- eller .NET-proces kan ikke indlæse 32-bit-biblioteket, uanset hvordan deklarationen ser ud

Arkitekturdiagram over én TPDFlib-facade eksponeret som en Stdcall Windows-DLL, et Safecall ActiveX-automatiseringsobjekt og en Cdecl macOS-dylib
Alle tre binære filer deler én flad PDF-facade, men adskiller sig i kaldkonvention, strenghåndtering og registreringskrav

Windows: Stdcall-instanser og W/A-funktionsparrene

Hver eksport, der tager strenge, findes to gange. En wide-version tager PWideChar (UTF-16, det naturlige valg til .NET, Java og Pythons c_wchar_p), og en version med A-suffiks tager PAnsiChar. De to har identisk semantik og adskiller sig kun i kodning, hvilket er præcis det, der gør det så pinefuldt at opspore, når man blander dem: intet kaster en undtagelse, intet returnerer en fejlkode, du får bare mojibake i metadata eller en falsk "file not found" for enhver sti med et tegn ud over ren ASCII. Den første kodningsfejl, et team rammer på den måde, koster som regel en eftermiddag, for symptomet peger på dataene, mens årsagen ligger i deklarationen

// Windows-binding (PDFlibDLL64.dll): Stdcall, almindelige eksportnavne
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: samme funktion, Cdecl og et understregningspræfiks på eksporten
function DLCreateLibrary: Integer; cdecl;
  external 'PDFlibDylib.dylib' name '_DLCreateLibrary';

Vælg én tegnbredde per vært, og skriv den ind i bindingsgeneratoren. En praktisk regel: har værtssproget indbyggede UTF-16-strenge, så bind W-versionerne overalt og rør aldrig A-familien igen

macOS: samme navne, anden ABI

Dylib'en eksporterer det samme sæt DL-funktioner med to systematiske ændringer. Kaldekonventionen er Cdecl frem for Stdcall, og hvert eksportnavn bærer en indledende understregning (_DLCreateLibrary, _DLLoadFromFile og så videre). Begge ændringer er rent mekaniske, hvilket gør dem ideelle til en genereret binding og farlige i en håndredigeret kopi af Windows-filen. Hold én kanonisk funktionsliste, og udsend platformsspecifikke deklarationer ud fra den, hvis dit værktøj tillader det. Springer du det over, får du præcis den stakødelæggelse, der beskrives øverst på denne side, og den reproducerer kun på den platform, din CI tilfældigvis motionerer mindst

COM- og ActiveX-værter: Safecall og Olevariant-nyttelast

Til VB.NET, C#, VBScript og ældre automatiseringsværter pakker OCX-builden den samme facade ind i et IDispatch-automatiseringsobjekt, IPDFlibrary, hvor hver metode er erklæret Safecall. Den konvention ændrer, hvordan fejl når frem til dig. Safecall oversætter en intern fejl til et COM-HRESULT, så en C#-kalder fanger en undtagelse dér, hvor den flade DLL ville have returneret et stilfærdigt heltal, kalderen selv skulle huske at tjekke. Samme operation, to fejlidiomer, alt efter hvilken binær du indlæste

Binære data følger en anden COM-specifik regel. Automatiseringsgrænsefladen har slet ingen pointerparametre. Alt binært, billedbytes på vej ind eller PDF-bytes på vej ud, krydser grænsen som en Olevariant gennem metoder som AddImageFromVariant og AppendToVariant. At marshalle et byte-array ind i en variant er en enkelt linje i .NET. Prøver du i stedet at række den en rå pointer med den begrundelse, at det jo er samme proces, afviser eller forvansker dispatch-laget kaldet. Endnu en registreringsdetalje spænder ben for udrulninger: COM-registrering er per bitbredde, så en OCX registreret med 32-bit-regsvr32 er usynlig for en 64-bit-vært. Den uoverensstemmelse viser sig som det berømt uhjælpsomme "class not registered" på kundens maskine, længe efter at den forlod din

Handle-disciplin: instanser ejer dokumenter

Det flade API kører på heltals-handles. DLCreateLibrary returnerer en instans. At indlæse en fil returnerer et dokument-ID inde i den instans. Signeringsprocesser, strenglister og direct access-filer returnerer hver deres eget heltals-handle, alle med samme instans som virkefelt. Livscyklussen ser ens ud fra enhver FFI-vært, her vist i Pascal, fordi det læses rent:

var
  Inst, Doc: Integer;
begin
  Inst := DLCreateLibrary;                       // én instans per arbejdstråd
  try
    Doc := DLLoadFromFile(Inst, 'in.pdf', '');   // returnerer et DocumentID, 0 ved fejl
    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);                      // frigiver hvert dokument, instansen ejer
  end;
end;

To ting følger af det ejerskabstræ. DLReleaseLibrary er det eneste oprydningskald, du strengt taget har brug for, eftersom det river hvert dokument- og proces-handle under instansen ned på én gang. I et kort script er det nok. I en langtidskørende tjeneste bliver det en langsom lækage med ekstra ceremoni, så frigiv dokumenter, efterhånden som du bliver færdig med dem, frem for at lade dem hobe sig op, indtil instansen dør. Instansen er også den naturlige enhed for trådisolering. Giv hver arbejdstråd sit eget InstanceID, og del aldrig ét på tværs af tråde uden ekstern låsning, af samme grund som du aldrig ville dele et enkelt TPDFlib-objekt mellem tråde

Returnerede strenge er lånte, ikke ejede

Funktioner, der returnerer tekst, som DLGetPageText, rækker en PWideChar eller PAnsiChar tilbage, der peger ind i en buffer, biblioteksinstansen ejer og genbruger. Kontrakten lyder: kopiér med det samme, frigiv aldrig

PDF Library for Delphi-tidslinje, der kontrasterer straks kopiering af en udlånt DLGetPageText-pointer med at holde den, indtil biblioteket genbruger den underliggende buffer
Returnerede char-pointere låner lager, som instansen genbruger, så kopieringen skal ske før det næste bibliotekskald
var
  P: PWideChar;
  PageText: string;
begin
  P := DLGetPageText(Inst, 7);   // pointer ind i en buffer, biblioteket ejer
  PageText := P;                 // kopiér nu; et senere kald kan genbruge bufferen
end;

I C# betyder det at marshalle den IntPtr til en managed streng inden næste bibliotekskald. I Pythons ctypes betyder det at skære wide-strengen ud af pointeren med det samme. Holder du fast i den rå pointer på tværs af kald, har du skrevet en fejl, der består enhver unit test og så fejler første gang to forespørgsler overlapper i produktion, fordi det andet kald genbrugte den buffer, det første stadig læste fra. Den samme ejerskabsregel gælder den anden vej for callbacks registreret gennem DLSetProgressCallback. Enhver pointer, biblioteket rækker ind i dit callback, er kun gyldig i selve callback-kroppen, og callback-objektet skal selv holdes i live (pinned, i en vært med garbage collection), så længe instansen stadig kan finde på at kalde det. En delegate, der bliver ryddet op midt i et job, er lærebogseksemplet på den "tilfældige" access violation, som dukker op i en .NET-binding, der har kørt rent i månedsvis

Byg en smoke test ind i selve bindingen, og kør den, før et genereret sæt deklarationer bliver sendt af sted. Afprøv ét kald fra hver kategori, der plejer at afsløre ABI-fejl: en funktion uden parametre som DLCreateLibrary for at bevise, at konventionen er rigtig, en funktion med streng ind fodret med en sti med ikke-ASCII-tegn for at bevise, at kodningen er rigtig, en funktion med streng ud for at bevise, at håndteringen af den lånte buffer er rigtig, og én operation, der fejler med vilje, så du kan se, hvordan en fejl når frem til din vært. Det er et kvarters arbejde, og det fanger de fejl i kaldekonvention og kodning, der ellers ville ankomme måneder senere som et crash dump fra en kunde

PDF Library for Delphi to gange to-gitter af bindings-smoketestprøver, der dækker calling convention, strengkodning, udlånte buffere og fejlsynliggørelse
Fire billige prøver fanger konventions-, kodnings- og ejerskabsfejl, før genererede deklarationer nogensinde når en kundemaskine

Python ctypes-tilfældet, helt konkret

Pythons ctypes er den binding, jeg oftest ser håndlavet, og den gør det nemt at demonstrere opdelingen på tværs af platforme. På Windows indlæser du biblioteket med ctypes.WinDLL, så ctypes anvender Stdcall, binder de W-funktioner uden suffiks og erklærer hver strengparameter som c_wchar_p. På macOS indlæser du det med ctypes.CDLL for Cdecl, beholder den identiske funktionsliste og slår navnene op uden den indledende understregning. De fleste FFI-lag, ctypes inklusive, folder understregningskonventionen ind for dig på macOS, men det er netop den ene antagelse, du skal bekræfte med et enkelt opslået kald, før du genererer hundredvis af deklarationer oven på den

To udrulningsspørgsmål følger i hælene på bindingsarbejdet, og de har klare svar. Den almindelige DLL kræver ingen registrering: regsvr32 gælder kun ActiveX-builden, og DLL'en leveres ved filkopiering, hvilket er hovedgrunden til at foretrække den til Windows-tjenester og containere, hvor du helst slet ikke vil røre registreringsdatabasen. Trådsikkerhed reduceres til den regel, der allerede er i spil ovenfor, én instans per tråd. Instans-handlet rummer hver eneste bid foranderlig tilstand, motoren holder styr på, det valgte dokument, renderingsindstillingerne, udtræksindstillingerne, så to tråde, der deler en instans, fletter hinandens tilstand sammen, selv når hvert enkelt kald returnerer succes

Når først en binding er solid, er operationerne på den anden side af den præcis dem, Delphi-artiklerne dækker i dybden, blandt andet at anvende og revidere PDF-kryptering og at udtrække tekst og billeder fra eksisterende dokumenter

Binære downloads til alle tre integrationslag følger med biblioteket; se PDF Library for Delphi-produktsiden for udgaver og licensering