Der Fehlerbericht lautete: „funktioniert aus C# unter Windows, stürzt aus Python auf macOS ab“. Das Team hatte sein macOS-Binding gebaut, indem es die Windows-Deklarationsdatei kopierte und nur den Binärnamen austauschte. Jedes Symbol wurde aufgelöst, der erste Aufruf gab Müll zurück, der zweite stürzte ab. Mit der PDF-Logik war nichts falsch: Die Windows-Exports verwenden die Stdcall-Konvention, während die macOS-dylib dieselben Funktionen als Cdecl mit führendem Unterstrich exportiert. Ein Foreign-Function-Binding, das eines dieser Details ignoriert, korrumpiert den Stack, bevor überhaupt ein Dokument geöffnet ist.
PDFlibPas, losLabs quellverfügbare PDF-Engine für Delphi und C++Builder, kapselt ihr gesamtes Objektmodell in einer flachen Fassadeklasse, TPDFlib, und liefert diese Fassade dann in drei Binärformen aus: als Windows-DLL mit etwa 1.250 exportierten Funktionen, als COM/ActiveX-Automation-Objekt und als macOS-dylib. Die PDF-Semantik ist in allen drei Formen identisch. Was sich unterscheidet — und was dieser Artikel kartiert — ist die ABI: Aufrufkonventionen, String-Codierungen, Handle-Besitz und wer welchen Puffer freigeben darf.
Eine Fassade, drei Binärformen
Jede öffentliche Funktion von TPDFlib besitzt ein flaches Gegenstück mit DL plus Methodennamen: Aus LoadFromFile wird DLLoadFromFile, aus Encrypt wird DLEncrypt, aus NewSignProcessFromFile wird DLNewSignProcessFromFile. Der erste Parameter fast jedes Exports ist eine InstanceID, die DLCreateLibrary zurückgibt und die Objektreferenz vertritt, die ein Delphi-Aufrufer halten würde. Diese Eins-zu-eins-Abbildung lohnt sich früh zu verinnerlichen, denn sie bedeutet: Die Delphi-API-Referenz ist gleichzeitig die Dokumentation für jede andere Sprache — was die Klasse kann, kann die DLL unter einem vorhersagbaren Namen.
Der Windows-Build erzeugt PDFlibDLL32.dll und PDFlibDLL64.dll; wählen Sie die Bibliothek passend zur Bitness Ihres Hostprozesses, denn ein 64-Bit-Java- oder .NET-Prozess kann die 32-Bit-Bibliothek nicht laden, egal wie die Deklaration aussieht.
Windows: Stdcall-Instanzen und die W/A-Funktionspaare
Jeder stringverarbeitende Export existiert doppelt: als Wide-Version mit PWideChar (UTF-16, die natürliche Passform für .NET, Java und Pythons c_wchar_p) und als A-suffigierte Version mit PAnsiChar. Beide sind semantisch identisch und unterscheiden sich nur in der Codierung. Genau deshalb ist das Mischen so schmerzhaft zu debuggen: Nichts fällt hart aus, Sie bekommen nur Mojibake in Metadaten oder „Datei nicht gefunden“ für Pfade mit Zeichen jenseits von ASCII.
// Windows binding (PDFlibDLL64.dll): Stdcall, plain export names
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: same function, Cdecl, and an underscore prefix on the export
function DLCreateLibrary: Integer; cdecl;
external 'PDFlibDylib.dylib' name '_DLCreateLibrary';
Wählen Sie pro Host eine Zeichenbreite und schreiben Sie sie im Binding-Generator fest. Praktische Regel: Wenn die Hostsprache native UTF-16-Strings hat, binden Sie überall die W-Versionen und berühren die A-Familie nie wieder.
macOS: gleiche Namen, andere ABI
Die dylib exportiert dieselbe DL-Funktionsmenge, aber mit zwei systematischen Änderungen: Die Aufrufkonvention ist Cdecl, und jeder Exportname trägt einen führenden Unterstrich — _DLCreateLibrary, _DLLoadFromFile und so weiter. Beide Änderungen sind mechanisch, also perfekte Kandidaten für ein generiertes Binding und miserable Kandidaten für handbearbeitete Kopien der Windows-Datei. Wenn Ihre Binding-Schicht es erlaubt, halten Sie eine kanonische Funktionsliste und erzeugen pro Plattform die Deklarationen. Der Fehlermodus, wenn man das nicht tut, ist die Stack-Korruption aus dem Einstieg dieses Artikels, und sie reproduziert sich nur auf der Plattform, die am seltensten getestet wird.
COM- und ActiveX-Hosts: Safecall und Olevariant-Nutzlasten
Für VB.NET, C#, VBScript und ältere Automation-Hosts kapselt der OCX-Build dieselbe Fassade in einem IDispatch-Automation-Objekt, IPDFlibrary, wobei jede Methode als Safecall deklariert ist. Diese Konventionswahl ist für die Fehlerbehandlung wichtig: Safecall übersetzt interne Fehler in COM-HRESULT-Werte, sodass ein C#-Aufrufer eine fangbare Exception sieht statt eines stillen Fehlercodes — das Gegenteil der flachen DLL, bei der Sie Rückgabewerte selbst prüfen müssen.
Die zweite COM-spezifische Regel betrifft Binärdaten. In der Automation-Schnittstelle gibt es keine Zeigerparameter; alles Binäre — Bildbytes hinein, PDF-Bytes heraus — überschreitet die Grenze als Olevariant, über Methoden wie AddImageFromVariant und AppendToVariant. Ein Bytearray in .NET in eine Variante zu marshallen ist eine Zeile, aber wenn Sie einen rohen Zeiger übergeben, weil „es ja derselbe Prozess ist“, weist oder verstümmelt die Dispatch-Schicht ihn. Denken Sie schließlich daran, dass COM-Registrierung pro Bitness gilt: Eine OCX, die mit dem 32-Bit-regsvr32 registriert wurde, ist für einen 64-Bit-Host unsichtbar. Beim Kunden äußert sich das als das berüchtigt wenig hilfreiche „class not registered“.
Handle-Disziplin: Instanzen besitzen Dokumente
Die flache API ist eine Handle-Ökonomie. DLCreateLibrary gibt eine Instanz zurück; Laden gibt eine Dokument-ID innerhalb dieser Instanz zurück; Signierprozesse, Stringlisten und Direct-Access-Dateien geben weitere Integer-Handles zurück, die derselben Instanz zugeordnet sind. Der kanonische Lebenszyklus sieht aus jedem FFI-Host so aus, hier der Lesbarkeit wegen in Pascal:
var
Inst, Doc: Integer;
begin
Inst := DLCreateLibrary; // one 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); // frees every document the instance owns
end;
end;
Zwei Folgen ergeben sich daraus. Erstens ist DLReleaseLibrary die einzige Bereinigung, die Sie strikt brauchen — sie räumt jedes Dokument- und Prozess-Handle unter der Instanz auf —, aber sich in einem lang laufenden Dienst darauf zu verlassen, ist ein langsames Leck mit Umweg; geben Sie Dokumente frei, wenn Sie mit ihnen fertig sind. Zweitens ist die Instanz die natürliche Einheit der Thread-Isolation: Geben Sie jedem Worker-Thread eine eigene InstanceID und teilen Sie eine Instanz nie ohne externe Sperre zwischen Threads, genau wie Sie kein TPDFlib-Objekt teilen würden.
Zurückgegebene Strings sind geliehen, nicht besessen
Funktionen, die Text zurückgeben, etwa DLGetPageText, liefern einen PWideChar oder PAnsiChar, der in einen Puffer zeigt, den die Bibliotheksinstanz besitzt und wiederverwendet. Der Vertrag lautet: sofort kopieren, nie freigeben.
var
P: PWideChar;
PageText: string;
begin
P := DLGetPageText(Inst, 7); // pointer into a library-owned buffer
PageText := P; // copy now; a later call may reuse the buffer
end;
In C# heißt das, den IntPtr vor dem nächsten Bibliotheksaufruf in einen verwalteten String zu marshallen; in Python ctypes, den Wide-String sofort aus dem Zeiger herauszuschneiden. Den rohen Zeiger über Aufrufe hinweg zu halten, ist die Art Bug, die jede Unit-Test-Suite besteht und unter Produktionsparallelität ausfällt. Dieselbe Besitzregel gilt in Gegenrichtung für Callbacks, die über DLSetProgressCallback registriert werden: Jeder Zeiger, den die Bibliothek an Ihren Callback übergibt, ist nur für die Dauer dieses Callbacks gültig, und der Callback selbst muss so lange lebendig bleiben — in Garbage-Collector-Hosts gepinnt —, wie die Instanz ihn aufrufen könnte. Ein mitten im Job eingesammelter Delegate ist die kanonische Ursache für „zufällige“ Zugriffsverletzungen in .NET-Bindings, die monatelang funktionierten.
Bauen Sie schließlich den Smoke-Test in das Binding selbst ein. Bevor Sie ein generiertes Deklarationsset ausliefern, führen Sie je einen Aufruf durch jede Kategorie aus: eine parameterlose Funktion (DLCreateLibrary), eine String-in-Funktion mit einem Nicht-ASCII-Pfad, eine String-out-Funktion und eine Operation, die absichtlich fehlschlägt, damit Sie sehen, wie Fehlercodes in Ihrem Host sichtbar werden. Fünfzehn Minuten davon fangen Konventions- und Codierungsfehler, die sonst als Crash-Dumps beim Kunden landen.
Binding-Fragen aus dem Support
Welche Funktionen sollte ein Python-ctypes-Binding unter Windows verwenden? Laden Sie die DLL mit WinDLL (Stdcall), binden Sie die unsuffigierten W-Funktionen und deklarieren Sie Stringparameter als c_wchar_p. Auf macOS wechseln Sie zu CDLL, behalten dieselbe Funktionsliste und lösen Namen ohne Unterstrich auf — der Loader auf macOS behandelt die Präfixkonvention in den meisten FFI-Schichten für Sie, aber prüfen Sie es mit einem Aufruf, bevor Sie Hunderte generieren.
Muss ich etwas registrieren, um die einfache DLL zu verwenden? Nein. Die Registrierung mit regsvr32 gilt nur für den ActiveX-Build. Die DLL wird per Dateikopie bereitgestellt, was ein Grund ist, sie für Dienste und containerisierte Windows-Workloads vorzuziehen.
Ist die DLL thread-safe? Das sichere Muster ist eine Instanz pro Thread. Das Instanz-Handle trägt den gesamten veränderlichen Zustand — ausgewähltes Dokument, Render-Optionen, Extraktionseinstellungen —, sodass zwei Threads mit geteilter Instanz Zustandsänderungen still verschränken, selbst wenn ihre Aufrufe erfolgreich sind.
Weiterführende Lektüre
Sobald das Binding steht, sind die Operationen, die es freilegt, dieselben, die die Delphi-Artikel im Detail behandeln — etwa PDF-Verschlüsselung anwenden und auditieren oder Text und Bilder aus bestehenden Dokumenten extrahieren.
Binärdownloads für alle drei Integrationsschichten werden mit der Bibliothek ausgeliefert; Editionen und Lizenzierung finden Sie auf der PDFlibPas-Produktseite.