Le rapport de bogue disait : « fonctionne depuis C# sous Windows, plante depuis Python sous macOS ». L’équipe avait construit sa liaison macOS en copiant le fichier de déclarations Windows et en remplaçant le nom du binaire. Tous les symboles se résolvaient, le premier appel renvoyait des données incohérentes, le second plantait. Leur logique PDF n’était pas en cause : les exports Windows utilisent la convention Stdcall tandis que la dylib macOS exporte les mêmes fonctions en Cdecl avec un préfixe souligné, et une liaison FFI qui ignore l’un ou l’autre détail corrompt la pile avant même l’ouverture d’un document
PDFlibPas, le moteur PDF à code source disponible de losLab pour Delphi et C++Builder, enveloppe tout son modèle objet dans une seule classe façade plate, TPDFlib, puis livre cette façade sous trois formes binaires : une DLL Windows avec environ 1 250 fonctions exportées, un objet d’automatisation COM/ActiveX et une dylib macOS. La sémantique PDF est identique dans les trois cas. Ce qui diffère — et ce que cet article cartographie — est l’ABI : conventions d’appel, encodages de chaînes, propriété des handles et qui a le droit de libérer quel tampon
Une façade, trois formes binaires
Chaque fonction publique de TPDFlib possède une contrepartie plate nommée DL plus le nom de méthode : LoadFromFile devient DLLoadFromFile, Encrypt devient DLEncrypt, NewSignProcessFromFile devient DLNewSignProcessFromFile. Le premier paramètre de presque chaque export est un InstanceID renvoyé par DLCreateLibrary, qui remplace la référence objet qu’un appelant Delphi conserverait. Cette correspondance un-pour-un mérite d’être intégrée tôt, car elle signifie que la référence API Delphi sert aussi de documentation pour tous les autres langages : tout ce que la classe sait faire, la DLL sait le faire sous un nom prévisible
Le build Windows produit PDFlibDLL32.dll et PDFlibDLL64.dll ; choisissez celui qui correspond au bitness du processus hôte, puisqu’un processus Java ou .NET 64 bits ne peut pas charger la bibliothèque 32 bits, quelle que soit la forme de la déclaration
Windows : instances Stdcall et paires de fonctions W/A
Chaque export qui reçoit des chaînes existe deux fois : une version wide prenant PWideChar (UTF-16, le choix naturel pour .NET, Java et le c_wchar_p de Python) et une version suffixée A prenant PAnsiChar. Les deux ont la même sémantique et ne diffèrent que par l’encodage, ce qui explique précisément pourquoi les mélanger est si pénible à déboguer : rien ne casse, vous obtenez simplement du mojibake dans les métadonnées ou un « fichier introuvable » pour les chemins contenant un caractère au-delà de l’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';
Choisissez une largeur de caractères par hôte et inscrivez-la dans le générateur de liaison. Règle pratique : si le langage hôte possède des chaînes UTF-16 natives, liez partout les versions W et ne touchez plus jamais à la famille A
macOS : mêmes noms, ABI différente
La dylib exporte le même jeu de fonctions DL, mais avec deux changements systématiques : la convention d’appel est Cdecl, et chaque nom d’export porte un souligné initial — _DLCreateLibrary, _DLLoadFromFile, etc. Les deux changements sont mécaniques, ce qui en fait de parfaits candidats pour une liaison générée et de très mauvais candidats pour des copies éditées à la main du fichier Windows. Si votre couche de liaison le permet, conservez une liste canonique de fonctions et émettez des déclarations par plateforme ; le mode de panne quand on ne le fait pas est l’histoire de corruption de pile qui ouvre cet article, et il ne se reproduit que sur la plateforme que vous testez le moins
Hôtes COM et ActiveX : Safecall et charges utiles Olevariant
Pour VB.NET, C#, VBScript et les hôtes d’automatisation hérités, le build OCX enveloppe la même façade dans un objet d’automatisation IDispatch, IPDFlibrary, avec toutes les méthodes déclarées Safecall. Ce choix de convention compte pour la gestion des erreurs : Safecall traduit les échecs internes en valeurs HRESULT COM, de sorte qu’un appelant C# voit une exception interceptable au lieu d’un code d’erreur silencieux, à l’inverse de la DLL plate où vous devez vérifier vous-même les valeurs de retour
La seconde règle spécifique à COM concerne les données binaires. L’interface d’automatisation ne comporte aucun paramètre pointeur ; tout ce qui est binaire, octets d’image en entrée ou octets PDF en sortie, traverse la frontière sous forme d’Olevariant, via des méthodes comme AddImageFromVariant et AppendToVariant. Marshalling d’un tableau d’octets vers un variant prend une ligne en .NET, mais si vous tentez de passer un pointeur brut parce que « c’est le même processus de toute façon », la couche dispatch le rejettera ou le déformera. Enfin, souvenez-vous que l’enregistrement COM est lié au bitness : un OCX enregistré avec le regsvr32 32 bits est invisible pour un hôte 64 bits, ce qui se manifeste chez le client par le célèbre et peu utile « classe non enregistrée »
Discipline des handles : les instances possèdent les documents
L’API plate est une économie de handles. DLCreateLibrary renvoie une instance ; le chargement renvoie un ID de document dans cette instance ; les processus de signature, listes de chaînes et fichiers à accès direct renvoient tous d’autres handles entiers limités à la même instance. Le cycle de vie canonique ressemble à ceci depuis n’importe quel hôte FFI, écrit ici en Pascal pour la lisibilité :
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;
Deux conséquences en découlent. D’abord, DLReleaseLibrary est le seul nettoyage strictement nécessaire, car il détruit tous les documents et handles de processus détenus par l’instance ; mais s’appuyer là-dessus dans un service longue durée revient à une fuite lente avec des étapes supplémentaires, donc libérez les documents terminés. Ensuite, l’instance est l’unité naturelle d’isolation entre threads : donnez à chaque worker thread son propre InstanceID et ne partagez jamais une instance entre threads sans verrouillage externe, exactement comme vous ne partageriez pas un objet TPDFlib
Les chaînes renvoyées sont empruntées, pas possédées
Les fonctions qui renvoient du texte, comme DLGetPageText, remettent un PWideChar ou un PAnsiChar pointant vers un tampon possédé et recyclé par l’instance de bibliothèque. Le contrat est simple : copiez immédiatement, ne libérez jamais
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;
En C#, cela signifie convertir l’IntPtr en chaîne managée avant l’appel suivant à la bibliothèque ; en Python ctypes, extraire aussitôt la chaîne wide depuis le pointeur. Conserver le pointeur brut à travers les appels est le type de bogue qui passe tous les tests unitaires et échoue sous concurrence de production. La même règle de propriété s’applique dans l’autre sens aux callbacks enregistrés via DLSetProgressCallback : tout pointeur que la bibliothèque transmet à votre callback n’est valide que pendant la durée de ce callback, et le callback lui-même doit rester vivant, épinglé dans les hôtes à ramasse-miettes, aussi longtemps que l’instance peut l’invoquer. Un delegate collecté en plein job est la cause canonique des violations d’accès « aléatoires » dans des liaisons .NET qui ont fonctionné pendant des mois
Enfin, intégrez le smoke test dans la liaison elle-même. Avant d’expédier un jeu de déclarations générées, exécutez un appel dans chaque catégorie : une fonction sans paramètre (DLCreateLibrary), une fonction avec chaîne en entrée sur un chemin non ASCII, une fonction avec chaîne en sortie et une opération qui échoue volontairement pour voir comment les codes d’erreur remontent dans votre hôte. Quinze minutes de ce test attrapent des erreurs de convention et d’encodage qui, sinon, deviennent des dumps de plantage client
Questions de liaison fréquentes au support
Quelles fonctions une liaison Python ctypes doit-elle utiliser sous Windows ? Chargez la DLL avec WinDLL (Stdcall), liez les fonctions W non suffixées et déclarez les paramètres chaîne en c_wchar_p. Sous macOS, passez à CDLL, gardez la même liste de fonctions et résolvez les noms sans le souligné ; le chargeur macOS gère la convention de préfixe dans la plupart des couches FFI, mais vérifiez avec un appel avant d’en générer des centaines
Dois-je enregistrer quelque chose pour utiliser la DLL plate ? Non. L’enregistrement avec regsvr32 ne concerne que le build ActiveX. La DLL se déploie par copie de fichier, ce qui est l’une des raisons de la préférer pour les services et les charges Windows conteneurisées
La DLL est-elle thread-safe ? Le modèle sûr est une instance par thread. Le handle d’instance porte tout l’état mutable, document sélectionné, options de rendu, paramètres d’extraction ; deux threads qui partagent une instance entremêleront silencieusement leurs changements d’état même lorsque les appels réussissent
Lectures connexes
Une fois la liaison en place, les opérations qu’elle expose sont les mêmes que celles décrites en détail dans les articles Delphi, par exemple appliquer et auditer le chiffrement PDF, ou extraire du texte et des images de documents existants
Les téléchargements binaires pour les trois couches d’intégration sont fournis avec la bibliothèque ; consultez la page produit PDFlibPas pour les éditions et les licences