Here is a problem that shows up the moment a PDF library leaves its home language. You have a binding that works perfectly from C# on Windows. You need the same calls from Python on macOS, so you copy the Windows declaration file, swap the binary name, and run it. Every symbol resolves. The first call returns garbage, the second crashes with an access violation, and none of your PDF code has changed. The fault is one layer below the PDF: the Windows exports use the Stdcall convention, the macOS dylib exports the same functions as Cdecl with a leading underscore, and a foreign-function declaration that gets either detail wrong corrupts the stack before a single document is opened.
That whole class of failure comes from one design decision worth understanding up front. PDFlibPas, losLab's source-available PDF engine for Delphi and C++Builder, wraps its entire object model in one flat facade class, TPDFlib, then ships that facade in three binary shapes: a Windows DLL with roughly 1,250 exported functions, a COM/ActiveX automation object, and a macOS dylib. The PDF semantics are identical across all three. The part that bites you lives in the ABI underneath: calling conventions, string encodings, handle ownership, and which side is allowed to free which buffer.
One facade, three binary shapes
Every public function of TPDFlib has a flat counterpart named DL plus the method name. LoadFromFile becomes DLLoadFromFile, Encrypt becomes DLEncrypt, NewSignProcessFromFile becomes DLNewSignProcessFromFile. The first parameter of nearly every export is an InstanceID returned by DLCreateLibrary, standing in for the object reference a Delphi caller would otherwise hold. Internalize that mapping early. It means the Delphi API reference doubles as the documentation for every other language: whatever the class can do, the DLL can do under a predictable name, and you can read a Pascal method signature to learn the call you need from Python or C#.
The Windows build produces PDFlibDLL32.dll and PDFlibDLL64.dll; pick the one matching your host process bitness, since a 64-bit Java or .NET process cannot load the 32-bit library no matter how the declaration looks.
Windows: Stdcall instances and the W/A function pairs
Each string-taking export exists twice. A wide version takes PWideChar (UTF-16, the natural fit for .NET, Java, and Python's c_wchar_p), and an A-suffixed version takes PAnsiChar. The two carry identical semantics and differ only in encoding, which is exactly what makes mixing them so painful to track down: nothing throws, nothing returns an error code, you simply get mojibake in metadata or a spurious "file not found" for any path with a character past plain ASCII. The first encoding bug a team hits this way usually costs an afternoon, because the symptom points at the data and the cause is in the declaration.
// 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';
Pick one character width per host and codify it in the binding generator. A practical rule: if the host language has native UTF-16 strings, bind the W versions everywhere and never touch the A family again.
macOS: same names, different ABI
The dylib exports the same DL function set with two systematic changes. The calling convention is Cdecl rather than Stdcall, and every export name carries a leading underscore (_DLCreateLibrary, _DLLoadFromFile, and so on). Both changes are purely mechanical, which makes them ideal for a generated binding and dangerous for a hand-edited copy of the Windows file. Keep one canonical function list and emit per-platform declarations from it if your tooling allows. Skip that and you get the exact stack corruption described at the top of this page, reproducing only on the platform your CI happens to exercise least.
COM and ActiveX hosts: Safecall and Olevariant payloads
For VB.NET, C#, VBScript, and legacy automation hosts, the OCX build wraps the same facade in an IDispatch automation object, IPDFlibrary, with every method declared Safecall. That convention changes how errors reach you. Safecall translates an internal failure into a COM HRESULT, so a C# caller catches an exception where the flat DLL would have returned a quiet integer the caller had to remember to check. The same operation, two failure idioms, depending on which binary you loaded.
Binary data follows a second COM-specific rule. The automation interface has no pointer parameters at all. Anything binary, image bytes going in or PDF bytes coming out, crosses the boundary as an Olevariant through methods such as AddImageFromVariant and AppendToVariant. Marshaling a byte array into a variant is a single line in .NET. Try to hand it a raw pointer instead, on the reasoning that it is the same process anyway, and the dispatch layer rejects or mangles the call. One more registration detail trips up deployments: COM registration is per-bitness, so an OCX registered with the 32-bit regsvr32 is invisible to a 64-bit host. That mismatch surfaces as the famously unhelpful "class not registered" on the customer machine, long after it left yours.
Handle discipline: instances own documents
The flat API runs on integer handles. DLCreateLibrary returns an instance. Loading a file returns a document ID inside that instance. Sign processes, string lists, and direct-access files each return their own integer handles, all scoped to the same instance. The lifecycle looks the same from any FFI host, shown here in Pascal because it reads cleanly:
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;
Two things follow from that ownership tree. DLReleaseLibrary is the only cleanup call you strictly need, since it tears down every document and process handle under the instance in one shot. In a short script that is enough. In a long-running service it becomes a slow leak with extra ceremony, so release documents as you finish with them rather than letting them pile up until the instance dies. The instance is also the natural unit of thread isolation. Give each worker thread its own InstanceID, and never share one across threads without external locking, for the same reason you would never share a single TPDFlib object between threads.
Returned strings are borrowed, not owned
Functions that return text, such as DLGetPageText, hand back a PWideChar or PAnsiChar that points into a buffer owned and recycled by the library instance. The contract is: copy immediately, never free.
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# that means marshaling the IntPtr into a managed string before the next library call. In Python ctypes, it means slicing the wide string out of the pointer right away. Hold the raw pointer across calls and you have written a bug that passes every unit test and then fails the first time two requests overlap in production, because the second call recycled the buffer the first one was still reading. The same ownership rule runs in the other direction for callbacks registered through DLSetProgressCallback. Any pointer the library hands into your callback is valid only for the body of that callback, and the callback object itself has to stay alive (pinned, in a garbage-collected host) for as long as the instance might still invoke it. A delegate collected mid-job is the textbook source of the "random" access violation that appears in a .NET binding which ran clean for months.
Build a smoke test into the binding itself, and run it before any generated declaration set ships. Exercise one call from each category that tends to expose ABI mistakes: a parameterless function such as DLCreateLibrary to prove the convention is right, a string-in function fed a path with non-ASCII characters to prove the encoding is right, a string-out function to prove the borrowed-buffer handling is right, and one operation that fails on purpose so you can watch how an error reaches your host. That is fifteen minutes of work, and it catches the calling-convention and encoding faults that would otherwise arrive months later as a customer crash dump.
The Python ctypes case, concretely
Python ctypes is the binding I see hand-rolled most often, and it makes the cross-platform split easy to demonstrate. On Windows, load the library with ctypes.WinDLL so ctypes applies Stdcall, bind the unsuffixed W functions, and declare every string parameter as c_wchar_p. On macOS, load it with ctypes.CDLL for Cdecl, keep the identical function list, and resolve the names without the leading underscore. Most FFI layers, ctypes included, fold the underscore convention back in for you on macOS, but that is the one assumption to confirm with a single resolved call before you generate hundreds of declarations on top of it.
Two deployment questions trail the binding work and have crisp answers. The plain DLL needs no registration: regsvr32 applies only to the ActiveX build, and the DLL ships by file copy, which is the main reason to prefer it for Windows services and containers where you would rather not touch the registry at all. Thread safety reduces to the rule already in play above, one instance per thread. The instance handle holds every piece of mutable state the engine tracks, the selected document, the render options, the extraction settings, so two threads sharing an instance interleave each other's state even when each individual call returns success.
Once a binding is solid, the operations on the far side of it are exactly the ones the Delphi articles cover in depth, including applying and auditing PDF encryption and extracting text and images from existing documents.
Binary downloads for all three integration layers ship with the library; see the PDFlibPas product page for editions and licensing.