Le composant PDFium trouve sa bibliothèque native par une chaîne de recherche fixe et ordonnée plutôt que de la laisser au chargeur du système d'exploitation, car un arbre de déploiement explicite est un arbre de déploiement que vous pouvez déboguer. Sous Windows, cette chaîne cherche un sous-répertoire Win32 ou Win64 que l'installateur livre déjà. Sur les autres cibles, elle construit le nom du sous-répertoire à partir des macros de cible Free Pascal, sous la forme <cpu>-<os>, de sorte que l'arbre de déploiement se lit exactement comme l'arbre des unités compilées. Cette dernière décision a introduit un bug qui vaut tout l'article, car la cause était une majuscule et le symptôme était le silence
La chaîne, dans l'ordre
Quatre emplacements, essayés en séquence, puis le chargeur de plateforme en dernier recours. D'abord la disposition préférée, un répertoire DLLs à côté de l'exécutable contenant un sous-répertoire par cible. Ensuite une disposition alternative avec le sous-répertoire de cible directement à côté de l'exécutable. Puis la disposition plate héritée, la bibliothèque posée à côté de l'exécutable sans aucun sous-répertoire. Quatrièmement, sous Windows seulement, le répertoire système, qui exige du soin car un processus 32 bits doit chercher dans SysWOW64 et un processus 64 bits dans System32, et sous Windows 32 bits le premier n'existe pas donc la recherche doit retomber. Seulement après tout cela, le chargeur est prié de chercher tout seul
Il n'y a délibérément aucune étape de répertoire système hors Windows. Le propre chemin de recherche du chargeur de plateforme, piloté par la configuration de l'éditeur de liens d'exécution et l'environnement de chemins de bibliothèques, couvre déjà ce terrain, et le dupliquer en Pascal signifierait réimplémenter des règles qui varient selon la distribution. Le diagnostic des échecs dans la chaîne Windows est couvert séparément dans le déploiement de la DLL PDFium et le diagnostic des échecs de chargement
D'où vient le nom du sous-répertoire
Sous Windows, c'est Win32 ou Win64, décidé par le nombre de bits du processus en cours plutôt que du système d'exploitation, car c'est cela qui détermine quel binaire peut être chargé. Partout ailleurs, le nom est construit à partir des macros de cible du compilateur pour qu'une machine qui compile pour deux architectures produise deux arbres clairement séparés, et pour que le dossier portant la bibliothèque native se trouve à côté du dossier portant les unités compilées avec le même nom
function BuildDllSubDir(UseV8: Boolean): string;
begin
{$IFDEF MSWINDOWS}
if IsWin64 then
Result := 'Win64'
else
Result := 'Win32';
{$ELSE}
// Les macros du compilateur mettent une majuscule au système ("Linux", "Darwin")
// alors que le répertoire de sortie des unités du paquet ne le fait pas, donc
// les deux ne s'accordent qu'après abaissement de casse. Sur un système de
// fichiers sensible à la casse, cette différence est toute la recherche
Result := LowerCase({$I %FPCTARGETCPU%} + '-' + {$I %FPCTARGETOS%});
{$ENDIF}
end;
Pourquoi une majuscule a cassé toute la chaîne
La macro du compilateur épelle le système d'exploitation cible avec une capitale initiale : Win64, Linux, Darwin. Le paquet Lazarus écrit sa sortie d'unités dans un répertoire nommé depuis sa propre variable de cible, qui est en minuscules : win64, linux, darwin. Deux orthographes de la même chose, et aucun moyen de le remarquer sous Windows, où le système de fichiers ne les distingue pas
Sous Linux, ce sont deux répertoires différents. Un déploiement qui met l'objet partagé dans DLLs/x86_64-linux est invisible pour un chargeur qui cherche DLLs/x86_64-Linux, donc les quatre étapes explicites de la chaîne ratent et le code tombe dans le fait de laisser le chargeur de plateforme chercher. Parfois cela fonctionne, si la bibliothèque se trouve installée à l'échelle du système, et parfois non, et de toute façon l'arbre de déploiement soigneusement agencé ne contribue à rien. L'échec n'a aucun message d'erreur car rien n'a échoué : chaque étape a correctement rapporté que le fichier n'était pas là où elle a cherché
Le programme sonde, compilé et exécuté
Cette classe de bug ne peut pas être trouvée en lisant, et elle ne peut pas être trouvée en compilant non plus. La technique habituelle pour vérifier une branche de plateforme qui ne compile jamais sur la machine de développement est de copier l'unité dans un répertoire temporaire, la renommer, remplacer le conditionnel de plateforme par un symbole jamais défini, et compiler la copie ; si elle compile, la clause uses et les signatures d'appel sur ce chemin sont au moins auto-cohérentes. Cela marche bien pour une unité autonome
Ici, cela ne marche pas. L'unité de liaison principale est très grande et tire la LCL, donc elle ne peut pas simplement être copiée et compilée avec le symbole Windows désactivé. À la place, la poignée de fonctions que le changement touchait a été transcrite verbatim dans un petit programme autonome, et ce programme a été exécuté. Il a imprimé x86_64-Win64, et la divergence était visible dans une ligne de sortie. Compiler le même programme ne vous aurait rien dit, car la chaîne est parfaitement valide ; seule sa valeur est fausse
program ProbeSubDir;
{$MODE DELPHI}
uses
SysUtils;
begin
// Imprimez, n'affirmez pas. Le point est de regarder la valeur à laquelle une
// macro s'étend réellement sur cette chaîne d'outils
Writeln('raw: ', {$I %FPCTARGETCPU%}, '-', {$I %FPCTARGETOS%});
Writeln('folded: ', LowerCase({$I %FPCTARGETCPU%} + '-' +
{$I %FPCTARGETOS%}));
end.
La leçon générale : quand un changement multiplateforme concerne la valeur de quelque chose plutôt que son type, une vérification par compilation seule n'est pas une vérification. Imprimez-la. L'ensemble plus large des différences inter-compilateurs entre Delphi et Free Pascal est rassemblé dans l'article sur les pièges inter-compilateurs Delphi et FPC
Laissez la plateforme expliquer ses propres échecs de chargement
La branche Windows du chargeur énumère à la main les raisons pour lesquelles un chargement peut échouer, car les distinctions utiles là-bas, une incompatibilité d'architecture, une dépendance transitive manquante, un chemin qui ne se résout pas, correspondent à des codes d'erreur qui valent la peine d'être nommés individuellement. Hors Windows, l'unité de chargeur portable renvoie déjà une chaîne descriptive qui couvre le même terrain, donc la branche non Windows l'utilise directement plutôt que de redériver des catégories depuis un numéro d'erreur qui signifie des choses différentes sur différents systèmes
Résister à l'envie de normaliser ces deux-là en un message est délibéré. Un échec de chargement est un problème de déploiement, et la personne qui lit le message a besoin du propre vocabulaire de la plateforme pour le chercher
Une collision de noms qui boucle sur elle-même
Encore un piège, petit et acéré. L'unité de chargeur portable exporte une procédure appelée UnloadLibrary, et l'unité de liaison a une procédure du même nom qui fait sa propre comptabilité avant de libérer le handle. À l'intérieur de cette procédure, un appel non qualifié à UnloadLibrary se résout vers celle de l'unité courante, qui s'appelle elle-même. La correction est de qualifier l'appel avec le nom de l'unité
C'est la même forme que les problèmes de masquage d'identificateurs qui dominent les portages Free Pascal en général : l'unité Windows exporte des fonctions minimum et maximum typées entiers qui masquent celles en virgule flottante, et un type de synchronisation qui masque la classe du même nom, et dans chaque cas la résolution dépend de l'ordre de la clause uses. Qualifier le site d'appel est la correction qui ne dépend pas de quelqu'un préservant cet ordre plus tard
Liste de contrôle de déploiement
Trois choses expliquent la plupart des échecs de chargement une fois l'arithmétique des chemins juste. L'architecture doit correspondre au processus, pas à la machine, donc une application 32 bits sur Windows 64 bits a besoin du binaire 32 bits. La compilation avec V8 a un nom de fichier différent, donc un déploiement qui les mélange aura l'air correct et ne chargera rien. Et une seule variante peut vivre dans un répertoire système à la fois, ce qui est une bonne raison de préférer la disposition de sous-répertoires explicite à installer quoi que ce soit à l'échelle du système
Pour Lazarus spécifiquement, mettez la bibliothèque native sous DLLs/<cpu>-<os> en minuscules, à côté de l'exécutable, et elle sera trouvée par la première étape de la chaîne sur chaque cible. L'exemple de visionneur qui exerce cela sur Lazarus est décrit dans l'article sur la visionneuse Lazarus et FPC, et la prise en charge actuelle des plateformes est listée sur la page produit du PDFium Delphi component