Technisch artikel

PDF Library for Delphi: DLL, ActiveX, and Dylib integration in Delphi

Dit probleem duikt op zodra een PDF-bibliotheek haar thuistaal verlaat. U hebt een binding die perfect werkt vanuit C# op Windows. U hebt dezelfde aanroepen nodig vanuit Python op macOS, dus u kopieert het Windows-declaratiebestand, vervangt de binaire naam en voert het uit. Elk symbool wordt gevonden. De eerste aanroep geeft onzin terug, de tweede crasht met een toegangsfout en geen enkele regel van uw PDF-code is gewijzigd. De fout zit een laag onder de PDF: de Windows-exports gebruiken de Stdcall-conventie, de macOS-dylib exporteert dezelfde functies als Cdecl met een voorloopunderscore, en een foreign-function-declaratie die een van beide details verkeerd heeft, beschadigt de stack voordat ook maar één document wordt geopend

Deze hele klasse fouten komt voort uit één ontwerpbeslissing die u beter vooraf begrijpt. PDF Library for Delphi, de PDF-engine met broncodebeschikbaarheid van losLab voor Delphi en C++Builder, verpakt zijn volledige objectmodel in één platte façadeklasse, TPDFlib, en levert die façade vervolgens in drie binaire vormen: een Windows-DLL met ongeveer 1.250 geëxporteerde functies, een COM/ActiveX-automatiseringsobject en een macOS-dylib. De PDF-semantiek is voor alle drie identiek. Het deel dat problemen veroorzaakt, bevindt zich in de onderliggende ABI: aanroepconventies, tekenreeksencoderingen, eigenaarschap van handles en welke kant welke buffer mag vrijgeven

Eén façade, drie binaire vormen

Elke publieke functie van TPDFlib heeft een platte tegenhanger met de naam DL plus de methodenaam. LoadFromFile wordt DLLoadFromFile, Encrypt wordt DLEncrypt, NewSignProcessFromFile wordt DLNewSignProcessFromFile. De eerste parameter van bijna elke export is een InstanceID die DLCreateLibrary retourneert, als vervanging voor de objectreferentie die een Delphi-aanroeper anders zou vasthouden. Maak die toewijzing vroeg eigen. Dat betekent dat de Delphi-API-referentie tegelijk de documentatie is voor elke andere taal: alles wat de klasse kan doen, kan de DLL onder een voorspelbare naam doen, en u kunt een Pascal-methodesignatuur lezen om de aanroep te leren die u nodig hebt vanuit Python of C#

De Windows-build levert PDFlibDLL32.dll en PDFlibDLL64.dll; kies de versie die overeenkomt met de bitness van uw hostproces, want een 64-bits Java- of .NET-proces kan de 32-bits bibliotheek niet laden, ongeacht hoe de declaratie eruitziet

Architectuurdiagram van één TPDFlib-facade die beschikbaar wordt gesteld als Stdcall Windows-DLL, Safecall ActiveX-automatiseringsobject en Cdecl macOS-dylib
Alle drie de binaries delen één platte PDF-façade maar verschillen in aanroepconventie, tekenafhandeling en registratievereisten

Windows: Stdcall-instanties en de W/A-functieparen

Elke export die tekenreeksen aanneemt, bestaat tweemaal. Een brede versie neemt PWideChar (UTF-16, de natuurlijke keuze voor .NET, Java en Python's c_wchar_p), en een versie met het achtervoegsel A neemt PAnsiChar. Beide hebben identieke semantiek en verschillen alleen in codering, precies wat het mengen ervan zo lastig maakt om te achterhalen: niets werpt een fout, niets retourneert een foutcode, u krijgt eenvoudig mojibake in metadata of een onterechte "bestand niet gevonden" voor elk pad met een teken buiten gewone ASCII. De eerste coderingsfout die een team op deze manier tegenkomt kost meestal een middag, omdat het symptoom naar de gegevens wijst en de oorzaak in de declaratie zit

// Windows binding (PDFlibDLL64.dll): Stdcall, kale exportnamen
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: dezelfde functie, Cdecl, en een underscore vóór de export
function DLCreateLibrary: Integer; cdecl;
  external 'PDFlibDylib.dylib' name '_DLCreateLibrary';

Kies één tekenbreedte per host en leg die vast in de bindingsgenerator. Een praktische regel: als de hosttaal eigen UTF-16-tekenreeksen heeft, bind dan overal de W-versies en raak de A-familie nooit meer aan

macOS: dezelfde namen, andere ABI

De dylib exporteert dezelfde DL-functieset met twee systematische wijzigingen. De aanroepconventie is Cdecl in plaats van Stdcall en elke exportnaam heeft een voorloopunderscore (_DLCreateLibrary, _DLLoadFromFile enzovoort). Beide wijzigingen zijn zuiver mechanisch, wat ze ideaal maakt voor een gegenereerde binding en gevaarlijk voor een handmatig bewerkte kopie van het Windows-bestand. Houd één canonieke functielijst aan en genereer daaruit per-platform-declaraties als uw tooling dat toelaat. Slaat u dat over, dan krijgt u exact de stackbeschadiging die bovenaan deze pagina is beschreven, en die alleen reproduceert op het platform dat uw CI toevallig het minst test

COM- en ActiveX-hosts: Safecall en Olevariant-payloads

Voor VB.NET, C#, VBScript en oudere automatiseringshosts verpakt de OCX-build dezelfde façade in een IDispatch-automatiseringsobject, IPDFlibrary, met elke methode gedeclareerd als Safecall. Die conventie verandert hoe fouten u bereiken. Safecall vertaalt een interne fout naar een COM-HRESULT, zodat een C#-aanroeper een uitzondering opvangt waar de platte DLL een stil geheel getal had teruggegeven dat de aanroeper moest onthouden te controleren. Dezelfde bewerking, twee foutidiomen, afhankelijk van welk binair bestand u hebt geladen

Binaire gegevens volgen een tweede COM-specifieke regel. De automatiseringsinterface heeft helemaal geen pointerparameters. Alles wat binair is, afbeeldingsbytes die binnenkomen of PDF-bytes die naar buiten gaan, passeert de grens als een Olevariant via methoden zoals AddImageFromVariant en AppendToVariant. Een bytearray in een variant marshalen is één regel in .NET. Geeft u in plaats daarvan een ruwe pointer door, omdat het toch hetzelfde proces is, dan verwerpt of verminkt de dispatchlaag de aanroep. Nog een registratiedetail veroorzaakt problemen bij implementaties: COM-registratie is per bitness, dus een OCX die met de 32-bits regsvr32 is geregistreerd, is onzichtbaar voor een 64-bits host. Die mismatch verschijnt als de berucht weinig behulpzame melding "class not registered" op de machine van de klant, lang nadat die van u is verlaten

Handle-discipline: instanties bezitten documenten

De platte API werkt met gehele handles. DLCreateLibrary retourneert een instantie. Het laden van een bestand retourneert een document-ID binnen die instantie. Ondertekeningsprocessen, tekenreekslijsten en direct-access-bestanden retourneren elk hun eigen gehele handles, allemaal binnen dezelfde instantie. De levenscyclus ziet er vanuit elke FFI-host hetzelfde uit, hier getoond in Pascal omdat dat helder leest:

var
  Inst, Doc: Integer;
begin
  Inst := DLCreateLibrary;                       // één 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);                      // geeft elk document vrij dat de instance in beheer heeft
  end;
end;

Uit deze eigendomsboom volgen twee zaken. DLReleaseLibrary is de enige opruimaanroep die u strikt nodig hebt, omdat deze in één keer elk document en elke proceshandle onder de instantie opruimt. In een kort script is dat voldoende. In een langlopende service wordt het een langzaam lek met extra ceremonie, dus geef documenten vrij zodra u ermee klaar bent in plaats van ze op te laten stapelen tot de instantie sterft. De instantie is ook de natuurlijke eenheid voor threadisolatie. Geef elke workerthread zijn eigen InstanceID, en deel er nooit één tussen threads zonder externe vergrendeling, om dezelfde reden waarom u nooit één TPDFlib-object tussen threads zou delen

Geretourneerde tekenreeksen zijn geleend, niet in eigendom

Functies die tekst retourneren, zoals DLGetPageText, geven een PWideChar of PAnsiChar terug die wijst naar een buffer die eigendom is van en wordt hergebruikt door de bibliotheekinstantie. Het contract luidt: onmiddellijk kopiëren, nooit vrijgeven

Tijdlijn van PDF Library for Delphi die het onmiddellijk kopiëren van een geleende DLGetPageText-pointer afzet tegen het vasthouden ervan tot de bibliotheek de onderliggende buffer recyclet
Teruggegeven char-pointers lenen opslag die de instantie recyclet, dus de kopie moet plaatsvinden vóór de volgende bibliotheekaanroep
var
  P: PWideChar;
  PageText: string;
begin
  P := DLGetPageText(Inst, 7);   // pointer into a library-owned buffer
  PageText := P;                 // nu kopiëren; een latere call kan de buffer hergebruiken
end;

In C# betekent dat dat u de IntPtr naar een beheerde tekenreeks moet marshalen vóór de volgende bibliotheekaanroep. In Python ctypes betekent het dat u de brede tekenreeks direct uit de pointer moet halen. Houdt u de ruwe pointer tussen aanroepen vast, dan hebt u een fout geschreven die elke unit-test doorstaat en vervolgens faalt zodra twee verzoeken in productie overlappen, omdat de tweede aanroep de buffer hergebruikte die de eerste nog las. Dezelfde eigendomsregel geldt in de andere richting voor callbacks die via DLSetProgressCallback zijn geregistreerd. Elke pointer die de bibliotheek aan uw callback geeft, is alleen geldig binnen de body van die callback, en het callbackobject zelf moet in leven blijven (vastgepind in een host met garbage collection) zolang de instantie het nog kan aanroepen. Een delegate die midden in een taak wordt verzameld, is de klassieke oorzaak van de "willekeurige" toegangsfout die verschijnt in een .NET-binding die maandenlang probleemloos heeft gewerkt

Bouw een smoketest in de binding zelf in en voer die uit voordat een gegenereerde declaratieset wordt uitgebracht. Test één aanroep uit elke categorie die ABI-fouten doorgaans blootlegt: een parameterloze functie zoals DLCreateLibrary om te bewijzen dat de conventie juist is, een tekenreeksinvoerfunctie met een pad met niet-ASCII-tekens om te bewijzen dat de codering juist is, een tekenreeksuitvoerfunctie om te bewijzen dat de geleende-bufferafhandeling juist is, en één bewerking die expres mislukt zodat u ziet hoe een fout uw host bereikt. Dat kost vijftien minuten werk en vangt de fouten in aanroepconventie en codering die anders maanden later als een crashdump van een klant zouden arriveren

PDF Library for Delphi: raster van twee bij twee met rooktestprobes voor bindingen, dekkend voor aanroepconventie, stringcodering, geleende buffers en foutsignalering
Vier goedkope probes vangen conventie-, encoding- en eigendomsfouten op voordat gegenereerde declaraties ooit een klantmachine bereiken

Het concrete geval van Python ctypes

Python ctypes is de binding die ik het vaakst handmatig zie schrijven, en hij maakt de platformopsplitsing eenvoudig te demonstreren. Laad op Windows de bibliotheek met ctypes.WinDLL zodat ctypes Stdcall toepast, bind de niet-gesuffixeerde W-functies en declareer elke tekenreeksparameter als c_wchar_p. Laad deze op macOS met ctypes.CDLL voor Cdecl, behoud de identieke functielijst en los de namen op zonder de voorloopunderscore. De meeste FFI-lagen, ctypes inbegrepen, voegen de underscoreconventie op macOS weer voor u toe, maar dat is de ene aanname die u met één opgeloste aanroep moet bevestigen voordat u daarbovenop honderden declaraties genereert

Twee implementatievragen volgen op het bindingswerk en hebben duidelijke antwoorden. De gewone DLL heeft geen registratie nodig: regsvr32 geldt alleen voor de ActiveX-build, en de DLL wordt door bestandskopie geleverd, wat de belangrijkste reden is om deze te verkiezen voor Windows-services en containers waarin u de registry liever helemaal niet aanraakt. Threadveiligheid komt neer op de hierboven al geldende regel: één instantie per thread. De instantiehandle bevat elk stuk wijzigbare staat dat de engine bijhoudt, het geselecteerde document, de renderopties, de extractie-instellingen, zodat twee threads die een instantie delen elkaars staat vervlechten, zelfs wanneer elke afzonderlijke aanroep succes retourneert

Zodra een binding degelijk is, zijn de bewerkingen aan de andere kant ervan precies de bewerkingen die de Delphi-artikelen diepgaand behandelen, waaronder PDF-versleuteling toepassen en auditen en tekst en afbeeldingen uit bestaande documenten extraheren

Binaire downloads voor alle drie de integratielagen worden met de bibliotheek geleverd; zie de PDF Library for Delphi-productpagina voor edities en licenties