Quand une bibliothèque Delphi gagne une configuration de compilation sans le framework visuel, les classes de substitution sont l'endroit où vivent les bugs. Pas la plateforme, pas le compilateur : les substituts. PDFlibPas a une couche graphique qui fournit des équivalents bitmap, canvas, police, métafichier et imprimante pour les compilations sans la VCL, et son portage vers Free Pascal a fait surface à tous les modes de défaillance qu'un substitut peut avoir. Ils se trient nettement par coût de diagnostic, et l'ordre est l'inverse de ce que l'intuition suggère
Un substitut qui lève une exception est bon marché à trouver ; l'exception nomme la méthode. Un substitut qui renvoie des données vides est coûteux, car la défaillance apparaît à plusieurs couches de sa cause. Un substitut qui renvoie le succès est le pire de tous, car le code de retour est valide, le code d'erreur est zéro, aucune exception n'est levée, et la seule preuve que quelque chose a mal tourné est dans les octets produits
Forme trois : un identifiant d'image valide sur un XObject vide
Le convertisseur de métafichiers vectoriels était un corps de procédure vide dans la configuration non VCL. Tout ce qui était au-dessus continuait de fonctionner. Les points d'entrée d'import EMF et le point d'entrée de capture de canvas s'exécutaient jusqu'au bout et renvoyaient un identifiant d'image légal, que l'appelant plaçait ensuite sur une page. Ce qui atterrissait dans le fichier était un form XObject de longueur de contenu zéro. La page se rendait blanche
Rien ne signalait de problème, et cela inclut le programme de démonstration de la bibliothèque pour cette fonctionnalité, qui dessinait une page blanche sans s'en apercevoir. Il n'y avait aucune valeur de retour en échec à vérifier, car la séquence d'appels a réellement tout réussi ; la seule chose qui était fausse était la taille du flux produit. Diagnostiquer cette classe de défaut signifie poser une autre question : pas « l'appel a-t-il échoué » mais « l'artefact est-il plausible ». Un form XObject de longueur zéro, une image de zéro pixels, une page de zéro octets de contenu, voilà les assertions qui l'attrapent
La correction a deux moitiés et la seconde est facile à oublier. D'abord, faire lever l'implémentation vide, pour que la défaillance ait un canal au moins. Ensuite, convertir cette exception en résultat nul à la fabrique d'images et ajouter des vérifications de null aux deux endroits qui consomment un identifiant d'image, car sinon « l'échec propre » se transforme directement en violation d'accès quand l'arbre de pages déréférence rien. Un stub qui lance n'est une amélioration que si les appelants étaient préparés à un échec qu'ils n'avaient jamais pu recevoir auparavant
Forme deux : données vides, trois couches du plantage
Le substitut de canvas de métafichier ne remplissait pas ses dimensions physiques. Cette valeur divise un calcul de géométrie de page, donc le calcul produisait zéro, donc le calcul de boîte englobante divisait par zéro. Un gestionnaire d'exception nu avalait cela, la fabrique d'images renvoyait un résultat nul, et la violation d'accès survenait enfin dans l'arbre de pages quand le null était utilisé. Trois couches entre cause et symptôme, avec un gestionnaire d'exception au milieu qui efface la preuve
La même unité avait deux autres instances du schéma. La classe de police avait des corps Assign et constructeur vides, ce qui compte plus qu'il n'y paraît car la propriété de police du canvas est en lecture seule : affecter dedans est la seule façon de livrer une police, donc une implémentation vide rend la sélection de police silencieusement inefficace et le texte sort avec la police par défaut. Et une valeur de zéro point par pouce faisait produire à tout appelant qui dimensionne un canvas à partir de métriques de police un canvas de zéro sur zéro, ce qui donne une page blanche et un retour de succès
// Le schéma à chercher dans une unité de substitut : une méthode qui ne
// lève rien et ne fait rien. Les deux compilent et les deux produisent
// un « succès » sans aucune sortie
procedure TMetafileCanvasStandIn.Create(...);
begin
// pas d'appel hérité, pas d'initialisation de champs
end;
function TBitmapStandIn.LoadFromStream(Stream: TStream): Boolean;
begin
Result := True; // et le bitmap est toujours vide
end;
La structure large qui ne garde que le premier caractère
Celle-ci n'est pas du tout un problème de substitut, mais elle appartient au même catalogue car le symptôme est également loin de la cause. La structure d'énumération d'imprimantes était déclarée avec ses douze membres chaîne typés comme pointeurs vers des caractères d'un octet, alors que la fonction qui la remplit est la variante caractères larges de l'API d'énumération
Les tailles de pointeur sont identiques, donc la disposition de la structure est correcte et rien ne plante. Ce qui se passe à la place est que lire une chaîne UTF-16 comme chaîne d'un octet s'arrête au premier octet zéro, qui pour tout nom d'imprimante ASCII est la moitié haute du second caractère. Chaque nom d'imprimante revenait avec exactement un caractère. En aval, la validation de nom a échoué, la création d'imprimante a échoué et l'impression a échoué pour chaque imprimante réelle de la machine, et aucun de ces symptômes ne pointe vers une déclaration de structure
// Faux : bonne taille, mauvais type d'élément. Pas d'erreur de compilation, pas de
// plantage, chaque chaîne tronquée à un caractère
type
TPrinterInfo2Wrong = record
pServerName: PAnsiChar;
pPrinterName: PAnsiChar;
// ... dix autres
end;
// Juste : une structure *W a des membres larges partout
type
TPrinterInfo2W = record
pServerName: PWideChar;
pPrinterName: PWideChar;
// ... dix autres
end;
La règle qui en sort est mécanique et vaut la peine d'être appliquée sans réfléchir : pour toute structure Win32 dont le nom se termine par W, vérifier que chaque membre chaîne est la variante large, champ par champ. Mélanger les mondes ANSI et large ne produit ni diagnostic du compilateur ni plantage, seulement une troncature silencieuse, et la même chose s'applique en sens inverse aux variantes ANSI
Un gestionnaire d'exception nu est le véritable adversaire
Chacune de ces enquêtes a été ralentie par le même construit : un gestionnaire qui attrape tout et le convertit en valeur de retour fausse. C'est une chose raisonnable à écrire autour d'un décodeur d'images, puisqu'une image corrompue ne doit pas faire tomber un travail de document. C'est aussi un dispositif pour supprimer le seul élément d'information dont vous avez besoin
La réponse pratique est de rendre le gestionnaire temporairement bruyant. Décharger la classe d'exception, le message et la pile d'appels depuis l'intérieur du gestionnaire nu, sous un conditionnel de débogage, transforme un retour nul inexpliqué en exception nommée avec un emplacement. Dans deux des trois cas ci-dessus, cette seule étape a terminé l'enquête, car l'exception était une division par zéro ou une violation d'accès dans une méthode de substitut dont le nom disait tout
Liste de contrôle pour adopter une voie de substitut
Quatre points, dans l'ordre où ils paient. Avant d'appeler dans une classe de substitution, lisez les méthodes que vous allez utiliser et confirmez que chacune a un vrai corps ; un corps vide n'est pas un détail d'implémentation, c'est une fonctionnalité manquante. Préférez les substituts qui lèvent à ceux qui renvoient des valeurs neutres, et associez-y des vérifications de null aux endroits où une fabrique peut maintenant légitimement ne rien renvoyer. Vérifiez une fonctionnalité en inspectant l'artefact, pas le code de retour, puisque tout le mode de défaillance ici est un code de retour propre sur un artefact vide ; une décomposition au niveau de l'octet de ce que contient réellement un document est la façon la plus rapide de le voir, et l'article sur l'audit de taille de fichier couvre ces outils. Et quand une fonctionnalité n'a aucune implémentation de substitution viable, acheminez les échantillons concernés vers la voie qui fonctionne et dites pourquoi dans un commentaire, plutôt que de laisser une démonstration qui produit discrètement une sortie blanche
Le point plus large s'applique bien au-delà d'une bibliothèque. Toute base de code avec une seconde implémentation conditionnelle, une couche de simulation, un mode headless, un shim de plateforme, est exposée à la forme trois. La raison pour laquelle il se cache si bien est que chaque barrière de qualité sur laquelle une équipe compte normalement, codes de retour, codes d'erreur, exceptions, statuts de sortie, est un canal de statut, et la forme trois les garde tous propres. Seule la sortie le trahit. C'est aussi le raisonnement derrière la vérification des artefacts plutôt que des statuts lors du traitement d'entrées non fiables, décrit dans l'article sur l'analyse de PDF non fiables, et derrière la comparaison des sorties rendues entre moteurs plutôt que d'en faire confiance à un seul, décrit dans le rendu multi-moteurs
PDFlibPas est une bibliothèque PDF native en Object Pascal pour Delphi, C++Builder et Free Pascal, et sa configuration non VCL est ce qui rend possibles les compilations headless et inter-chaînes d'outils ; la couverture actuelle des configurations est listée sur la page produit de la losLab PDF Developer Library