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