Her er et problem som dukker opp i det øyeblikket et PDF-bibliotek forlater hjemmespråket sitt. Du har en binding som fungerer perfekt fra C# på Windows. Du trenger de samme kallene fra Python på macOS, så du kopierer deklarasjonsfilen fra Windows, bytter ut binærnavnet og kjører. Hvert symbol lar seg slå opp. Det første kallet returnerer søppel, det andre krasjer med en tilgangsfeil, og ingenting i PDF-koden din er endret. Feilen ligger ett lag under PDF-en: eksportene på Windows bruker Stdcall-konvensjonen, macOS-dylib-en eksporterer de samme funksjonene som Cdecl med et innledende understrek, og en deklarasjon for fremmedfunksjoner som bommer på én av de to detaljene, ødelegger stakken før et eneste dokument er åpnet
Hele denne feilklassen springer ut av én designbeslutning det lønner seg å forstå med én gang. PDF Library for Delphi, losLabs PDF-motor med tilgjengelig kildekode for Delphi og C++Builder, pakker hele objektmodellen sin inn i én flat fasadeklasse, TPDFlib, og leverer så den fasaden i tre binære former: en Windows-DLL med rundt 1250 eksporterte funksjoner, et COM/ActiveX-automasjonsobjekt og en macOS-dylib. PDF-semantikken er identisk i alle tre. Den delen som biter deg, bor i ABI-en under: kallekonvensjoner, strengkodinger, eierskap til håndtak og hvilken side som har lov til å frigjøre hvilken buffer
Én fasade, tre binære former
Hver offentlige funksjon i TPDFlib har en flat motpart som heter DL pluss metodenavnet. LoadFromFile blir DLLoadFromFile, Encrypt blir DLEncrypt, NewSignProcessFromFile blir DLNewSignProcessFromFile. Den første parameteren i nesten hver eksport er en InstanceID returnert av DLCreateLibrary, som står i stedet for objektreferansen en Delphi-kaller ellers ville holdt. Lær den avbildningen tidlig. Den betyr at Delphi-API-referansen samtidig er dokumentasjonen for alle andre språk: alt klassen kan gjøre, kan DLL-en gjøre under et forutsigbart navn, og du kan lese en Pascal-metodesignatur for å finne ut hvilket kall du trenger fra Python eller C#
Windows-bygget produserer PDFlibDLL32.dll og PDFlibDLL64.dll; velg den som svarer til bitbredden i vertsprosessen din, siden en 64-bits Java- eller .NET-prosess ikke kan laste 32-bitersbiblioteket uansett hvordan deklarasjonen ser ut
Windows: Stdcall-instanser og W/A-funksjonsparene
Hver eksport som tar en streng, finnes to ganger. En bred versjon tar PWideChar (UTF-16, det naturlige valget for .NET, Java og Pythons c_wchar_p), og en versjon med A-suffiks tar PAnsiChar. De to bærer identisk semantikk og skiller seg bare i koding, og det er nettopp det som gjør en sammenblanding så vond å spore opp: ingenting kaster, ingenting returnerer en feilkode, du får rett og slett mojibake i metadata eller en falsk «file not found» for enhver sti med et tegn utenfor ren ASCII. Den første kodingsfeilen et team treffer på denne måten, koster gjerne en ettermiddag, fordi symptomet peker på dataene mens årsaken ligger i deklarasjonen
// Windows-binding (PDFlibDLL64.dll): Stdcall, rene eksportnavn
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 funksjon, Cdecl, og et innledende understrek på eksportnavnet
function DLCreateLibrary: Integer; cdecl;
external 'PDFlibDylib.dylib' name '_DLCreateLibrary';
Velg én tegnbredde per vert, og fest den i bindingsgeneratoren. En praktisk regel: har vertsspråket UTF-16-strenger innebygd, bind W-versjonene overalt og rør aldri A-familien igjen
macOS: samme navn, annen ABI
Dylib-en eksporterer det samme settet med DL-funksjoner, med to systematiske endringer. Kallekonvensjonen er Cdecl framfor Stdcall, og hvert eksportnavn bærer et innledende understrek (_DLCreateLibrary, _DLLoadFromFile og så videre). Begge endringene er rent mekaniske, noe som gjør dem ideelle for en generert binding og farlige for en håndredigert kopi av Windows-filen. Hold én kanonisk funksjonsliste og produser plattformspesifikke deklarasjoner fra den hvis verktøykjeden din tillater det. Dropper du det, får du nøyaktig den stakkorrupsjonen som er beskrevet øverst på denne siden, og den reproduseres bare på den plattformen CI-en din tilfeldigvis kjører minst
COM- og ActiveX-verter: Safecall og Olevariant-nyttelast
For VB.NET, C#, VBScript og eldre automasjonsverter pakker OCX-bygget den samme fasaden inn i et IDispatch-automasjonsobjekt, IPDFlibrary, med hver metode deklarert Safecall. Den konvensjonen endrer hvordan feil når fram til deg. Safecall oversetter en intern feil til en COM-HRESULT, så en C#-kaller fanger et unntak der den flate DLL-en ville returnert et stille heltall kalleren måtte huske å sjekke. Samme operasjon, to feilidiomer, avhengig av hvilken binær du lastet
Binærdata følger en annen COM-spesifikk regel. Automasjonsgrensesnittet har ingen pekerparametre i det hele tatt. Alt binært, bildebyte inn eller PDF-byte ut, krysser grensen som en Olevariant gjennom metoder som AddImageFromVariant og AppendToVariant. Å marshalere et bytearray inn i en variant er én linje i .NET. Prøver du i stedet å gi den en rå peker, med den begrunnelsen at det uansett er samme prosess, avviser eller forvrenger dispatch-laget kallet. Én registreringsdetalj til feller utrullinger: COM-registrering skjer per bitbredde, så en OCX registrert med 32-bits regsvr32 er usynlig for en 64-bits vert. Det misforholdet dukker opp som den berømt lite hjelpsomme «class not registered» på kundemaskinen, lenge etter at den forlot din
Håndtaksdisiplin: instanser eier dokumenter
Det flate API-et kjører på heltallshåndtak. DLCreateLibrary returnerer en instans. Å laste en fil returnerer en dokument-ID inne i den instansen. Signeringsprosesser, strenglister og filer med direkte tilgang returnerer hver sine egne heltallshåndtak, alle med samme instans som virkefelt. Livssyklusen ser lik ut fra enhver FFI-vert, her vist i Pascal fordi det leser rent:
var
Inst, Doc: Integer;
begin
Inst := DLCreateLibrary; // én instans per arbeidertråd
try
Doc := DLLoadFromFile(Inst, 'in.pdf', ''); // returnerer en DocumentID, 0 ved feil
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); // frigjør hvert dokument instansen eier
end;
end;
To ting følger av det eierskapstreet. DLReleaseLibrary er det eneste oppryddingskallet du strengt tatt trenger, siden det river ned hvert dokument- og prosesshåndtak under instansen i én operasjon. I et kort skript er det nok. I en tjeneste som lever lenge, blir det en langsom lekkasje med ekstra seremoni, så frigjør dokumenter etter hvert som du blir ferdig med dem, framfor å la dem hope seg opp til instansen dør. Instansen er også den naturlige enheten for trådisolasjon. Gi hver arbeidertråd sin egen InstanceID, og del aldri én på tvers av tråder uten ekstern låsing, av samme grunn som du aldri ville delt ett enkelt TPDFlib-objekt mellom tråder
Returnerte strenger er lånt, ikke eid
Funksjoner som returnerer tekst, slik som DLGetPageText, gir tilbake en PWideChar eller PAnsiChar som peker inn i en buffer biblioteksinstansen eier og gjenbruker. Kontrakten er: kopier umiddelbart, frigjør aldri
var
P: PWideChar;
PageText: string;
begin
P := DLGetPageText(Inst, 7); // peker inn i en buffer biblioteket eier
PageText := P; // kopier nå; et senere kall kan gjenbruke bufferet
end;
I C# betyr det å marshalere IntPtr-en til en administrert streng før neste bibliotekskall. I Pythons ctypes betyr det å skjære den brede strengen ut av pekeren med én gang. Holder du på den rå pekeren mellom kall, har du skrevet en feil som består hver enhetstest og deretter svikter første gang to forespørsler overlapper i produksjon, fordi det andre kallet gjenbrukte bufferet det første fortsatt leste fra. Den samme eierskapsregelen gjelder motsatt vei for tilbakekall registrert gjennom DLSetProgressCallback. Enhver peker biblioteket gir inn i tilbakekallet ditt, er bare gyldig i kroppen av det tilbakekallet, og selve tilbakekallsobjektet må holdes i live (festet, i en vert med søppelsamling) så lenge instansen fremdeles kan tenkes å kalle det. En delegat som blir samlet inn midt i en jobb, er den klassiske kilden til den «tilfeldige» tilgangsfeilen som dukker opp i en .NET-binding som har gått rent i månedsvis
Bygg en røyktest inn i selve bindingen, og kjør den før et generert deklarasjonssett i det hele tatt leveres. Prøv ett kall fra hver kategori som pleier å avsløre ABI-tabber: en funksjon uten parametre som DLCreateLibrary for å bevise at konvensjonen stemmer, en funksjon med streng inn matet med en sti med tegn utenfor ASCII for å bevise at kodingen stemmer, en funksjon med streng ut for å bevise at håndteringen av den lånte bufferen stemmer, og én operasjon som feiler med vilje så du kan se hvordan en feil når fram til verten din. Det er femten minutters arbeid, og det fanger kallekonvensjons- og kodingsfeilene som ellers ville kommet måneder senere som en krasjdump fra en kunde
Tilfellet Python ctypes, konkret
Pythons ctypes er den bindingen jeg oftest ser håndlaget, og den gjør det lett å demonstrere splittelsen på tvers av plattformer. På Windows laster du biblioteket med ctypes.WinDLL slik at ctypes bruker Stdcall, binder de usuffikserte W-funksjonene og deklarerer hver strengparameter som c_wchar_p. På macOS laster du det med ctypes.CDLL for Cdecl, beholder den identiske funksjonslisten og slår opp navnene uten det innledende understreket. De fleste FFI-lag, ctypes inkludert, føyer understrek-konvensjonen inn for deg på macOS, men det er nettopp den antakelsen du bør bekrefte med ett enkelt oppslått kall før du genererer hundrevis av deklarasjoner oppå den
To utrullingsspørsmål følger i kjølvannet av bindingsarbeidet, og de har klare svar. Den rene DLL-en trenger ingen registrering: regsvr32 gjelder bare ActiveX-bygget, og DLL-en leveres ved filkopi, som er hovedgrunnen til å foretrekke den for Windows-tjenester og containere der du helst ikke vil røre registeret i det hele tatt. Trådsikkerhet koker ned til regelen som allerede er i spill ovenfor, én instans per tråd. Instanshåndtaket holder hver bit av foranderlig tilstand motoren sporer, det valgte dokumentet, rendringsvalgene, uttrekksinnstillingene, så to tråder som deler en instans, flettes inn i hverandres tilstand selv når hvert enkelt kall returnerer suksess
Når en binding først er solid, er operasjonene på den andre siden av den nøyaktig de samme som Delphi-artiklene dekker i dybden, inkludert å anvende og granske PDF-kryptering og å trekke ut tekst og bilder fra eksisterende dokumenter
Binærnedlastinger for alle tre integrasjonslagene følger med biblioteket; se produktsiden for PDF Library for Delphi for utgaver og lisensiering