Il bug report diceva "funziona da C# su Windows, crasha da Python su macOS". Il team aveva costruito il binding macOS copiando il file di dichiarazioni Windows e cambiando il nome del binario. Ogni simbolo si risolveva, la prima chiamata restituiva spazzatura, la seconda crashava. La loro logica PDF non aveva nulla che non andasse: gli export Windows usano la convenzione Stdcall mentre la dylib macOS esporta le stesse funzioni come Cdecl con underscore iniziale, e un binding foreign-function che ignora uno dei due dettagli corrompe lo stack prima ancora che un documento venga aperto.
PDFlibPas, motore PDF losLab con sorgente disponibile per Delphi e C++Builder, avvolge l'intero object model in una facade class flat, TPDFlib, e poi distribuisce quella facade in tre forme binarie: una DLL Windows con circa 1.250 funzioni esportate, un oggetto automation COM/ActiveX e una dylib macOS. La semantica PDF è identica in tutte e tre. Ciò che cambia, e che questo articolo mappa, è l'ABI: calling convention, encoding delle stringhe, ownership degli handle e chi può liberare quale buffer.
Una facade, tre forme binarie
Ogni funzione pubblica di TPDFlib ha una controparte flat chiamata DL più il nome del metodo: LoadFromFile diventa DLLoadFromFile, Encrypt diventa DLEncrypt, NewSignProcessFromFile diventa DLNewSignProcessFromFile. Il primo parametro di quasi ogni export è un InstanceID restituito da DLCreateLibrary, che sostituisce il riferimento oggetto che un chiamante Delphi terrebbe. Questa mappatura uno-a-uno vale la pena interiorizzarla presto, perché significa che il riferimento API Delphi funge anche da documentazione per ogni altro linguaggio: qualunque cosa faccia la classe, la DLL può farla con un nome prevedibile.
La build Windows produce PDFlibDLL32.dll e PDFlibDLL64.dll; scegli quella che combacia con la bitness del processo host, perché un processo Java o .NET a 64 bit non può caricare la libreria a 32 bit qualunque sia la dichiarazione.
Windows: istanze Stdcall e coppie funzione W/A
Ogni export che prende stringhe esiste due volte: una versione wide con PWideChar, UTF-16 e naturale per .NET, Java e c_wchar_p di Python, e una versione con suffisso A che prende PAnsiChar. Le due sono semanticamente identiche e differiscono solo per encoding, che è esattamente perché mischiarle è così doloroso da debug: nulla fallisce, ottieni solo mojibake nei metadati o "file not found" per percorsi con caratteri oltre 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';
Scegli una larghezza carattere per host e codificala nel generatore di binding. Regola pratica: se il linguaggio host ha stringhe native UTF-16, collega ovunque le versioni W e non toccare più la famiglia A.
macOS: stessi nomi, ABI diversa
La dylib esporta lo stesso set di funzioni DL, ma con due cambi sistematici: la calling convention è Cdecl, e ogni nome export porta un underscore iniziale, _DLCreateLibrary, _DLLoadFromFile e così via. Entrambi i cambi sono meccanici, quindi perfetti per un binding generato e pessimi per copie Windows editate a mano. Se il tuo livello di binding lo supporta, mantieni una lista canonica di funzioni ed emetti dichiarazioni per piattaforma; il failure mode del non farlo è la corruzione dello stack con cui l'articolo si è aperto, e si riproduce solo sulla piattaforma che testi meno.
Host COM e ActiveX: Safecall e payload Olevariant
Per VB.NET, C#, VBScript e host automation legacy, la build OCX avvolge la stessa facade in un oggetto automation IDispatch, IPDFlibrary, con ogni metodo dichiarato Safecall. Questa scelta di convenzione conta per la gestione errori: Safecall traduce i fallimenti interni in valori COM HRESULT, quindi un chiamante C# vede un'eccezione catturabile invece di un error code silenzioso, l'opposto della DLL flat, dove devi controllare tu i valori restituiti.
La seconda regola specifica COM riguarda i dati binari. Nell'interfaccia automation non ci sono parametri puntatore; qualunque cosa binaria, byte immagine in ingresso o byte PDF in uscita, attraversa il boundary come Olevariant, tramite metodi come AddImageFromVariant e AppendToVariant. Marshallare un byte array in una variant è una riga in .NET, ma se provi a passare un puntatore grezzo perché "è comunque lo stesso processo", il layer dispatch lo rifiuterà o lo storpierà. Infine, ricorda che la registrazione COM è per bitness: un OCX registrato con regsvr32 a 32 bit è invisibile a un host a 64 bit, cosa che al sito cliente si manifesta come il famigerato "class not registered".
Disciplina degli handle: le istanze possiedono documenti
La flat API è un'economia di handle. DLCreateLibrary restituisce un'istanza; il load restituisce un document ID dentro quell'istanza; processi di firma, string list e file direct-access restituiscono ulteriori handle interi con scope nella stessa istanza. Il ciclo di vita canonico appare così da qualunque host FFI, scritto qui in Pascal per leggibilità:
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;
Ne derivano due conseguenze. Primo, DLReleaseLibrary è l'unico cleanup strettamente necessario, perché abbatte ogni handle documento e processo sotto l'istanza, ma appoggiarsi a questo in un servizio long-running è un leak lento con passaggi in più; rilascia i documenti quando hai finito. Secondo, l'istanza è l'unità naturale di isolamento dei thread: dai a ogni worker thread il proprio InstanceID e non condividerne mai uno tra thread senza locking esterno, esattamente come non condivideresti un oggetto TPDFlib.
Le stringhe restituite sono prese in prestito, non possedute
Le funzioni che restituiscono testo, come DLGetPageText, consegnano un PWideChar o PAnsiChar che punta in un buffer posseduto e riciclato dall'istanza della libreria. Il contratto è: copia subito, non liberare mai.
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# significa marshallare l'IntPtr a stringa managed prima della chiamata successiva alla libreria; in Python ctypes, estrarre subito la wide string dal puntatore. Tenere il puntatore grezzo attraverso più chiamate è il tipo di bug che passa ogni unit test e fallisce sotto concorrenza di produzione. La stessa regola di ownership vale nell'altra direzione per callback registrate tramite DLSetProgressCallback: qualunque puntatore passato dalla libreria alla callback è valido solo per la durata di quella callback, e la callback stessa deve restare viva, pinned negli host garbage-collected, finché l'istanza potrebbe invocarla. Un delegate raccolto dal GC a metà job è la causa canonica di access violation "casuali" in binding .NET che hanno funzionato per mesi.
Infine, incorpora lo smoke test nel binding stesso. Prima di distribuire un set di dichiarazioni generato, esegui una chiamata per ogni categoria: funzione senza parametri, DLCreateLibrary, funzione con stringa in ingresso e percorso non ASCII, funzione con stringa in uscita, e un'operazione che fallisce apposta per vedere come gli error code emergono nell'host. Quindici minuti così intercettano errori di convenzione ed encoding che altrimenti emergono come crash dump cliente.
Domande sui binding che arrivano al supporto
Quali funzioni dovrebbe usare un binding Python ctypes su Windows? Carica la DLL con WinDLL, Stdcall, collega le funzioni W senza suffisso e dichiara i parametri stringa come c_wchar_p. Su macOS, passa a CDLL, mantieni la stessa lista funzioni e risolvi i nomi senza underscore: il loader su macOS gestisce la convenzione del prefisso nella maggior parte dei layer FFI, ma verifica con una chiamata prima di generarne centinaia.
Devo registrare qualcosa per usare la DLL semplice? No. La registrazione con regsvr32 si applica solo alla build ActiveX. La DLL si distribuisce copiando file, uno dei motivi per preferirla per servizi e workload Windows containerizzati.
La DLL è thread-safe? Il pattern sicuro è un'istanza per thread. L'handle istanza porta tutto lo stato mutabile, documento selezionato, opzioni di rendering, impostazioni di estrazione, quindi due thread che condividono un'istanza interlacciano silenziosamente cambi di stato anche quando le chiamate riescono.
Letture correlate
Una volta in posizione il binding, le operazioni che espone sono le stesse trattate in profondità dagli articoli Delphi, ad esempio applicare e auditare la cifratura PDF, oppure estrarre testo e immagini da documenti esistenti.
I download binari per tutti e tre i livelli di integrazione sono distribuiti con la libreria; vedi la pagina prodotto PDFlibPas per edizioni e licenza.