Article technique

Charger la bibliothèque native PDFium sur toute cible

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

Diagramme de la chaîne de recherche de la bibliothèque native PDFium pour Delphi, du sous-répertoire de cible DLLs par les dispositions alternative, plate et répertoire système Windows jusqu'au chargeur de plateforme
Quatre emplacements explicites sont sondés dans l'ordre avant que le chargeur du système ne soit 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é

Comment une majuscule dans la macro de cible FPC casse la recherche de la DLL PDFium sous Linux : le chargeur cherche DLLs/x86_64-Linux alors que le dossier déployé est DLLs/x86_64-linux, qui ne correspond que sur Windows insensible à la casse
La même cible orthographiée de deux façons correspond sous Windows et rate silencieusement sur un système de fichiers sensible à la casse

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

Collision de nom UnloadLibrary dans le composant PDFium : un appel non qualifié dans l'unité de liaison Delphi boucle sur lui-même, tandis qu'un appel qualifié par unité atteint l'unité de chargeur portable et libère le handle
Qualifier le site d'appel envoie la libération par l'unité de chargeur au lieu de boucler dans l'unité de liaison

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