Voici un problème qui surgit dès qu'une bibliothèque PDF quitte son langage d'origine. Vous avez une liaison qui fonctionne parfaitement depuis C# sous Windows. Il vous faut les mêmes appels depuis Python sous macOS, alors vous copiez le fichier de déclarations Windows, changez le nom du binaire et lancez le tout. Chaque symbole se résout. Le premier appel renvoie n'importe quoi, le deuxième plante sur une violation d'accès, et rien n'a changé dans votre code PDF. La faute se situe une couche en dessous du PDF : les exports Windows utilisent la convention Stdcall, la dylib macOS exporte les mêmes fonctions en Cdecl avec un tiret bas en préfixe, et une déclaration de fonction étrangère qui se trompe sur l'un de ces deux détails corrompt la pile avant même qu'un document ne soit ouvert
Toute cette famille de pannes découle d'une décision de conception qu'il vaut mieux comprendre d'emblée. PDF Library for Delphi, le moteur PDF à sources disponibles 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'automation COM/ActiveX et une dylib macOS. La sémantique PDF est identique dans les trois cas. Ce qui vous mord vit dans l'ABI en dessous : conventions d'appel, encodages de chaînes, propriété des handles, et le camp autorisé à libérer tel ou tel tampon
Une façade, trois formes binaires
Chaque fonction publique de TPDFlib a une contrepartie plate nommée DL suivi du nom de la méthode. LoadFromFile devient DLLoadFromFile, Encrypt devient DLEncrypt, NewSignProcessFromFile devient DLNewSignProcessFromFile. Le premier paramètre de presque tous les exports est un InstanceID renvoyé par DLCreateLibrary, qui tient lieu de la référence objet qu'un appelant Delphi détiendrait autrement. Intériorisez cette correspondance tôt. Elle signifie que la référence de l'API Delphi sert aussi de documentation pour tous les autres langages : ce que la classe sait faire, la DLL le fait sous un nom prévisible, et vous pouvez lire une signature de méthode Pascal pour apprendre l'appel dont vous avez besoin depuis Python ou C#
La compilation Windows produit PDFlibDLL32.dll et PDFlibDLL64.dll ; choisissez celle qui correspond au nombre de bits de votre processus hôte, car un processus Java ou .NET 64 bits ne peut pas charger la bibliothèque 32 bits, quelle que soit l'allure de la déclaration
Windows : instances Stdcall et les paires de fonctions W/A
Chaque export prenant une chaîne existe en deux exemplaires. Une version large prend PWideChar (UTF-16, le format naturel pour .NET, Java et le c_wchar_p de Python), et une version suffixée A prend PAnsiChar. Les deux portent une sémantique identique et ne diffèrent que par l'encodage, ce qui est précisément ce qui rend leur mélange si pénible à traquer : rien ne lève d'exception, rien ne renvoie de code d'erreur, vous obtenez simplement du mojibake dans les métadonnées ou un "fichier introuvable" fallacieux pour tout chemin comportant un caractère au-delà de l'ASCII pur. Le premier bug d'encodage rencontré ainsi coûte en général une après-midi à une équipe, car le symptôme désigne les données alors que la cause est dans la déclaration
// Liaison Windows (PDFlibDLL64.dll) : Stdcall, noms d'export simples
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';
// Liaison macOS : même fonction, Cdecl, et un tiret bas en préfixe de l'export
function DLCreateLibrary: Integer; cdecl;
external 'PDFlibDylib.dylib' name '_DLCreateLibrary';
Choisissez une largeur de caractère par hôte et codifiez-la dans le générateur de liaisons. Une 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 avec deux changements systématiques. La convention d'appel est Cdecl plutôt que Stdcall, et chaque nom d'export porte un tiret bas en préfixe (_DLCreateLibrary, _DLLoadFromFile, et ainsi de suite). Les deux changements sont purement mécaniques, ce qui les rend idéaux pour une liaison générée et dangereux pour une copie du fichier Windows retouchée à la main. Gardez une seule liste canonique de fonctions et émettez-en les déclarations par plateforme si votre outillage le permet. Faites l'impasse et vous obtenez exactement la corruption de pile décrite en tête de cette page, reproductible uniquement sur la plateforme que votre CI exerce le moins
Hôtes COM et ActiveX : Safecall et charges utiles Olevariant
Pour VB.NET, C#, VBScript et les hôtes d'automation hérités, la compilation OCX enveloppe la même façade dans un objet d'automation IDispatch, IPDFlibrary, dont chaque méthode est déclarée Safecall. Cette convention change la façon dont les erreurs vous parviennent. Safecall traduit une défaillance interne en HRESULT COM, si bien qu'un appelant C# attrape une exception là où la DLL plate aurait renvoyé un entier discret que l'appelant devait penser à vérifier. La même opération, deux idiomes de défaillance, selon le binaire chargé
Les données binaires suivent une seconde règle propre à COM. L'interface d'automation ne comporte aucun paramètre pointeur. Tout ce qui est binaire, octets d'image en entrée ou octets PDF en sortie, franchit la frontière sous forme d'Olevariant via des méthodes telles que AddImageFromVariant et AppendToVariant. Marshaler un tableau d'octets dans un variant tient en une ligne sous .NET. Essayez de lui passer un pointeur brut à la place, en vous disant que c'est de toute façon le même processus, et la couche de dispatch rejette ou massacre l'appel. Un détail d'enregistrement supplémentaire fait trébucher les déploiements : l'enregistrement COM dépend du nombre de bits, donc un OCX enregistré avec le regsvr32 32 bits est invisible pour un hôte 64 bits. Ce décalage se manifeste par le fameux et peu utile "classe non enregistrée" sur la machine du client, longtemps après avoir quitté la vôtre
Discipline des handles : les instances possèdent les documents
L'API plate fonctionne sur des handles entiers. DLCreateLibrary renvoie une instance. Charger un fichier renvoie un identifiant de document à l'intérieur de cette instance. Les processus de signature, les listes de chaînes et les fichiers en accès direct renvoient chacun leurs propres handles entiers, tous rattachés à la même instance. Le cycle de vie est identique depuis n'importe quel hôte FFI, montré ici en Pascal parce qu'il se lit clairement :
var
Inst, Doc: Integer;
begin
Inst := DLCreateLibrary; // une instance par thread de travail
try
Doc := DLLoadFromFile(Inst, 'in.pdf', ''); // renvoie un DocumentID, 0 en cas d'échec
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); // libère tous les documents détenus par l'instance
end;
end;
Deux conséquences découlent de cet arbre de propriété. DLReleaseLibrary est le seul appel de nettoyage dont vous ayez strictement besoin, puisqu'il démonte en un coup tous les documents et handles de processus sous l'instance. Dans un script court, cela suffit. Dans un service de longue durée, cela devient une fuite lente accompagnée de cérémonie superflue, alors libérez les documents au fur et à mesure plutôt que de les laisser s'accumuler jusqu'à la mort de l'instance. L'instance est aussi l'unité naturelle d'isolation des threads. Donnez à chaque thread de travail son propre InstanceID, et n'en partagez jamais une entre threads sans verrouillage externe, pour la même raison que vous ne partageriez jamais un unique objet TPDFlib entre threads
Les chaînes renvoyées sont empruntées, pas possédées
Les fonctions qui renvoient du texte, comme DLGetPageText, rendent un PWideChar ou un PAnsiChar qui pointe dans un tampon détenu et recyclé par l'instance de la bibliothèque. Le contrat est le suivant : copiez immédiatement, ne libérez jamais
var
P: PWideChar;
PageText: string;
begin
P := DLGetPageText(Inst, 7); // pointeur dans un tampon détenu par la bibliothèque
PageText := P; // copier maintenant ; un appel ultérieur peut réutiliser le tampon
end;
En C#, cela signifie marshaler l'IntPtr en chaîne gérée avant l'appel suivant à la bibliothèque. En ctypes Python, cela signifie découper la chaîne large hors du pointeur sans attendre. Conservez le pointeur brut d'un appel à l'autre et vous avez écrit un bug qui passe tous les tests unitaires puis échoue la première fois que deux requêtes se chevauchent en production, parce que le deuxième appel a recyclé le tampon que le premier était encore en train de lire. La même règle de propriété joue dans l'autre sens pour les rappels enregistrés via DLSetProgressCallback. Tout pointeur que la bibliothèque passe à votre rappel n'est valide que pendant le corps de ce rappel, et l'objet de rappel lui-même doit rester vivant (épinglé, dans un hôte à ramasse-miettes) aussi longtemps que l'instance est susceptible de l'invoquer. Un délégué collecté en plein travail est la source scolaire de la violation d'accès "aléatoire" qui apparaît dans une liaison .NET tournée sans faute pendant des mois
Intégrez un test de fumée dans la liaison elle-même, et exécutez-le avant toute livraison d'un jeu de déclarations généré. Exercez un appel de chaque catégorie qui tend à révéler les erreurs d'ABI : une fonction sans paramètre comme DLCreateLibrary pour prouver que la convention est bonne, une fonction prenant une chaîne alimentée par un chemin à caractères non ASCII pour prouver que l'encodage est bon, une fonction renvoyant une chaîne pour prouver que la gestion du tampon emprunté est bonne, et une opération qui échoue exprès afin d'observer comment une erreur remonte jusqu'à votre hôte. Cela représente un quart d'heure de travail, et cela attrape les fautes de convention d'appel et d'encodage qui arriveraient autrement des mois plus tard sous la forme du vidage mémoire d'un client
Le cas de ctypes en Python, concrètement
ctypes en Python est la liaison que je vois le plus souvent écrite à la main, et elle rend la division multiplateforme facile à démontrer. Sous Windows, chargez la bibliothèque avec ctypes.WinDLL pour que ctypes applique Stdcall, liez les fonctions W sans suffixe et déclarez chaque paramètre chaîne en c_wchar_p. Sous macOS, chargez-la avec ctypes.CDLL pour Cdecl, gardez la liste de fonctions identique et résolvez les noms sans le tiret bas initial. La plupart des couches FFI, ctypes comprise, réintègrent pour vous la convention du tiret bas sous macOS, mais c'est la seule hypothèse à confirmer par un unique appel résolu avant de générer des centaines de déclarations par-dessus
Deux questions de déploiement traînent derrière le travail de liaison et ont des réponses nettes. La DLL simple ne nécessite aucun enregistrement : regsvr32 ne concerne que la compilation ActiveX, et la DLL se livre par simple copie de fichier, ce qui est la principale raison de la préférer pour les services Windows et les conteneurs où vous préférez ne pas toucher au registre du tout. La sûreté des threads se réduit à la règle déjà en vigueur ci-dessus, une instance par thread. Le handle d'instance détient chaque morceau d'état mutable que suit le moteur, le document sélectionné, les options de rendu, les réglages d'extraction, si bien que deux threads partageant une instance entrelacent leurs états respectifs même quand chaque appel pris isolément renvoie un succès
Une fois la liaison solide, les opérations qui se trouvent de l'autre côté sont exactement celles que les articles Delphi traitent en profondeur, notamment appliquer et auditer le chiffrement PDF et extraire texte et images de documents existants
Les téléchargements binaires des trois couches d'intégration sont livrés avec la bibliothèque ; consultez la page produit de PDF Library for Delphi pour les éditions et les licences