Teknisk artikkel

PDF Library for Delphi DLL-, ActiveX- og dylib-bindinger: Kalle én PDF-motor fra alle språk

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

Arkitekturdiagram over én TPDFlib-fasade eksponert som en Stdcall Windows-DLL, et Safecall ActiveX-automatiseringsobjekt og en Cdecl macOS-dylib
Alle tre binærfilene deler én flat PDF-fasade, men varierer i kallkonvensjon, streng-håndtering og registreringskrav

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

PDF Library for Delphi-tidslinje som setter umiddelbar kopiering av en utlånt DLGetPageText-peker opp mot å holde den til biblioteket resirkulerer den underliggende bufferen
Returnerte char-pekere låner lagringsplass som instansen resirkulerer, så kopieringen må skje før neste bibliotekkall
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

PDF Library for Delphi to-på-to-rutenett av bindings-røyktester som dekker kallkonvensjon, strengkoding, utlånte buffere og feilsynlighet
Fire billige prøver fanger feil i kallkonvensjon, koding og eierskap før de genererte deklarasjonene i det hele tatt når en kundemaskin

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