IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Programmation en PERL

de Larry Wall, Tom Christiansen et Jon Orwant


précédentsommairesuivant

III. Technique Perl

15. Unicode

Si vous ne savez pas encore ce qu'est Unicode, vous le saurez bientôt — même si vous ne lisez pas ce chapitre —, car travailler avec Unicode devient indispensable. (Certains pensent que c'est une nécessité maléfique, mais c'est bien davantage une nécessité bénéfique. Dans tous les cas, c'est un mal nécessaire.)

Historiquement, les gens ont construit des jeux de caractères qui reflétaient ce qu'ils devaient faire dans le contexte de leur propre culture. Comme les gens de toutes les cultures sont naturellement paresseux, ils ont eu tendance à n'inclure que les symboles dont ils avaient besoin en excluant ceux qui ne leur étaient pas utiles. Cela fonctionnait à merveille aussi longtemps que nous ne communiquions qu'avec d'autres gens appartenant à notre propre culture, mais maintenant que nous commençons à utiliser l'Internet pour des communications interculturelles, nous nous heurtons à des problèmes du fait de cette approche exclusive. Il est déjà difficile d'imaginer comment taper des caractères accentués sur un clavier américain. Comment dans le monde (littéralement) peut-on écrire une page web multilingue ?

Unicode est la solution, ou tout au moins une partie de la solution (voir également XML). Unicode est un jeu de caractères inclusif plutôt qu'exclusif. Alors que les gens peuvent chicaner, et ne s'en privent pas, sur les divers détails d'Unicode (et il y a une grande quantité de détails sur lesquels chicaner), l'intention générale est de contenter tout le monde de manière suffisante(124) avec Unicode pour qu'ils utilisent de leur propre gré Unicode comme le médium international d'échange de données textuelles. Personne ne vous force à utiliser Unicode, tout comme personne ne vous force à lire ce chapitre (du moins, nous l'espérons). Les gens pourront toujours utiliser leurs anciens jeux de caractères exclusifs dans leur propre culture. Mais dans ce cas (comme on dit), la portabilité en souffrira.

La loi de la conservation de la souffrance dit que si nous réduisons la souffrance quelque part, elle doit augmenter autre part. Dans le cas d'Unicode, nous devons souffrir de la migration d'une sémantique d'octet vers une sémantique de caractère. Puisque, par un accident de l'histoire, il a été inventé par un Américain, Perl a historiquement confondu les notions d'octet et de caractère. En migrant vers Unicode, Perl ne doit plus les confondre d'une manière ou d'une autre.

Paradoxalement, en amenant Perl à ne plus confondre les octets et les caractères, nous permettons aux programmeurs Perl de les confondre, car ils savent que Perl rétablira la distinction, exactement comme ils peuvent confondre les nombres et les chaînes et s'en remettre à Perl pour convertir autant que nécessaire les uns vers les autres et réciproquement. Autant que possible, Perl a une approche d'Unicode semblable à son approche de tout le reste : Faire Ce Qu'il Faut Faire. Idéalement, nous aimerions parvenir à ces quatre objectifs :

Objectif n° 1

  • Les anciens programmes orientés octet ne devraient pas spontanément se planter avec les anciennes données orientées octet avec lesquelles ils travaillaient.

Objectif n° 2

  • Les anciens programmes orientés octet devraient par magie commencer à fonctionner avec les nouvelles données orientées caractère lorsque c'est approprié.

Objectif n° 3

  • Les programmes devraient tourner aussi vite avec les nouvelles données orientées caractère qu'avec les anciennes données orientées octet.

Objectif n° 4

  • Perl devrait rester un langage unifié plutôt que se scinder en un Perl orienté octet et un Perl orienté caractère.

Pris ensemble, ces objectifs sont pratiquement impossibles à atteindre. Mais nous nous en approchons relativement bien. Ou plutôt, nous sommes toujours dans le processus de rapprochement, puisqu'il s'agit d'une tâche en cours de réalisation. Perl continue d'évoluer en même temps qu'Unicode. Mais notre plan d'envergure est d'offrir une voie de migration qui soit sûre et qui puisse nous amener là où nous voulons aller en rencontrant le minimum d'accidents en cours de route. Comment y arriver est le sujet du prochain paragraphe.

15-1. Construction d'un caractère

Dans les versions de Perl antérieures à 5.6, toutes les chaînes étaient vues en tant que séquences d'octets. Dans les versions 5.6 et suivantes, une chaîne peut toutefois contenir des caractères d'une taille supérieure à un octet. Nous voyons maintenant les chaînes, non plus en tant que séquences d'octets, mais en tant que séquences de numéros compris dans l'intervalle 0 .. 2**32-1 (ou dans le cas d'ordinateurs 64 bits, 0 .. 2**64-1). Ces numéros représentent des caractères abstraits et, dans un certain sens, plus grand est le numéro, plus la taille du caractère est grande ; mais au contraire de beaucoup de langages, Perl n'est pas lié à une taille particulière de la représentation d'un caractère. Perl utilise un encodage de taille variable (basé sur UTF8), ainsi ces numéros de caractères abstraits peuvent être empaquetés, ou non, à raison d'un numéro par octet. Évidemment, le numéro de caractère 18 446 744 073 709 551 515 (c'est-à-dire, « \x{ffff_ffff_ffff_ffff} ») ne tiendra jamais dans un octet (en fait, il prend 13 octets), mais si tous les caractères de votre chaîne se situent dans l'intervalle 0..127 en décimal, alors ils seront certainement empaquetés à raison d'un par octet, puisque l'UTF8 est identique à l'ASCII pour les sept premiers bits de poids faible.

Perl utilise de l'UTF8 lorsqu'il pense que c'est bénéfique, ainsi, si tous les caractères de votre chaîne sont dans l'intervalle 0..255, il y a une grande chance que les caractères soient empaquetés dans des octets — mais en l'absence d'autres informations connues, vous ne pouvez être sûr, car Perl fait en interne une conversion entre les caractères 8 bits et les caractères UTF8 de taille variable si nécessaire. L'essentiel est que vous ne devriez pas vous en soucier la plupart du temps, car la sémantique de caractère est préservée à un niveau abstrait indépendamment de la représentation.

Dans tous les cas, si votre chaîne contient des numéros de caractères supérieurs à 255 en décimal, la chaîne est certainement stockée en UTF8. Plus exactement, elle est stockée dans la version étendue d'UTF8 de Perl que nous appelons utf8, en hommage au pragma du même nom, mais surtout parce que c'est plus simple à taper. (Et de plus, l'UTF8 « réel » n'est autorisé à contenir que des numéros de caractères consacrés par le Consortium Unicode. L'utf8 de Perl est autorisé à contenir tous les caractères dont vous avez besoin pour accomplir votre travail. Perl se moque du fait que vos numéros de caractères soient officiellement corrects ou juste corrects.)

Nous avons dit que vous ne devriez pas vous en soucier la plupart du temps, mais les gens aiment de toute façon se faire du souci. Supposez que vous ayez une v-chaîne représentant une adresse IPv4 :

 
Sélectionnez
$adr_loc = v127.0.0.1;     # Sûrement stockée en octets.
$oreilly = v204.148.40.9;  # Pourrait être stockée en octets ou en utf8.
$adr_nok = v2004.148.40.9; # Sûrement stockée en utf8.

Tout le monde peut s'apercevoir que $adr_nok ne marchera pas en tant qu'adresse IP. Il est donc facile de penser que si l'adresse réseau d'O'Reilly est forcée à une représentation UTF8, elle ne marchera plus. Mais les caractères dans la chaîne sont des numéros abstraits, non des octets. Tout ce qui utilise une adresse IPv4, comme la fonction gethostbyaddr, devrait automatiquement forcer les numéros de caractères abstraits à être reconvertis vers une représentation en octets (et échouer avec $adr_nok).

Les interfaces entre Perl et le monde réel doivent s'accommoder des détails de la représentation. Jusqu'à un certain point, les interfaces existantes essaient de faire ce qu'il faut sans que vous ayez à leur dire quoi faire. Mais de temps en temps, vous devez donner des instructions à certaines interfaces (comme la fonction open) et si vous écrivez votre propre interface vers le monde réel, elle aura besoin d'être soit assez astucieuse pour arriver à comprendre les choses par elle-même, soit au moins assez élégante pour suivre les instructions lorsque vous voulez qu'elle se comporte différemment de ce qu'elle aurait fait par défaut.(125)

Puisque Perl se soucie de maintenir une sémantique de caractère transparente à l'intérieur du langage lui-même, la seule place où vous devez vous inquiéter d'une sémantique d'octet, opposée à une sémantique de caractère, est dans vos interfaces. Par défaut, toutes vos anciennes interfaces Perl vers le monde extérieur sont orientées octet. C'est-à-dire qu'à un niveau abstrait, toutes vos chaînes sont des séquences de numéros dans l'intervalle 0..255, ainsi, si rien dans le programme ne les force à des représentations en utf8, vos anciens programmes continueront à fonctionner avec des données orientées octet comme ils le faisaient auparavant. Vous pouvez donc cocher l'objectif n° 1 ci-dessus.

Si vous voulez que vos anciens programmes fonctionnent avec les nouvelles données orientées caractère, vous devez marquer vos interfaces orientées caractère de manière que Perl s'attende à recevoir des données orientées caractère de ces interfaces. Une fois que c'est fait, Perl devrait automatiquement faire toutes les conversions nécessaires pour préserver l'abstraction des caractères. La seule différence est que vous devez introduire dans votre programme des chaînes qui soient marquées comme contenant potentiellement des caractères plus grands que 255, ainsi, si vous effectuez une opération entre une chaîne d'octets et une chaîne utf8, Perl convertira en interne la chaîne d'octets en une chaîne utf8 avant d'effectuer l'opération. Généralement, les chaînes utf8 ne sont reconverties en chaînes d'octets que lorsque vous les envoyez à une interface orientée octet, auquel cas, si vous aviez une chaîne contenant des caractères plus grands que 255, vous aurez un problème qui peut être résolu de diverses manières selon l'interface en question. Vous pouvez donc cocher l'objectif n° 2.

Parfois, vous voulez mélanger du code comprenant une sémantique de caractère avec du code devant tourner avec une sémantique d'octet, comme du code d'entrées/sorties qui lit ou écrit des blocs de taille fixe. Dans ce cas, vous pouvez mettre une déclaration use bytes autour du code orienté octet pour le forcer à utiliser une sémantique d'octet même sur les chaînes marquées en tant que chaînes utf8. Les conversions sont alors sous votre responsabilité. Mais c'est un moyen de renforcer une lecture locale plus stricte de l'objectif n° 1, aux dépens d'une lecture globale plus relâchée de l'objectif n° 2.

L'objectif n° 3 a largement été atteint, en partie en faisant des conversions paresseuses entre les représentations en octets et en utf8 et en partie en étant discret sur la manière dont nous avons implémenté potentiellement des fonctionnalités lentes d'Unicode, comme la recherche des propriétés de caractères dans des tables énormes.

L'objectif n° 4 a été atteint en sacrifiant un peu de la compatibilité des interfaces à la quête des autres objectifs. D'une certaine façon, nous n'avons pas bifurqué vers deux Perl différents ; mais en le considérant d'une autre manière, la révision 5.6 de Perl est une version bifurquée de Perl au regard des versions antérieures et nous ne nous attendons pas à ce que les gens migrent depuis leurs versions antérieures avant d'être sûrs que la nouvelle version fera bien ce qu'ils veulent. Mais c'est toujours le cas avec les nouvelles versions, nous nous permettrons donc de cocher également l'objectif n° 4.

15-2. Conséquences de la sémantique de caractère

Le résultat de tout ceci est qu'un opérateur interne typique manipulera des caractères sauf s'il est dans la portée d'un pragma use bytes. Toutefois, même en dehors de la portée d'un use bytes, si tous les opérandes de l'opérateur sont stockés en tant que caractères 8 bits (c'est-à-dire qu'aucun opérande n'est stocké en tant qu'utf8), alors la sémantique de caractère se confond avec la sémantique d'octet et le résultat de l'opérateur sera stocké en interne dans un format 8 bits. Ceci préserve une compatibilité antérieure pourvu que vous n'approvisionniez pas vos programmes avec des caractères plus larges qu'en Latin-1.

Le pragma utf8 est avant tout un dispositif de compatibilité permettant la reconnaissance de l'UTF8 dans les littéraux et les identificateurs rencontrés par l'analyseur grammatical. Il peut également être utilisé pour permettre certaines fonctionnalités des plus expérimentales d'Unicode. Notre objectif à long terme est de changer le pragma utf8 en une opération ne faisant rien (no-op).

Le pragma use bytes ne sera jamais changé en une opération ne faisant rien. Non seulement il est nécessaire pour le code orienté octet, mais un de ses effets de bords est également de définir des enrobages orientés octet autour de certaines fonctions pour les utiliser en dehors d'une portée de use bytes. Au moment où nous écrivons ceci, le seul enrobage défini est pour la fonction, length mais d'autres sont susceptibles de l'être au fil du temps. Pour utiliser cet enrobage, écrivez :

 
Sélectionnez
use bytes (); # Charge les enrobages sans importer la sémantique d'octet.
...
$lg_car = length("\x{ffff_ffff}");               # Renvoie 1.
$lg_oct = bytes::length("\x{ffff_ffff}"); # Renvoie 7.

En dehors de la portée d'une déclaration use bytes, la version 5.6 de Perl fonctionne (ou du moins, est censée fonctionner) ainsi :

  • Les chaînes et les motifs peuvent maintenant contenir des caractères ayant une valeur ordinale supérieure à 255 :
 
Sélectionnez
use utf8;
$convergence = "☞ ☜";
  • En supposant que vous ayez un éditeur compatible avec Unicode pour éditer votre programme, de tels caractères se présenteront généralement directement à l'intérieur de chaînes littérales en tant que caractères UTF8. Pour le moment, vous devez déclarer un use utf8 au début de votre programme pour permettre l'utilisation de l'UTF8 dans les littéraux.
    Si vous ne disposez pas d'éditeur Unicode, vous pouvez toujours spécifier un caractère particulier en ASCII avec une extension de la notation \x. Un caractère dans l'intervalle Latin-1 peut être écrit comme \x{ab} ou comme, \xab mais si le numéro dépasse les deux chiffres hexadécimaux, vous devez utiliser des accolades. Les caractères Unicode sont spécifiés en mettant le code hexadécimal à l'intérieur des accolades après le \x. Par exemple, un smiley Unicode vaut \x{263A}. Il n'y a pas de construction syntaxique en Perl présumant que les caractères Unicode fassent exactement 16 bits, vous ne pouvez donc pas utiliser \u263A comme dans certains langages ; \x{263A} est l'équivalent le plus proche.
    Pour insérer des caractères nommés via \N{NOM_CAR}, voir le pragma use charnames au chapitre 31, Modules de pragmas.
  • Les identificateurs à l'intérieur du script Perl peuvent contenir des caractères Unicode alphanumériques, y compris des idéogrammes :

     
    Sélectionnez
    use utf8;
    $++; # Un enfant est né.
  • Ici encore, use utf8 est nécessaire (pour l'instant) pour reconnaître de l'UTF8 dans votre script. Vous êtes actuellement livré à vous-même lorsqu'il s'agit d'utiliser les formes canoniques des caractères — Perl ne tente pas (encore) de canoniser pour vous les noms de variable. Voir www.unicode.org pour le dernier rapport sur la canonisation.

  • Les expressions régulières correspondent à des caractères au lieu d'octets. Par exemple un point correspond à un caractère au lieu de correspondre à un octet. Si le Consortium Unicode arrive même à approuver l'emploi du twengar, alors (malgré le fait que de tels caractères sont représentés avec 4 octets en UTF8), ceci réussit la correspondance :

     
    Sélectionnez
    "\N{TENGWAR LETTER SILME NUQUERNA}" =~ /^.$/
  • Le motif \C est fourni pour forcer une correspondance à un seul octet (« char » en C, d'où \C). Utilisez \C avec prudence, puisqu'il peut désynchroniser des caractères dans votre chaîne et vous pouvez obtenir des erreurs « Caractère UTF8 mal formé ». Vous ne pouvez pas utiliser \C dans des crochets, puisqu'il ne représente aucun caractère particulier ni aucun ensemble de caractères.

  • Les classes de caractères dans les expressions régulières correspondent à des caractères au lieu d'octets et correspondent avec les propriétés des caractères spécifiées dans la base de données des propriétés Unicode. Ainsi, \w peut être utilisé pour correspondre à un idéogramme :
 
Sélectionnez
"⋏" =~ /\w/;
  • Les propriétés nommées d'Unicode et les intervalles de blocs peuvent être utilisés comme classes de caractères avec les nouvelles constructions \p (correspond à la propriété) et \P (ne correspond pas à la propriété). Par exemple, \p{Lu} correspond à tout caractère ayant la propriété majuscule d'Unicode, alors que \p{M} correspond à tout caractère marqué. Les propriétés d'une seule lettre peuvent omettre les accolades, ainsi \pM peut également correspondre à des caractères marqués. Beaucoup de classes de caractères prédéfinies sont disponibles, comme \p{IsMirrored} et \p{InTibetan} :

     
    Sélectionnez
    "\N{greek:Iota}" =~ /p{Lu}/
  • Vous pouvez également utiliser \p et \P à l'intérieur des crochets des classes de caractères. (Dans la version 5.6.0 de Perl, vous avez besoin de faire use utf8 pour que les propriétés des caractères fonctionnent bien. Cette restriction sera levée à l'avenir.) Voir le chapitre 5, Correspondance de motifs, pour plus de détails sur la correspondance de propriétés Unicode.

  • Le motif spécial \X correspond à toute séquence Unicode étendue (une « séquence de caractères combinés » en langage standard), où le premier caractère est un caractère de base et les suivants des caractères marqués s'appliquant au caractère de base. C'est équivalent à (?:\PM\pM*) :

     
    Sélectionnez
    "o\N{COMBINING TILDE BELOW}" =~ /\X/
  • Vous ne pouvez pas utiliser \X à l'intérieur de crochets, car il pourrait correspondre à plusieurs caractères et il ne représente aucun caractère particulier ni aucun ensemble de caractères.

  • L'opérateur tr/// traduit des caractères au lieu d'octets. Pour changer tous les caractères hors de l'intervalle Latin-1 en points d'interrogation, vous pourriez écrire :

     
    Sélectionnez
    tr/\0-\x{10ffff}/\0-\xff?/; # caractère utf8 vers latin1
  • Les opérateurs de traduction de casse utilisent des tables de traduction de casse Unicode lorsqu'on leur fournit des caractères en entrée. Remarquer que uc traduit en majuscules, alors qu'ucfisrt traduit l'initiale en majuscule de titre (titlecase), pour les langues qui font la distinction. Naturellement les séquences d'antislashs correspondantes ont la même sémantique :

     
    Sélectionnez
    $x = "\u$mot";  # met l'initiale de $mot en majuscule de titre
    $x = "\U$mot";  # met $mot en majuscules
    $x = "\l$mot";  # met l'initiale de $mot en minuscule
    $x = "\L$mot";  # met $mot en minuscules

    Soyez prudent, car les tables de traduction de casse Unicode ne tentent pas de fournir des tables de correspondance bijectives pour chaque instance, particulièrement pour les langues utilisant un nombre de caractères différent pour les majuscules et les minuscules. Comme ils disent dans les standards, alors que les propriétés de casse sont elles-mêmes normatives, les tables de casse ne sont qu'informelles.

  • La plupart des opérateurs manipulant des positions ou des longueurs dans une chaîne basculeront automatiquement à l'utilisation de positions de caractères, ce qui comprend : chop, substr, pos, index, rindex, sprintf, write et length. Les opérateurs qui ne basculent pas délibérément comprennent vec, pack et unpack. Les opérateurs qui s'en moquent vraiment comprennent chomp, ainsi que tout autre opérateur traitant une chaîne comme un paquet de bits, tels que le sort par défaut et les opérateurs manipulant des fichiers.

     
    Sélectionnez
    use bytes;
    $lg_octets = length("Je fais de l'合 氣 道"); # 15 octets
    no bytes;
    $lg_caracs = length("Je fais de l'合 氣 道"); # mais 9 caractères
  • Les lettres « c » et « C » de pack/unpack ne changent pas, puisqu'elles sont souvent utilisées pour des formats orientés octet. (Encore une fois, pensez « char » en langage C.) Toutefois, il existe un nouveau spécificateur « U », qui fera les conversions entre les caractères UTF8 et les entiers :

     
    Sélectionnez
    pack("U*", 1, 20, 300, 4000) eq v1.20.300.4000
  • Les fonctions chr et ord fonctionnent avec des caractères :

     
    Sélectionnez
    chr(1).chr(20).chr(300).chr(4000) eq v1.20.300.4000
  • En d'autres termes, chr et ord sont comme pack("U") et unpack("U") et non comme pack("C") et unpack("C"). En fait, ces derniers sont un moyen pour vous d'émuler les fonctions chr et ord orientées octet si vous êtes trop paresseux pour faire un use bytes.

  • Enfin, un reverse en contexte scalaire inverse par caractère plutôt que par octet :
 
Sélectionnez
"☞ ☜" eq reverse "☜ ☞"

Si vous regardez dans le répertoire CHEMIN_BIBLIOTHEQUE_PERL/unicode, vous trouverez de nombreux fichiers en rapport avec la définition des sémantiques ci-dessus. La base de données des propriétés Unicode provenant du Consortium Unicode se situe dans un fichier appelé Unicode.300 (pour Unicode 3.0). Ce fichier a déjà été traité par mktables.PL pour donner un tas de petits fichiers .pl dans le même répertoire (et dans les sous-répertoires Is/, In/ et To/), dont certains sont automatiquement avalés par Perl pour implémenter des choses comme \p (voir les répertoires Is/ et In/) et uc (voir le répertoire To/). Les autres fichiers sont avalés par des modules comme le pragma use charnames (voir Name.pl). Mais au moment où nous écrivons ceci, il reste encore de nombreux fichiers qui sont juste assis là sans rien faire, en attendant que vous leur écriviez un module d'accès :

  • ArabLink.pl
  • ArabLnkGrp.pl
  • Bidirectional.pl
  • Block.pl
  • Category.pl
  • CombiningClass.pl
  • Decomposition.pl
  • JamoShort.pl
  • Number.pl
  • To/Digit.pl

Un résumé bien plus lisible d'Unicode, avec beaucoup de liens hypertextes, se trouve dans CHEMIN_BIBLIOTHEQUE_PERL/unicode/Unicode3.html.

Remarquez que lorsque le Consortium Unicode sort une nouvelle version, certains de ces noms de fichier sont susceptibles de changer, vous devrez donc fouiller un peu. Vous pouvez trouver CHEMIN_BIBLIOTHEQUE_PERL avec l'incantation suivante :

 
Sélectionnez
% perl -MConfig -le 'print $Config{privlib}'

Pour trouver tout ce que l'on peut trouver sur Unicode, vous devriez consulter The Unicode Standard Version 3.0 (ISBN 0-201-61633-5).

15-3. Attention ⋏ au travail

Au moment où nous écrivons ceci (c'est-à-dire, en phase avec la version 5.6.0 de Perl), il existe encore des précautions à prendre pour utiliser Unicode. (Consultez votre documentation en ligne pour les mises à jour.)

  • Le compilateur d'expressions régulières existant ne produit pas de codes d'opérations (opcodes) polymorphiques. Cela signifie que le fait de déterminer si un motif particulier correspondra ou non à des caractères Unicode est réalisé lorsque le motif est compilé (basé sur le fait que le motif contient ou non des caractères Unicode) et non lorsque la correspondance arrive à l'exécution. Ceci doit être changé pour correspondre de manière adaptée à de l'Unicode si la chaîne sur laquelle se fait la correspondance est en Unicode.
  • Il n'y a actuellement pas de moyen facile de marquer une donnée lue depuis un fichier ou depuis toute autre source externe comme étant en utf8. Ceci sera un domaine sur lequel se concentrer dans un futur proche et ce sera probablement déjà corrigé au moment où vous lirez ceci.
  • Il n'y a pas de méthode pour forcer automatiquement l'entrée et la sortie vers d'autres encodages que UTF8. Ceci est toutefois prévu dans un futur proche, consultez donc votre documentation en ligne.
  • L'utilisation de locales avec utf8 peut conduire à d'étranges résultats. Actuellement, il existe quelques tentatives pour appliquer l' information d'une locale 8 bits à des caractères dans l'intervalle, 0..255 mais ceci est de façon évidente incorrect pour des locales qui utilisent des caractères au-delà de cet intervalle (lorsqu'ils sont convertis en Unicode). Cela aura également tendance à tourner plus lentement. Le bannissement des locales est fortement encouragé.

L'Unicode est « fun » — il faut juste que vous définissiez le fun correctement.

16. Communication interprocessus

Les processus peuvent communiquer de presque autant de façons que les personnes. Mais les difficultés de la communication interprocessus ne devraient pas être sous-estimées. Il ne vous sert à rien d'attendre une réplique verbale si votre partenaire n'utilise que le langage des signes. De même, deux processus ne peuvent se parler que lorsqu'ils s'accordent sur la méthode de communication et sur les conventions bâties sur cette méthode. Comme avec tout type de communication, l'éventail des conventions sur lesquelles s'accorder va du lexical au pragmatique : tout, depuis quel jargon vous utiliserez jusqu'au choix de celui dont c'est le tour de parler. Ces conventions sont nécessaires, car il est très difficile de communiquer avec une sémantique pure, dépouillée de tout contexte.

Dans notre jargon, la communication interprocessus s'épelle habituellement IPC (Inter-Process Communication). Les fonctionnalités d'IPC de Perl vont du très simple au très complexe. Votre choix dépend de la complexité des informations à communiquer. La forme la plus simple d'information est, d'une certaine manière, aucune information, sinon l'avertissement qu'un certain événement s'est produit à un certain moment. En Perl, ces événements sont communiqués par un mécanisme de signal inspiré de celui du système UNIX.

À l'autre extrême, les fonctionnalités de socket de Perl permettent de communiquer avec n'importe quel autre processus de l'Internet grâce à n'importe quel protocole mutuellement supporté. Cette liberté a bien sûr un prix : vous devez parcourir un certain nombre d'étapes pour mettre en place la connexion et vous assurer que vous parlez le même langage que le processus à l'autre bout. Ce qui peut vous forcer à adhérer à d'étranges coutumes, selon les conventions culturelles en usage. Il se peut même que pour être correct au niveau du protocole, vous deviez même vous mettre à parler un langage comme XML, Java ou Perl. Quelle horreur !

Entre ces deux extrêmes, on trouve des fonctionnalités intermédiaires conçues à l'origine pour faire communiquer des processus sur la même machine. Il s'agit notamment des bons vieux fichiers, des pipes (ou tubes), des FIFO et les divers appels système d'IPC System V. L'implémentation de ces fonctionnalités varie selon les plates-formes ; les systèmes Unix modernes (y compris le MAC OS X d'Apple) devraient toutes les implémenter et, mis à part les signaux et les IPC System V, la plupart des autres sont implémentées sur les systèmes d'exploitation récents de Microsoft, y compris les pipes, les forks, les verrouillages de fichiers et les sockets.(126)

Vous pouvez trouver de plus amples informations sur le portage en général dans l'ensemble de documentations standard de Perl (dans n'importe quel format affiché par votre système) sous perlport. Les informations spécifiques à Microsoft peuvent être trouvées sous perlwin32 et perlfork, qui sont installés même sur des systèmes non Microsoft. Pour les livres publiés, nous vous suggérons les suivants :

  • Perl en action, par Tom Christiansen et Nathan Torkington (O'Reilly Associates, 1998), chapitres 16 à 18.
  • Advanced Programming in the UNIX Environment, par W. Richard Stevens (Addison-Wesley, 1992).
  • TCP/IP Illustrated, par W. Richard Stevens, Volumes I-III (Addison-Wesley, 19921996).

16-1. Signaux

Perl utilise un modèle simple de gestion des signaux : le hachage %SIG contient des références (qu'elles soient symboliques ou en dur) à des gestionnaires de signaux définis par l'utilisateur. Certains événements entraînent le système d'exploitation à délivrer un signal au processus affecté. Le gestionnaire correspondant à cet événement est appelé avec un argument contenant le nom du signal qui l'a déclenché. Pour envoyer un signal à un autre processus, utilisez la fonction kill. Pensez-y comme à une information d'un bit envoyée à l'autre processus.(127) Si ce processus a installé un gestionnaire de signal pour le signal en question, il peut exécuter du code lorsqu'il le reçoit. Mais il n'y a aucun moyen pour le processus émetteur d'obtenir une quelconque sorte de valeur de retour, autre que de savoir si le signal a été légitimement envoyé. L'émetteur ne reçoit aucun retour lui disant ce que le processus récepteur a fait du signal, s'il en a fait quelque chose.

Nous avons classé cette fonctionnalité dans les IPC alors qu'en fait, les signaux peuvent provenir de diverses sources et pas seulement d'autres processus. Le signal pourrait également provenir du même processus, ou bien il pourrait être généré lorsque l'utilisateur tape au clavier une séquence particulière comme Ctrl-C ou Ctrl-Z, ou il pourrait être fabriqué par le noyau quand certains événements se produisent, comme la fin d'un processus fils, ou lorsque le processus n'a plus assez d'espace sur la pile, ou parce qu'un fichier a atteint sa taille limite en mémoire. Mais votre propre processus ne sait pas distinguer facilement ces différents cas. Un signal ressemble à un paquet qui arrive mystérieusement sur le pas de la porte sans l'adresse de l'expéditeur. Vous feriez mieux d'être prudent au moment de l'ouvrir.

Puisque les entrées dans le hachage %SIG peuvent être des références en dur, une technique courante est d'utiliser des fonctions anonymes pour les gestionnaires de signaux simples :

 
Sélectionnez
$SIG{INT}  = sub { die "\nIci\n" };
$SIG{ALRM} = sub { die "Votre minuterie a expiré" };

Ou vous pourriez créer une fonction nommée et affecter son nom ou une référence à l'entrée appropriée du hachage. Par exemple, pour intercepter les signaux d'interruption et de sortie (souvent associés aux touches Ctrl-C et Ctrl-\ sur votre clavier), mettez en place un gestionnaire comme ceci :

 
Sélectionnez
sub intercepte {
    my $nom_sig = shift;
    our $ben_voyons++;
    die "Quelqu'un m'a envoyé un SIG$nom_sig !";
}
$ben_voyons = 0;
$SIG{INT} = 'intercepte';  # signifie toujours &main::intercepte
$SIG{INT} = \&intercepte;  # la meilleure stratégie
$SIG{QUIT} = \&intercepte; # en intercepte également un autre

Remarquez que tout ce que nous faisons dans le gestionnaire de signaux est de positionner une variable globale et de lever une exception avec die. Évitez autant que possible de faire quelque chose de plus compliqué que cela, car sur la plupart des systèmes, la bibliothèque C n'est pas réentrante. Les signaux sont délivrés de manière asynchrone,(128) ainsi, l'appel à une quelconque fonction print (voire à tout ce qui a besoin d'allouer plus de mémoire avec malloc(3)) pourrait en théorie déclencher une erreur de mémoire et engendrer un core dump si vous étiez déjà dans une routine apparentée de la bibliothèque C lorsque le signal a été délivré. (Même la routine die est quelque peu dangereuse sauf si le processus est exécuté à l'intérieur d'un eval, ce qui supprime les entrées/ sorties depuis die et par conséquent empêche d'appeler la bibliothèque C. Probablement.)

Une manière encore plus simple d'intercepter les signaux est d'utiliser le pragma sigtrap pour installer de simples gestionnaires de signaux par défaut :

 
Sélectionnez
use sigtrap qw(die INT QUIT);
use sigtrap qw(die untrapped normal-signals
    stack-trace any error-signals);

Le pragma est très utile lorsque vous ne voulez pas vous embêter à écrire votre propre gestionnaire, mais que vous voulez toujours intercepter les signaux dangereux et tout fermer dans les règles. Par défaut, certains de ces signaux ont un caractère si fatal pour votre processus que votre programme s'arrêtera en pleine course lorsqu'il en recevra un. Malheureusement, cela signifie que les fonctions END gérant la terminaison du programme et les méthodes DESTROY s'occupant de l'achèvement d'un objet ne seront pas appelées. Mais elles le seront sur des exceptions Perl ordinaires (comme lorsque vous appelez die), vous pouvez donc utiliser ce pragma pour convertir sans douleur les signaux en exceptions. Même si vous ne manipulez pas de signaux vous-même, votre programme devra bien se comporter quand même. Voir la description de use sigtrap au Chapitre 31, Modules de pragmas, pour beaucoup plus de fonctionnalités de ce pragma.

Vous pouvez également choisir d'affecter au gestionnaire de %SIG les chaînes « IGNORE » ou « DEFAULT », auquel cas Perl essayera d'écarter le signal ou permettra que se produise l'action par défaut pour ce signal (bien que certains signaux ne peuvent être ni interceptés ni ignorés, comme les signaux KILL et STOP ; voir signal(3), s'il est implémenté, pour obtenir une liste des signaux disponibles sur votre système et leur comportement par défaut.

Le système d'exploitation voit les signaux comme des nombres plutôt que comme des noms, mais Perl, comme la plupart des gens, préfère les noms symboliques aux nombres magiques. Pour trouver les noms des signaux, passez en revue les clefs du hachage %SIG ou utilisez la commande kill -l si vous en disposez sur votre système. Vous pouvez également utiliser le module standard Config pour déterminer la correspondance que fait votre système entre les noms et les numéros des signaux. Voir Config(3) pour des exemples.

Puisque %SIG est un hachage global, les affections que vous y effectuez se répercutent sur tout votre programme. Il est souvent plus respectueux pour le reste de votre programme de confiner l'interception des signaux à une portée réduite. Faites cela avec une affectation en local du gestionnaire de signaux, ce qui n'a plus d'effet une fois que l'on sort du bloc l'encadrant. (Mais souvenez-vous que les valeurs avec local sont visibles depuis les fonctions appelées depuis ce bloc.)

 
Sélectionnez
{
    local $SIG{INT} = 'IGNORE';
    ...     # Faites ce que vous voulez ici, en ignorant tous les SIGINT.
    fn();   # Les SIGINT sont également ignorés dans fn() !
    ...     # Et ici.
}           # La fin du bloc restaure la valeur précédente de $SIG{INT}.
fn();       # Les SIGINT ne sont pas ignorés dans fn() (a priori).
16-1-a. Envoi de signaux à des groupes de processus

Les processus (du moins sous Unix) sont organisés en groupes de processus, correspondant généralement à une tâche complète. Par exemple, lorsque vous lancez une seule commande shell consistant en une série de filtres envoyant les données de l'un à l'autre à travers des pipes, ces processus (et leurs fils) appartiennent au même groupe. Ce groupe de processus possède un numéro correspondant au numéro du processus leader de ce groupe. Si vous envoyez un signal à un numéro de processus positif, il n'est transmis qu'à ce processus ; mais si vous l'envoyez à un numéro négatif, le signal est transmis à tous les processus dont le numéro de groupe est le nombre positif correspondant, c'est-à-dire le numéro de processus du leader de ce groupe. (De façon bien pratique, pour le leader du groupe, l'ID du groupe de processus est simplement $$.)

Imaginez que votre programme veuille envoyer un signal « hang-up » à tous ses processus fils, ainsi qu'à tous les petits-fils lancés par ces fils, ainsi qu'à tous les arrière-petits-fils lancés par ces petits-fils, ainsi qu'à tous les arrière-arrière-petits-fils lancés par ces arrière-petits-fils, et ainsi de suite. Pour faire cela, votre programme appelle d'abord setpgrp(0,0) pour devenir le leader d'un nouveau groupe de processus et tous les processus qu'il créera ensuite feront partie de ce nouveau groupe. Il importe peu que ces processus soient démarrés manuellement via fork ou automatiquement via des open sur des pipes ou encore en tant que tâches d'arrière-plan avec system("cmd &"). Même si ces processus ont leurs propres fils, l'envoi d'un signal « hang-up » au groupe de processus entier les trouvera tous (sauf pour les processus qui ont positionné leur propre groupe de processus ou changé leur UID pour s'autoaccorder une immunité diplomatique envers vos signaux).

 
Sélectionnez
{
    local $SIG{HUP} = 'IGNORE';   # m'exempte
    kill(HUP, -$$);               # envoie le signal à mon propre
                                  # groupe de processus
}

Un autre signal intéressant est le signal numéro 0. En fait, ce dernier n'affecte pas le processus cible, mais vérifie plutôt s'il est toujours en vie et s'il n'a pas changé son UID. C'est-à-dire qu'il vérifie s'il est légal de lui envoyer un signal sans le faire effectivement.

 
Sélectionnez
unless (kill 0 => $pid_fils) {
    warn "quelque chose d'affreux est arrivé à $pid_fils";
}

Le signal numéro 0 est le seul qui fonctionne de la même façon sous les portages sur Microsoft comme il le fait sous Unix. Sur les systèmes Microsoft, kill ne délivre pas effectivement de signal. À la place, il force le processus fils à se terminer avec le statut indiqué par le numéro de signal. Il se peut que cela soit corrigé un jour. Cependant, le signal magique 0 a toujours le comportement par défaut, non destructeur.

16-1-b. Fossoyage des zombies

Lorsqu'un processus se termine, son père reçoit un signal CHLD envoyé par le noyau et le processus devient un zombie(129) jusqu'à ce que le parent appelle wait ou waitpid. Si vous démarrez un autre processus en Perl en utilisant n'importe quoi d'autre que fork, Perl s'occupe de fossoyer vos fils zombies, mais si vous utilisez un fork pur, vous êtes censés nettoyer derrière vous. Sur la plupart des noyaux, mais pas tous, une bidouille simple pour fossoyer automatiquement les zombies consiste à positionner $SIG{CHLD} à 'IGNORE'. Une approche plus souple (mais fastidieuse) est de les fossoyer vous-mêmes. Comme il se peut qu'il y ait plus d'un fils qui soit mort avant que vous ne vous en occupiez, vous devez rassembler vos zombies dans une boucle jusqu'à ce qu'il n'y en ait plus :

 
Sélectionnez
use POSIX ":queue";
sub FOSSOYEUR { 1 until (waitpid(-1, WNOHANG) == -1) }

Pour lancer ce code comme il se faut, vous pouvez soit positionner un gestionnaire pour le signal CHLD :

 
Sélectionnez
$SIG{CHLD} = \&FOSSOYEUR;

ou, si vous êtes dans une boucle, arrangez-vous simplement pour appeler le fossoyeur de temps à autre. Ceci est la meilleure approche, car elle n'est pas sujette au core dump occasionnel que les signaux peuvent parfois déclencher dans la bibliothèque C. Toutefois, elle s'avère coûteuse si on l'appelle dans une boucle concise, un compromis raisonnable consiste donc à utiliser une stratégie hybride dans laquelle vous minimisez le risque à l'intérieur du gestionnaire en en faisant le moins possible et en attendant sur l'extérieur pour fossoyer les zombies :

 
Sélectionnez
our $zombies = 0;
$SIG{CHLD} = sub { $zombies++ };
sub fossoyeur {
    my $zombie;
    our %Statut_Fils; # Stocke chaque code de retour
    $zombies = 0;
    while (($zombie = waitpid(-1, WNOHANG)) != -1) {
        $Statut_Fils{$zombie} = $?;
    }
}
while (1) {
    fossoyeur() if $zombies;
    ...
}

Ce code présume que votre noyau implémente des signaux fiables. Ce n'était traditionnellement pas le cas pour les anciens SysV, ce qui rendait impossible l'écriture de gestionnaires de signaux corrects. Depuis la version 5.003, Perl utilise, là où il est disponible, l'appel système sigaction(2), qui est bien plus digne de confiance. Cela signifie qu'à moins que vous ne tourniez sur un ancien système ou avec un ancien Perl, vous n'aurez pas à réinstaller vos gestionnaires et risquer de manquer des signaux. Heureusement, tous les systèmes de la famille BSD (y compris Linux, Solaris et Mac OS X) et les systèmes compatibles POSIX fournissent des signaux fiables, le comportement dégradé des anciens SysV est donc plus une remarque historique qu'un problème d'actualité.

Avec ces nouveaux noyaux, beaucoup d'autres choses fonctionnent également mieux. Par exemple, les appels système « lents » (ceux qui peuvent bloquer, comme read, wait et accept) redémarreront automatiquement s'ils sont interrompus par un signal. Au mauvais vieux temps, le code de l'utilisateur devait se souvenir de vérifier explicitement si chaque appel système lent avait échoué ou non, avec $! ($ERRNO) positionné à EINTR et, si c'était le cas, le relancer. Ceci ne se produisait pas seulement avec les signaux INT; même des signaux inoffensifs comme TSTP (provenant d'un Ctrl-Z) ou CONT (en passant la tâche en premier plan) auraient fait avorter l'appel système. Perl redémarre maintenant l'appel système automatiquement pour vous si le système d'exploitation le permet. Ceci est généralement décrit comme étant une fonctionnalité.

Vous pouvez vérifier si vous disposez ou non d'un comportement des signaux plus rigoureux, dans le style de POSIX, en chargeant le module Config et en testant si $Config{d_sigaction} est à vrai. Pour trouver si les appels système lents sont redémarrables ou non, vérifiez la documentation de votre système à propos de sigaction(2) ou sigvec(3) ou dénichez SV_INTERRUPT ou SA_RESTART dans votre fichier C sys/signal.h. Si vous trouvez l'un de ces symboles ou les deux, vous avez certainement des appels système redémarrables.

16-1-c. Minutage des opérations lentes

Une utilisation courante des signaux consiste à imposer une limite de temps aux opérations s'exécutant lentement. Si vous êtes sur un système Unix (ou sur tout autre système compatible POSIX implémentant le signal ALRM), vous pouvez demander au noyau d'envoyer un signal ALRM à votre processus à un moment à venir précis :

 
Sélectionnez
use Fcntl ':flock';
eval {
    local $SIG{ALRM} = sub { die "redémarrage de la minuterie" };
    alarm 10;              # planifie une alarme dans 10 secondes
    eval {
        flock(HF, LOCK_EX) # un verrou exclusif bloquant
            or die "flock impossible : $!";
    };
    alarm 0;               # annule l'alarme
};
alarm 0;            # protection contre une situation de concurrence
die if $@ && $@ !~ /redémarrage de la minuterie/;     # relance

Si l'alarme est déclenchée pendant que vous attendez le verrou et que vous ne faites qu'intercepter le signal avant de sortir, vous retournerez directement dans le flock car Perl relance automatiquement les appels système là où c'est possible. La seule solution est de lever une exception avec die et de laisser ensuite eval l'intercepter. (Ceci fonctionne, car l'exception aboutit à un appel de la fonction longjmp(3) de la bibliothèque C et c'est réellement ce qui vous dispense de relancer l'appel système.)

La capture d'exception imbriquée existe, car l'appel de flock lèverait une exception si flock n'était pas implémenté sur votre plate-forme et vous devez vous assurez d'annuler l'alarme dans tous les cas. Le second alarm 0 sert dans le cas où le signal arrive après avoir lancé le, flock mais avant d'atteindre le premier alarm 0. Sans le second alarm 0, vous prendriez le risque d'avoir une légère situation de concurrence (N.d.T. Race condition) — mais le poids ne compte pas dans les situations de concurrence ; soit il y en a, soit il n'y en a pas. Et nous préférons qu'il n'y en ait pas.

16-1-d. Blocage des signaux

De temps à autre, vous aimeriez bien retarder la réception d'un signal durant une section de code critique. Vous ne voulez pas ignorer le signal aveuglément, mais ce que vous êtes en train de faire est trop important pour être interrompu. Le hachage %SIG de Perl n'implémente pas de blocage des signaux, contrairement au module POSIX, à travers son interface à l'appel système sigprocmask(2) :

 
Sélectionnez
use POSIX qw(:signal_h);
$ensemble_signaux = POSIX::SigSet->new;
$ensemble_bloques = POSIX::SigSet>new(SIGINT,SIGQUIT,SIGCHLD);
sigprocmask(SIG_BLOCK, $ensemble_bloques, $ensemble_signaux)
    or die "Impossible de bloquer les signaux INT,QUIT,CHLD : $!\n";

Une fois que les trois signaux sont bloqués, vous pouvez faire ce que vous voulez sans craindre d'être embêté. Lorsque vous en avez fini avec votre section critique, débloquez les signaux en restaurant l'ancien masque de signaux :

 
Sélectionnez
sigprocmask(SIG_SETMASK, $ensemble_signaux)
    or die "Impossible de restaurer les signaux INT, QUIT, CHLD: $!\n";

Si l'un de ces trois signaux était arrivé alors qu'ils étaient bloqués, ils sont délivrés immédiatement. Si deux signaux ou plus sont en attente, l'ordre de délivrance est indéfini. De plus, on ne fait aucune distinction entre le fait de recevoir une seule fois un signal particulier que l'on a bloqué et le recevoir plusieurs fois.(130) Par exemple, si neuf processus fils se terminaient pendant que vous bloquiez les signaux CHLD, votre gestionnaire (si vous en aviez un) ne serait appelé qu'une seule fois après avoir débloqué les signaux. C'est pourquoi, lorsque vous fossoyez les zombies, vous devez toujours boucler jusqu'à ce qu'ils soient tous partis.

16-2. Fichiers

Peut-être n'aviez-vous jamais pensé aux fichiers comme étant un mécanisme d'IPC, mais ils se taillent la part du lion dans les communications interprocessus — loin devant tous les autres procédés réunis. Lorsqu'un processus dépose ses précieuses données dans un fichier et qu'un autre processus les extrait, ces processus ont communiqué entre eux. Les fichiers offrent quelque chose d'unique parmi toutes les formes d'IPC couvertes ici : comme un rouleau de papyrus enseveli dans le désert et déterré après des millénaires, un fichier peut être déterré et lu longtemps après le décès de celui qui l'a écrit.(131) En travaillant sur la persistance tout en étant d'une facilité d'utilisation comparable, il n'est pas surprenant que les fichiers soient toujours populaires.

L'emploi de fichiers, pour transmettre des informations depuis un passé révolu vers un futur incertain, amène quelques surprises. Vous écrivez le fichier sur un médium permanent, tel qu'un disque, et c'est tout. (Vous pourriez dire à un serveur web où le trouver, s'il contient du HTML.) Cela devient un challenge intéressant lorsque tous les protagonistes sont toujours en vie et essaient de communiquer entre eux. Sans une convention à propos de celui dont le temps de parole est arrivé, toute communication fiable est impossible ; l'accord peut être conclu via le verrouillage de fichier, qui sera abordé dans le prochain paragraphe. Dans le paragraphe suivant, nous parlerons de la relation spéciale existant entre un processus père et ses fils, qui permet à toutes les protagonistes d'échanger de l'information via l'héritage de l'accès aux mêmes fichiers.

Les fichiers présentent toutefois des limites quand il est question d'accès distant, de synchronisation, de fiabilité et de suivi des sessions. D'autres paragraphes de ce chapitre présentent divers mécanismes d'IPC inventés pour pallier ces limites.

16-2-a. Verrouillage de fichier

Dans un environnement multitâche, vous devez faire attention à ne pas rentrer en collision avec d'autres processus essayant d'utiliser le même fichier que vous. Tant que tous les processus ne font que lire, il n'y a aucun problème, mais dès qu'au moins un d'entre eux a besoin d'écrire dans le fichier, il s'en suit un chaos complet à moins qu'un quelconque mécanisme de verrouillage ne vienne faire la police.

Ne vous contentez pas de l'existence d'un fichier (c'est-à-dire, -e $fichier) comme une indication de verrouillage, car il existe une situation de concurrence (race condition) entre le test de l'existence de ce nom de fichier et ce que vous avez prévu de faire avec (comme le créer, l'ouvrir ou l'effacer). Voir le paragraphe « Gérer les situations de concurrences » au chapitre 23, Sécurité, pour plus d'informations à ce sujet.

L'interface portable de Perl pour le verrouillage est la fonction flock(HANDLE, DRAPEAUX), décrite au chapitre 29, Fonctions. Pour une portabilité maximale, Perl n'utilise que les fonctionnalités de verrouillage les plus simples et les plus largement répandues que l'on puisse trouver sur le plus grand éventail de plates-formes. Cette sémantique est suffisamment simple pour être émulée sur la plupart des systèmes, y compris ceux qui n'implémentent pas l'appel traditionnel du même nom, comme System V ou Windows NT. (Cependant, si vous tournez sur un système Microsoft antérieur à NT, vous n'avez probablement aucune chance, vous n'en auriez pas plus si vous étiez sur un système Apple antérieur à Mac OS X.)

Il existe deux variétés de verrous : les verrous partagés (le drapeau LOCK_SH) et les verrous exclusifs (le drapeau LOCK_EX). Malgré le nom intimidant « exclusif », les processus ne sont pas obligés d'obéir aux verrous sur les fichiers. C'est-à-dire que flock n'implémente qu'un verrouillage consultatif, signifiant que le verrouillage d'un fichier n'empêche pas un autre processus de lire, ni même d'écrire dans ce fichier. Une demande de verrou exclusif n'est qu'une manière pour un processus de permettre au système d'exploitation de bloquer cette demande jusqu'à ce qu'il n'y ait plus de verrous, qu'ils soient partagés ou exclusifs. De même, lorsqu'un processus demande un verrou partagé, il ne fait que se bloquer lui-même jusqu'à ce qu'il n'y ait plus de verrous exclusifs. Un fichier en concurrence ne peut être accessible de manière sûre que lorsque tous les protagonistes utilisent le mécanisme de verrouillage de fichier.

Ainsi, flock est une opération bloquante par défaut. C'est-à-dire que si vous ne pouvez obtenir immédiatement le verrou que vous désirez, le système d'exploitation bloque votre processus jusqu'à ce que ce soit possible. Voici comment obtenir un verrou partagé, bloquant, généralement utilisé pour lire un fichier :

 
Sélectionnez
use Fcntl qw(:DEFAULT :flock);
open(HF, "< nom_fichier") or die "impossible d'ouvrir nom_fichier: $!";
flock(HF, LOCK_SH) or die "impossible de verrouiller nom_fichier : $!";
# maintenant, lisez le fichier

Vous pouvez essayer d'acquérir un verrou de manière non bloquante en incluant le drapeau LOCK_NB dans la requête flock. S'il est impossible de vous donner le verrou immédiatement, la fonction échoue et renvoie instantanément faux. Voici un exemple :

 
Sélectionnez
flock(HF, LOCK_SH | LOCK_NB)
    or die "impossible de verrouiller nom_fichier : $!";

Vous pouvez désirer quelque chose d'autre que la levée d'une exception comme nous l'avons fait ici, mais vous n'oserez certainement pas faire d'entrées/sorties sur le fichier. Si le verrou vous est refusé, vous ne devriez pas accéder au fichier jusqu'à ce que vous l'obteniez. Qui sait dans quel état de confusion vous pourriez trouver le fichier ? L'intérêt principal du mode non bloquant est de vous laisser sortir faire quelque chose d'autre pendant l'attente. Mais il peut également s'avérer utile pour produire des interactions plus amicales en avertissant les utilisateurs que cela pourrait prendre du temps pour obtenir le verrou afin qu'ils ne se sentent pas abandonnés :

 
Sélectionnez
use Fcntl qw(:DEFAULT :flock);
open(HF, "< nom_fichier") or die "impossible d'ouvrir nom_fichier: $!";
unless (flock(HF, LOCK_SH | LOCK_NB)) {
    local $| = 1;
    print "Attente d'un verrou sur nom_fichier...";
    flock(HF, LOCK_SH)
        or die "impossible de verrouiller nom_fichier : $!";
    print "obtenu.\n";
}
# maintenant, lisez dans HF

Certains seront tentés de mettre ce mode non bloquant dans une boucle. Le problème majeur avec le mode non bloquant est que, pendant que vous revenez pour vérifier à nouveau, quelqu'un d'autre a pu s'emparer du verrou parce que vous avez abandonné votre place dans la file d'attente. Parfois vous n'avez qu'à vous mettre dans la file et patienter. Si vous avez de la chance, il y aura quelques magazines à lire.

Les verrous s'appliquent sur les handles de fichiers, non sur les noms de fichiers.(132) Lorsque vous fermez le fichier, le verrou se dissout automatiquement, que vous ayez fermé le fichier explicitement en appelant close ou implicitement en le rouvrant ou en quittant votre processus.

Pour obtenir un verrou exclusif, généralement utilisé pour écrire dans un fichier, vous devez faire attention. Vous ne pouvez pas utiliser pour cela un open ordinaire ; si vous utilisez un mode d'ouverture de <, il échouera sur les fichiers qui n'existent pas encore et si vous utilisez >, il écrasera les fichiers existant déjà. Utilisez plutôt sysopen sur le fichier afin qu'il puisse être verrouillé avant d'être écrasé. Une fois que vous avez ouvert le fichier en toute sécurité pour y écrire, mais que vous n'y avez pas encore touché, obtenez un verrou exclusif et alors seulement, tronquez le fichier. Maintenant vous pouvez l'écraser avec les nouvelles données.

 
Sélectionnez
use Fcntl qw(:DEFAULT :flock);
sysopen(HF, "nom_fichier", O_WRONLY | O_CREAT)
    or die "impossible d'ouvrir nom_fichier : $!";
flock(HF, LOCK_EX)
    or die "impossible de verrouiller nom_fichier : $!";
truncate(HF, 0)
    or die "impossible de tronquer nom_fichier : $!";
# maintenant, écrivez dans HF

Si vous voulez modifier le contenu d'un fichier sur place, utilisez encore sysopen. Cette fois-ci, vous demandez un accès à la fois en lecture et en écriture, en créant le fichier si besoin. Une fois le fichier ouvert, mais avant d'avoir fait la moindre lecture ou écriture, obtenez le verrou exclusif et gardez-le durant toute votre transaction. Il vaut souvent mieux relâcher le verrou en fermant le fichier, car cela garantit que tous les tampons seront vidés avant que le verrou ne soit relâché.

Une mise à jour implique la lecture d'anciennes valeurs et l'écriture de nouvelles. Vous devez accomplir ces deux opérations sous un seul verrou exclusif, de peur qu'un autre processus ne lise les valeurs (en passe de devenir incorrectes) après (voire avant) vous, mais avant que vous n'écriviez. (Nous reverrons cette situation quand nous aurons parlé de la mémoire partagée, plus loin dans ce chapitre.)

 
Sélectionnez
use Fcntl qw(:DEFAULT :flock);

sysopen(HF, "fichier_compteur", O_RDWR | O_CREAT)
    or die "impossible d'ouvrir fichier_compteur : $!";
flock(HF, LOCK_EX)
    or die "impossible de verrouiller en écriture fichier_compteur : $!";
$compteur = <HF> || 0; # serait indéfini la première fois
seek(HF, 0, 0)
    or die "impossible de rembobiner fichier_compteur : $!";
print HF $compteur+1, "\n"
    or die "impossible d'écrire dans fichier_compteur : $!";

# la prochaine ligne est superflue dans ce programme
#, mais est une bonne idée dans le cas général
truncate(HF, tell(HF))
    or die "impossible de tronquer fichier_compteur : $!";
close(HF)
    or die "impossible de fermer fichier_compteur : $!";

Vous ne pouvez pas verrouiller un fichier que vous n'avez pas encore ouvert et vous ne pouvez pas avoir un verrou unique s'appliquant à plus d'un fichier. Cependant, ce qu'il vous est possible de faire est d'utiliser un fichier totalement séparé qui se comporterait comme une sorte de sémaphore, comme un feu de signalisation, et qui fournirait un accès contrôlé à quelque chose d'autre à travers des verrous partagés et exclusifs sur ce fichier sémaphore. Cette approche possède de multiples avantages. Vous pouvez avoir un fichier verrou qui contrôle l'accès à de multiples fichiers, évitant le genre de verrou mortel (deadlock) survenant lorsqu'un processus essaie de verrouiller ces fichiers dans un certain ordre pendant qu'un autre processus essaie de les verrouiller dans un ordre différent. Vous pouvez utiliser un fichier sémaphore pour verrouiller un répertoire entier. Vous pouvez même contrôler l'accès à quelque chose d'autre qui ne soit même pas dans le système de fichiers, comme un objet de mémoire partagée ou une socket sur laquelle plusieurs serveurs préforkés aimeraient appeler accept.

Si vous avez un fichier DBM ne fournissant pas son propre mécanisme de verrouillage explicite, un fichier verrou auxiliaire est la meilleure façon de contrôler les accès concurrents par de multiples agents. Sinon, le cache interne de votre bibliothèque DBM peut se désynchroniser du fichier sur disque. Avant d'appeler dbmopen ou tie, ouvrez et verrouillez le fichier sémaphore. Si vous ouvrez la base de données avec O_RDONLY, vous devrez utiliser LOCK_SH pour le verrou. Sinon, utilisez LOCK_EX, pour un accès exclusif lors des mises à jour dans la base. (Encore une fois, cela ne fonctionne que si tous les protagonistes s'entendent pour accorder de l'attention au sémaphore.)

 
Sélectionnez
use Fcntl qw(:DEFAULT :flock);
use DB_File; # seulement pour l'exemple; toute autre db convient

$NOM_DB = "chemin/a/la/base";
$VERROU = $NOM_DB . ".verrou";

# utilisez O_RDWR si vous pensez mettre des données dans le fichier verrou
sysopen(DB_VER, $VERROU, O_RDONLY | O_CREAT)
     or die "impossible d'ouvrir $VERROU : $!";

# vous devez obtenir le verrou avant d'ouvrir la base
flock(DB_VER, LOCK_SH)
    or die "impossible de verrouiller $VERROU avec LOCK_SH : $!";

tie(%hachage, "DB_File", $NOM_DB, O_RDWR | O_CREAT)
    or die "impossible de lier $NOM_DB : $!";

Maintenant vous pouvez en toute sécurité faire ce que vous voulez avec le %hachage lié. Lorsque vous en avez fini avec votre base de données, assurez-vous de relâcher explicitement ces ressources et dans l'ordre inverse de celui avec lequel vous les avez acquises :

 
Sélectionnez
untie %hachage;   # vous devez fermer la base avant le fichier verrou
close DB_VER;     # maintenant le verrou peut partir en toute sécurité

Si la bibliothèque GNU DBM est installée, vous pouvez utiliser le verrouillage implicite du module standard GDBM_File. Sauf si le tie initial contient le drapeau GDBM_NOLOCK, la bibliothèque s'assure que seul un écrivain à la fois puisse ouvrir le fichier GDBM et que les lecteurs et les écrivains n'aient pas à la fois la base de données ouverte.

16-2-b. Passage de handles de fichiers

À chaque fois que vous créez un processus fils en utilisant fork, ce nouveau processus hérite de tous les handles de fichiers ouverts de son père. L'utilisation des handles de fichiers pour les communications interprocessus est plus facile à illustrer en utilisant tout d'abord des fichiers. La compréhension de ce mécanisme est essentielle pour maîtriser les mécanismes plus élaborés des pipes et des sockets, décrits plus loin dans ce chapitre.

L'exemple le plus simple consiste à ouvrir un fichier et à démarrer un processus fils. Le fils utilise alors le handle de fichier déjà ouvert pour lui :

 
Sélectionnez
open(ENTREE, "< /etc/motd") or die "/etc/motd : $!";
if ($pid = fork) { waitpid($pid, 0) }
else {
    defined($pid) or die "fork : $!";
    while (<ENTREE>) { print "$. : $_" }
    exit; # empêche le fils de retomber dans le code principal
}
# le handle de fichier ENTREE se trouve maintenant à EOF pour le père

Une fois que l'accès à un fichier a été accordé par open, il le reste jusqu'à la fermeture du handle de fichier ; la modification des permissions du fichier ou des privilèges d'accès n'a aucun effet sur son accessibilité. Même si le processus altère ultérieurement son ID d'utilisateur ou de groupe ou si le fichier est transféré à un nouveau propriétaire, cela n'affecte pas les handles de fichiers déjà ouverts. Les programmes tournant sous des permissions accrues (comme les programmes set-id ou les démons du système) ouvrent souvent un fichier sous leurs droits accrus et délèguent ensuite le handle de fichier à un processus fils qui n'aurait pas pu ouvrir le fichier lui-même.

Bien que cette fonctionnalité soit très pratique lorsqu'elle est utilisée intentionnellement, elle peut également poser des problèmes de sécurité si les handles de fichiers ont des fuites accidentelles d'un programme à l'autre. Pour éviter d'accorder un accès implicite à tous les handles de fichiers possibles, Perl ferme automatiquement tout handle de fichier qu'il a ouvert (y compris les pipes et les sockets) à chaque fois que vous lancez avec exec un nouveau programme ou que vous en exécutez un implicitement avec un open sur un pipe, avec system ou avec qx// (apostrophes inverses). Les handles de fichiers du système STDIN, STDOUT et STDERR échappent à cette règle, car leur fonction première est de fournir un lien dans les communications entre programmes. Un moyen de passer un handle de fichier à un nouveau programme consiste donc à copier ce handle vers un des handles standard :

 
Sélectionnez
open(ENTREE, "< /etc/motd")    or die "/etc/motd : $!";
if ($pid = fork) { wait }
else {
    defined($pid)              or die "fork : $!";
    open(STDIN, "<&ENTREE")    or die "dup : $!";
    exec("cat", "-n")          or die "exec cat : $!";
}

Si vous voulez vraiment que le nouveau programme acquière l'accès à un handle de fichier autre que ces trois-là, c'est possible, mais vous devrez faire une ou deux petites choses. Lorsque Perl ouvre un nouveau fichier (ou un pipe, ou une socket), il vérifie la valeur courante de la variable $^F($SYSTEM_MAX_FD). Si le descripteur numérique du fichier utilisé par ce nouveau handle de fichier est supérieur à $^F, il est marqué comme l'un des descripteurs qu'il faudra fermer. Sinon, Perl le laisse tranquille et les nouveaux programmes que vous lancerez avec exec hériteront de l'accès.

Il n'est pas toujours aussi facile de prédire quel descripteur de fichier aura votre handle récemment ouvert, mais vous pouvez positionner de manière temporaire votre numéro maximal pour les descripteurs de fichiers système à une valeur immodérément grande le temps de faire le open :

 
Sélectionnez
# ouvre le fichier et marque ENTREE comme devant être
# conservé entre les exec
{
    local $^F = 10_000;
    open(ENTREE, "< /etc/motd") or die "/etc/motd : $!";
} # l'ancienne valeur de $^F est restaurée lorsqu'on sort de la portée

Maintenant, tout ce qu'il vous reste à faire est d'obtenir que le nouveau programme accorde de l'attention au numéro de descripteur du handle de fichier que vous venez d'ouvrir. La solution la plus propre (sur les systèmes implémentant ceci) consiste à passer un nom de fichier spécial équivalant à un descripteur de fichier. Si votre système possède un répertoire nommé /dev/fd ou /proc/$$/fd, contenant des fichiers numérotés de 0 au numéro maximal de descripteurs supportés, vous pouvez probablement utiliser cette stratégie. (Beaucoup de systèmes d'exploitation Linux ont les deux, mais seule la version /proc a tendance à être correctement peuplée. Les systèmes BSD et Solaris préfèrent /dev/fd. Vous devrez fureter dans votre système pour voir ce qui vous convient le mieux.) Ouvrez d'abord votre handle de fichier et marquez-le comme étant l'un de ceux à laisser ouverts entre les exec comme dans le code précédent, puis faites un fork comme ceci :

 
Sélectionnez
if ($pid = fork) { wait }
else {
    defined($pid)                    or die "fork : $!";
    $fichier_df = "/dev/fd/" . fileno(ENTREE);
    exec("cat", "-n", $fichier_df)   or die "exec cat : $!";
}

Si votre système implémente l'appel système fcntl, vous pouvez ajuster manuellement le drapeau de handle de fichier close-on-exec. Ceci est pratique pour les fois où vous n'avez pas réalisé lorsque vous avez créé le handle de fichier que vous voudriez le partager avec vos fils.

 
Sélectionnez
use Fcntl qw/F_SETFD/;

fcntl(ENTREE, F_SETFD, 0)
    or die "Impossible de mettre à zéro le flag close-on-exec
        pour ENTREE : $!\n";

Vous pouvez également forcer un handle de fichier à se fermer :

 
Sélectionnez
fcntl(ENTREE, F_SETFD, 1)
    or die "Impossible de positionner le flag close-on-exec
        pour ENTREE : $!\n";

Vous pouvez aussi demander le statut actuel :

 
Sélectionnez
use Fcntl qw/F_SETFD F_GETFD/;

printf("ENTREE sera %s entre les exec\n",
    fcntl(ENTREE, F_GETFD, 1) ? "fermé" : "laissé ouvert");

Si votre système n'implémente pas les descripteurs de fichiers nommés dans le système de fichiers et que vous voulez passer un handle de fichier autre que STDIN, STDOUT ou STDERR, vous pouvez toujours le faire, mais vous devrez procéder à quelques arrangements spéciaux dans votre programme. Les stratégies courantes pour ceci consistent à passer le numéro de descripteur par le biais d'une variable d'environnement ou d'une option de la ligne de commande.

Si le programme exécuté est en Perl, vous pouvez utiliser open pour convertir un descripteur de fichier en un handle de fichier. Au lieu de spécifier un nom de fichier, utilisez "&=", suivi du numéro de descripteur.

 
Sélectionnez
if (defined($ENV{no_df_entree}) && $ENV{no_df_entree} =~ /^d$/) {
    open(ENTREE, "<&=$ENV{no_df_entree}")
        or die "fdopen $ENV{no_df_entree} en entrée impossible : $!";
}

Cela devient encore plus facile que cela si vous allez lancer un sous-programme ou un programme Perl qui attend un nom de fichier comme argument. Vous pouvez utiliser la fonctionnalité d'ouverture de descripteur de la fonction ordinaire open de Perl (et non sysopen ou la forme à trois arguments d'open) pour que cela se fasse automatiquement. Imaginez que vous ayez un simple programme Perl comme ceci :

 
Sélectionnez
#!/usr/bin/perl -p
# nl -numéro de lignes en entrée
printf "%6d ", $.;

En supposant que vous vous êtes arrangé pour que le handle ENTREE reste ouvert entre les exec, vous pouvez appeler ce programme de cette manière :

 
Sélectionnez
$df_spec = '<&=' . fileno(ENTREE);
system("nl", $df_spec);

ou pour capturer la sortie :

 
Sélectionnez
@lignes = `nl '$df_spec'`;   # les apostrophes empêchent le
                             # shell d'interpoler df_spec

Que vous fassiez ou non un exec d'un autre programme, si vous utilisez les descripteurs de fichiers hérités à travers un fork, il y a un petit pépin. Au contraire des variables copiées à travers un fork, qui deviennent en fait des copies dupliquées, mais indépendantes, les descripteurs de fichiers sont vraiment les mêmes dans les deux processus. Si l'un des processus lit des données depuis le handle, le pointeur de seek (représentant la position dans le fichier) avance également dans l'autre processus et ces données ne sont plus accessibles par les deux processus. S'ils lisent chacun à leur tour, ils joueront ensemble à saute-mouton dans le fichier. Intuitivement, cela reste sensé pour les handles attachés à des périphériques séries, des pipes ou des sockets, puisque ceux-ci ont tendance à être des périphériques en lecture seule avec des données éphémères. Mais ce comportement peut vous surprendre avec des fichiers sur disque. Si c'est un problème, rouvrez tout fichier ayant besoin de traçages séparés après le fork.

L'opérateur fork est un concept tiré d'Unix, ce qui signifie qu'il peut ne pas être correctement implémenté sur toutes les plates-formes non-Unix/non-POSIX. Notamment, fork ne fonctionne sur les systèmes Microsoft que si vous lancez Perl 5.6 (ou une version supérieure) sur Windows 98 (ou une version ultérieure). Bien que fork soit implémenté sur ces systèmes via de multiples flux d'exécution concurrents dans le même programme, il ne s'agit pas des types de tâches (threads) où toutes les données sont partagées par défaut ; ici, seuls les descripteurs de fichiers le sont. Voir également le chapitre 17, Threads.

16-3. Pipes

Un pipe est un canal d'entrées/sorties unidirectionnel pouvant transférer un flux d'octets d'un processus à un autre. Il existe deux variétés de pipes : les pipes nommés et les pipes anonymes. Il se peut que vous connaissiez mieux les pipes anonymes, dont nous commencerons donc par parler.

16-3-a. Pipes anonymes

La fonction open de Perl ouvre un pipe plutôt qu'un fichier lorsque vous ajoutez un symbole de pipe au début ou à la fin du deuxième argument d'open. Cela change le reste de l'argument en une commande, qui sera interprétée en tant que processus (ou en tant qu'ensemble de processus) depuis lequel vous voulez recevoir ou vers lequel vous voulez envoyer un flux de données via un pipe. Voici comment démarrer un processus fils auquel vous avez l'intention d'écrire :

 
Sélectionnez
open IMPRESSION, "| cat -v | lpr -h 2>/dev/null"
    or die "fork impossible : $!";
local $SIG{PIPE} = sub { die "pipe d'impression cassé" };
print IMPRESSION "trucs\n";
close IMPRESSION or die "mauvaise impression : $! $?";

Cet exemple démarre en fait deux processus, nous écrivons directement dans le premier (effectuant cat). Le second processus (effectuant lpr) reçoit ensuite la sortie du premier. Dans la programmation shell, on appelle fréquemment ceci un pipeline. Un pipeline peut contenir autant de processus à la suite que vous voulez, pourvu que ceux se trouvant au milieu sachent se comporter comme des filtres ; c'est-à-dire qu'ils sachent lire depuis l'entrée standard et écrire dans la sortie standard.

Perl utilise le shell par défaut de votre système (/bin/sh sur Unix) à chaque fois qu'une commande de pipe comprend des caractères spéciaux que le shell prend en compte. Si vous ne faites que démarrer une commande et si vous n'avez pas besoin d'utiliser un shell — ou si vous ne voulez pas —, vous pouvez plutôt employer la forme à plusieurs arguments d'un open sur un pipe :

 
Sélectionnez
open IMPRESSION, "|-", "lpr", "-h" # exige Perl 5.6.1
    or die "impossible de lancer lpr : $!";

Si vous rouvrez la sortie standard de votre programme en tant que pipe vers un autre programme, tout ce que vous afficherez ultérieurement avec print vers STDOUT deviendra l'entrée standard du nouveau programme. Ainsi, pour envoyer à un pager la sortie de votre programme,(133) vous devez utiliser :

 
Sélectionnez
if (-t STDOUT) {     # seulement si stdout est un terminal
    my $pager = $ENV{PAGER} || 'more';
    open(STDOUT, "| $pager")  or die "fork du pager impossible : $!";
}
END {
    close(STDOUT)             or die "impossible de fermer STDOUT : $!"
}

Lorsque vous écrivez dans un handle de fichier connecté à un pipe, fermez toujours ce handle explicitement avec close lorsque vous en avez fini avec lui. De cette manière, votre programme principal ne se terminera pas avant sa progéniture.

Voici comment démarrer un processus fils dans lequel vous avez l'intention de lire :

 
Sélectionnez
open STATUT, "netstat -an 2>/dev/null |"
    or die "fork impossible : $!";
while (<STATUT>) {
    next if /^(tcp|udp)/;
    print;
}
close STATUT or die "mauvais netstat: $! $?";

Vous pouvez ouvrir un pipeline sur plusieurs niveaux en entrée exactement comme vous le pouvez en sortie. Et comme précédemment, vous pouvez éviter le shell en utilisant une autre forme d'open :

 
Sélectionnez
open STATUT, "-|", "netstat", "-an" # exige Perl 5.6.1
    or die "impossible de lancer netstat : $!";

Mais dans ce cas, vous n'aurez pas de redirection des entrées/sorties, ni d'expansion de jokers (wilcard), ni de pipes sur plusieurs niveaux, puisque Perl se base sur votre shell pour ces fonctionnalités.

Vous pourriez avoir remarqué que vous pouvez utiliser des apostrophes inverses pour obtenir le même effet qu'en ouvrant un pipe en lecture :

 
Sélectionnez
print grep { !/^(tcp|udp)/ } `netstat -an 2>&1`;
die "mauvais netstat" if $?;

Si les apostrophes sont extrêmement commodes, le programme lit tout en mémoire en une seule fois, il est donc souvent plus efficace d'ouvrir votre propre handle de fichier sur un pipe et de traiter le fichier ligne par ligne ou enregistrement par enregistrement. Cela vous donne un contrôle plus fin sur l'ensemble de l'opération, en vous laissant tuer le processus fils plus tôt si ça vous plaît. Vous pouvez également être plus efficace en traitant l'entrée au fur et à mesure, puisque les ordinateurs savent intercaler diverses opérations lorsque deux processus ou plus tournent en même temps. (Même sur une machine avec une seule CPU, les opérations d'entrées/sorties peuvent survenir lorsque la CPU fait autre chose.)

Comme vous lancez deux processus ou plus en concurrence, une catastrophe peut frapper le processus fils à n'importe quel moment entre l'ouverture avec open et la fermeture avec close. Cela signifie que le père doit vérifier la valeur de retour des deux fonctions, open et close. La seule vérification d'open n'est pas suffisante, puisqu'elle vous dirait seulement si la commande a été lancée avec succès ou non. (Elle ne peut vous dire ceci que dans les versions récentes de Perl et seulement si la commande est exécutée directement par le fils et non via le shell.) Le fils rapporte à son père les catastrophes qui interviennent après cela avec un statut de fin non nul. Lorsque la fonction close voit cela, elle sait renvoyer une valeur fausse, indiquant que le véritable statut devrait être lu à partir de la variable $?($CHILD_ERROR). Vérifier la valeur renvoyée par close est donc aussi important que celle renvoyée par open. Si vous écrivez dans un pipe, vous devriez également vous préparer à gérer le signal PIPE, qui vous est envoyé si le processus à l'autre extrémité meurt avant que vous ayez fini de lui transmettre des données.

16-3-b. Soliloque

Une autre approche de la communication interprocessus consiste à faire soliloquer le programme, pour ainsi dire. En fait, votre processus discute avec une copie de lui-même par le biais de pipes. Cela fonctionne largement comme l'open sur un pipe dont nous avons parlé au paragraphe précédent, hormis le fait que le processus fils continue à exécuter votre script plutôt qu'une autre commande.

Pour indiquer ceci à la fonction open, utilisez une pseudocommande composée d'un signe moins. Le second argument d'open ressemble donc à « -| » ou à « |- », selon que vous voulez transmettre des données depuis ou vers vous-même. Comme avec une commande fork ordinaire, la fonction open renvoie l'ID du processus fils dans le processus père, mais 0 dans le processus fils. Une autre asymétrie est que le handle de fichier cité dans l'open n'est utilisé que dans le processus père. L'extrémité du pipe pour le fils est rattachée soit à STDIN, soit à STDOUT, selon le cas. C'est-à-dire que si vous ouvrez un pipe vers le signe moins, vous pouvez écrire des données dans le handle de fichier que vous avez ouvert et votre fils les lira dans STDIN :

 
Sélectionnez
if (open(VERS, "|-")) {
    print VERS $depuis_pere;
}
else {
    $vers_fils = <STDIN>;
    exit;
}

Si vous ouvrez un pipe depuis le signe moins, vous pouvez lire dans le handle de fichier ouvert ce que le fils écrit dans STDOUT :

 
Sélectionnez
if (open(DEPUIS, "-|"))
    $vers_pere = <DEPUIS>;
}
else {
    print STDOUT $depuis_fils;
    exit;
}

Une application courante de cette construction est de court-circuiter le shell lorsque vous voulez ouvrir un pipe depuis une commande. Vous pourriez vouloir faire ceci, car vous ne voulez pas que le shell interprète les métacaractères éventuels dans les noms de fichiers que vous essayez de passer à la commande. Si vous tournez sous la version 5.6.1 de Perl, ou une version supérieure, vous pouvez utiliser la forme à plusieurs arguments d'open pour obtenir le même résultat.

Une autre utilisation d'un open sur un fork consiste à ouvrir un fichier ou une commande en toute sécurité alors que vous tournez sous un certain UID ou GID. Le fils que vous lancez avec fork abandonne tous les droits d'accès spéciaux, puis ouvre en toute sécurité le fichier ou la commande et se comporte comme un intermédiaire, en passant les données entre son père plus puissant et le fichier ou la commande qu'il a ouvert. Vous pouvez trouver des exemples dans la section Accéder aux commandes et aux fichiers avec des privilèges restreints, au chapitre 23.

Une utilisation pleine de créativité d'un open sur un fork est le filtrage de votre propre sortie. Certains algorithmes sont beaucoup plus faciles à implémenter en deux passes séparées plutôt qu'en une seule. Voici un exemple simple dans lequel nous émulons le programme tee(1) d'Unix en envoyant notre sortie ordinaire vers un pipe. L'agent à l'autre extrémité du pipe (l'un de vos propres sous-programmes) distribue notre sortie vers tous les fichiers spécifiés :

 
Sélectionnez
tee("/tmp/truc", "/tmp/machin", "/tmp/bidule");

while (<>) {
    print "$ARGV à la ligne $. => $_";
}
close(STDOUT) or die "impossible de fermer STDOUT : $!";

sub tee {
    my @sortie = @_;
    my @handles = ();
    for my $chemin (@sortie) {
        my $hf; # open remplira ceci
        unless (open ($hf, ">", $chemin)) {
            warn "impossible d'écrire dans $chemin : $!";
            next;
        }
        push @handles, $hf;
    }
    # rouvre STDOUT dans le père et renvoie
    return if my $pid = open(STDOUT, "|-");
    die "fork impossible : $!" unless defined $pid;

    # traite STDIN dans le fils
    while (<STDIN>) {
        for my $hf (@handles) {
            print $hf $_ or die "échec de la sortie de tee : $!";
        }
    }
    for my $hf (@handles) {
        close($hf) or die "échec de la fermeture de tee : $!";
    }
    exit; # empêche le fils de renvoyer au programme principal !
}

Cette technique peut être appliquée en boucle pour mettre autant de filtres que vous le désirez sur votre sortie. Continuez simplement d'appeler les fonctions qui font un fork-open sur STDOUT, ayez le fils qui lit depuis son père (qu'il voit en tant que STDIN) et passez la sortie triturée à la prochaine fonction de la chaîne.

Une autre application intéressante du soliloque avec fork-open consiste à capturer la sortie d'une fonction ayant de mauvaises manières en envoyant tous ses résultats éclabousser STDOUT. Imaginez Perl qui n'aurait qu'une fonction printf, sans fonction sprintf. Ce dont vous auriez besoin est d'une fonction marchant comme les apostrophes inverses, mais avec des fonctions Perl au lieu de commandes externes :

 
Sélectionnez
fonction_mechante("arg");        # zut, ça nous a échappé !
$chaine = sub_fork(\&fonction_mechante, "arg"); # capturez-la dans une
                                                # chaîne
@lignes = sub_fork(\&fonction_mechante, "arg"); # dans des lignes séparées

sub sub_fork {
    my $pid_fils = open my $self, "-|";
    defined $pid_fils   or die "fork impossible : $!";
    shift->(@_), exit   unless $pid_fils;
    local $/            unless wantarray;
    return <$self>;     # ferme à la sortie de la portée
}

Nous ne prétendons pas que ceci est efficace ; un handle de fichier lié avec tie irait certainement bien plus vite. Mais c'est beaucoup plus facile à coder si vous êtes plus pressé que votre ordinateur.

16-3-c. Communication bidirectionnelle

Bien que l'emploi d'open, pour se connecter à une autre commande via un pipe, fonctionne raisonnablement bien pour les communications unidirectionnelles, quid des communications bidirectionnelles ? L'approche la plus évidente ne fonctionne pas :

 
Sélectionnez
open(PROG_EN_LECTURE_ECRITURE, "| un programme |")    # MAUVAIS !

et si vous oubliez d'activer les avertissements, le message de diagnostic passera à la trappe :

 
Sélectionnez
Can't do bidirectional pipe at mon_prog line 3.

La fonction open ne l'autorise pas, car cette approche est tout à fait encline à causer des verrous mortels (deadlocks) si vous n'êtes pas très attentif. Mais si vous êtes déterminé, vous pouvez utiliser le module standard de bibliothèque IPC::Open2 pour rattacher deux pipes aux STDIN et STDOUT d'un sous-processus. Il existe également IPC::Open3 pour les entrées/sorties tridirectionnelles (vous permettant également de capturer le STDERR de votre fils), mais ce dernier exige soit une boucle select embarrassée, soit le module IO::Select, plus commode. Mais vous devrez alors éviter les opérations dans le tampon d'entrée de Perl, comme <> (readline).

Voici un exemple utilisant open2 :

 
Sélectionnez
use IPC::OPen2;
local (*Lecteur, *Ecrivain);
$pid = open2(\*Lecteur, \*Ecrivain, "bc -l");
$somme = 2;
for (1 .. 5) {
    print Ecrivain "$somme * $somme\n";
    chomp($somme = <Lecteur>);
}
close Ecrivain;
close Lecteur;
waitpid($pid, 0);
print "La somme vaut $somme\n";

Vous pouvez également autovivifier des handles de fichiers lexicaux :

 
Sélectionnez
my ($hf_lecture, $hf_ecriture);
$pid = open2($hf_lecture, $hf_hf_ecriture, "cat -u -n");

Généralement le problème avec ceci est que le vidage de tampon standard des entrées/ sorties va vraiment gâcher votre journée. Même si votre handle de fichier de sortie est automatiquement vidé (la bibliothèque le fait pour vous) de sorte que le processus à l'autre extrémité recevra vos données de manière opportune, vous ne pouvez généralement rien faire pour l'obliger à vous retourner la politesse. Dans ce cas particulier, nous avons de la chance : bc s'attend à manipuler un pipe et sait vider (flush) chaque ligne de sortie. Mais peu de commandes sont conçues de la sorte, cela fonctionne donc rarement à moins d'écrire vous-même le programme à l'autre extrémité du pipe bidirectionnel. Même les simples programmes apparemment interactifs, comme ftp, échouent ici, car ils ne videront pas leur tampon ligne par ligne dans un pipe. Il ne le feront que sur un périphérique tty.

Les modules de CPAN IO::Pty et Expect peuvent vous venir en aide, car ils fournissent un vrai tty (en fait, un vrai pseudo-tty mais se comportant comme un vrai). Cela vous donne un vidage de tampon ligne par ligne dans l'autre processus, sans modifier son programme.

Si vous éclatez votre programme en plusieurs processus et que vous voulez qu'ils aient tous ensemble une conversation allant dans les deux sens, vous ne pouvez pas utiliser les interfaces de haut niveau de Perl, car ces dernières sont toutes unidirectionnelles. Vous devrez utiliser deux appels de fonction pipe, de bas niveau, chacun manipulant une direction dans la conversation :

 
Sélectionnez
pipe(DEPUIS_PERE, VERS_FILS)   or die "pipe : $!";
pipe(DEPUIS_FILS, VERS_PERE)   or die "pipe : $!";
select((select(VERS_FILS), $| = 1))[0]); # vidage automatique
select((select(VERS_PERE), $| = 1))[0]); # vidage automatique

if ($pid = fork) {
    close DEPUIS_PERE; close VERS_PERE;
    print VERS_FILS "Pid $$ du père envoie ceci\n";
    chomp($ligne = <DEPUIS_FILS>);
    print "Pid $$ du père vient de lire ceci : '$ligne'\n";
    close DEPUIS_FILS; close VERS_FILS;
    waitpid($pid,0);
} else {
    die "fork impossible : $!" unless defined $pid;
    close DEPUIS_FILS; close VERS_FILS;
    chomp($ligne = <DEPUIS_PERE>);
    print "Pid $$ du fils vient de lire ceci : '$ligne'\n";
    print VERS_PERE "Pid $$ du fils envoie ceci\n";
    close DEPUIS_PERE; close VERS_PERE;
    exit;
}

Sur la plupart des systèmes Unix, vous n'avez en fait pas à faire deux appels pipe séparés pour accomplir une communication bidirectionnelle complète (full duplex) entre un père et son fils. L'appel socketpair fournit des connexions bidirectionnelles entre des processus associés sur la même machine. Ainsi, plutôt que deux pipe, vous n'avez besoin que d'un socketpair :

 
Sélectionnez
use Socket;
socketpair(Fils, Pere, AF_UNIX, SOCK_STREAM, PF_UNSPEC)
    or die "socketpair: $!";
# ou laissez Perl choisir les handles de fichiers pour vous
my ($hf_fils, $hf_pere);
socketpair($hf_fils, $hf_pere, AF_UNIX, SOCK_STREAM, PF_UNSPEC)
    or die "socketpair: $!";

Après le fork, le père ferme le handle Pere, puis lit et écrit via le handle Fils. Pendant ce temps, le fils ferme le handle Fils, puis lit et écrit via le handle Pere.

16-3-d. Pipes nommés

Un pipe nommé (souvent appelé FIFO) est un mécanisme pour mettre en place une conversation entre des processus sans rapport les uns avec les autres, sur la même machine. Les noms en question existent dans le système de fichiers, ce qui est une manière amusante de dire que vous pouvez mettre un fichier spécial dans le système de fichiers, sous-tendant un processus plutôt qu'un disque.(134)

Un FIFO est pratique lorsque vous voulez connecter un processus à un autre qui n'a aucun rapport. Lorsque vous ouvrez un FIFO, votre processus bloquera jusqu'à ce qu'il y ait un processus à l'autre extrémité. Ainsi, si un lecteur ouvre en premier un FIFO, il bloquera jusqu'à ce que l'écrivain apparaisse — et vice-versa.

Pour créer un pipe nommé, utilisez la fonction mkfifo de POSIX — enfin, si vous êtes sur un système POSIX. Sur les systèmes Microsoft, vous pourrez à la place regarder le module Win32::Pipe, qui, malgré les apparences, crée des pipes nommés. (Les utilisateurs de Win32 créent des pipes anonymes en employant pipe, tout comme le reste d'entre nous.)

Par exemple, mettons que vous aimeriez que votre fichier .signature produise une réponse différente à chaque fois qu'il est lu. Vous n'avez qu'à le mettre en pipe nommé avec un programme Perl à l'autre extrémité crachant des aphorismes au hasard. Maintenant, à chaque fois qu'un programme quelconque (comme en logiciel envoyant des mails, un lecteur de forums de discussion, un programme finger, et ainsi de suite) essayera de lire dans ce fichier, ce programme se connectera au vôtre et lira une signature dynamique.

Dans l'exemple suivant, nous utilisons l'opérateur de test de fichier -p, que l'on rencontre rarement, pour déterminer si quelqu'un (ou quelque chose) a supprimé accidentellement notre FIFO.(135) Si c'est le cas, il n'y a aucune raison pour essayer de l'ouvrir, nous traiterons donc ceci comme une demande de terminaison. Si nous utilisions une simple fonction open avec un mode valant « +> $chemin_f », il y aurait une minuscule situation de concurrence (race condition) qui risquerait de créer la signature en tant que simple fichier s'il disparaissait entre le test -p et l'ouverture. Nous ne pourrions pas non plus utiliser un mode valant « +< $chemin_f », car l'ouverture d'une FIFO en lecture-écriture est non bloquante (ceci n'est vrai que pour les FIFO). En utilisant sysopen et en omettant le drapeau O_CREAT, nous évitons ce problème en ne créant jamais de fichier par accident.

 
Sélectionnez
use Fcntl;               # pour sysopen
chdir;                   # au répertoire « maison »
$chemin_f = '.signature';
$ENV{PATH} .= ":/usr/games";

unless (-p $chemin_f) { # pas un pipe
    if (-e _) {         #, mais quelque chose d'autre
        die "$0 : n'écrasera pas .signature\n";
    } else {
        require POSIX;
        POSIX::mkfifo($chemin_f, 0666)
           or die "mknod $chemin_f impossible: $!";
        warn "$0 : a créé $chemin_f en tant que pipe nommé\n";
    }
}
while (1) {
    # quitte si le fichier signature est supprimé manuellement
    die "Le fichier pipe a disparu" unless -p $chemin_f;
    # la prochaine ligne bloque s'il n'y a pas de lecteur
    sysopen(FIFO, $chemin_f, O_WRONLY)
        or die "Impossible d'écrire dans $chemin_f: $!";
    print FIFO "Gérard Lambert (lambert\@machine.org)\n", `fortune -s`;
    close FIFO;
    select(undef, undef, undef, 0.2); # dort 1/5ème de seconde
}

La courte sieste après la fermeture du FIFO est nécessaire pour laisser au lecteur une chance de lire ce qui a été écrit. Si nous avions recommencé la boucle immédiatement et rouvert le FIFO avant que notre lecteur ait fini de lire les données que nous venions d'envoyer, nous n'aurions pas vu de fin de fichier (end-of-file), car il y aurait encore un écrivain. Nous aurions bouclé tous les deux encore et encore jusqu'à ce qu'à une itération donnée, l'écrivain arrive un peu après et que le lecteur voie cette fin de fichier insaisissable. (Et nous nous sommes inquiétés à propos des situations de concurrence ?)

16-4. IPC System V

Tout le monde déteste les IPC System V. Elles sont plus lentes que des cartes perforées, se taillent de petits espaces de noms insidieux sans aucun rapport avec le système de fichiers, utilisent des numéros hostiles à l'entendement humain pour nommer leurs objets et perdent constamment le fil de leurs propres pensées. Et de temps à autre, votre administrateur système doit se lancer dans une opération de ratissage pour rechercher et de détruire ces objets perdus d'IPC SysV avec ipcs(1) et les tuer avec ipcrm(1), avant que le système n'ait plus de mémoire, si tout va bien.

Malgré tous ces maux, les antiques IPC SysV ont toujours des applications valides. Les trois sortes d'objets d'IPC sont la mémoire partagée, les sémaphores et les messages. Pour la transmission de messages, on préférera de nos jours le mécanisme de sockets qui est en outre beaucoup plus portable. Pour les utilisations simples des sémaphores, on a plutôt tendance à utiliser le système de fichiers. Quant à la mémoire partagée — en fait, vous avez maintenant un problème. Si vous l'avez, l'appel système mmap(2), plus moderne, remplit ce contrat(136), mais la qualité de l'implémentation varie d'un système à l'autre. Il requiert en outre un peu d'attention pour que Perl évite de réallouer vos chaînes depuis l'endroit où mmap(2) les a mises. Mais lorsque les programmeurs envisagent d'utiliser mmap(2), ils entendent marmonner de manière incohérente les gourous locaux sur le fait que mmap souffre de problèmes épineux avec la cohérence de cache s'il manque quelque chose appelé un « cache de tampon unifié » (unified buffer cache)—à moins que ce ne soit une « tâche d'unité de canton » ( flycatcher unibus) — et, réfléchissant sur le fait que l'on sait ce qu'on perd, mais qu'on ne sait pas ce qu'on trouve, ils retournent vite aux IPC SysV qu'ils connaissent et détestent pour tous leurs besoins en mémoire partagée.

Voici un petit programme démontrant l'accès contrôlé à un tampon en mémoire partagé par une nichée de processus apparentés. Les objets d'IPC SysV peuvent également être partagés entre des processus sans rapport les uns avec les autres, sur le même ordinateur, mais vous devez alors imaginer comment ils vont se trouver les uns les autres. Nous créerons un sémaphore par entité, qui servira d'intermédiaire pour un accès sécurisé.(137)

À chaque fois que vous voulez récupérer ou mettre une nouvelle valeur en mémoire partagée, vous devez passer d'abord par le sémaphore. Ceci peut devenir un peu pénible, nous enroberons donc l'accès dans une classe d'objets. Le module IPC::Shareable va encore plus loin en enrobant sa classe d'objets dans une interface tie.

Ce programme tourne jusqu'à ce que vous l'interrompiez avec un Ctrl-C ou un équivalent :

 
Sélectionnez
#!/usr/bin/perl -w
use v5.6.0;   # ou une version supérieure
use strict;
use sigtrap qw(die INT TERM HUP QUIT);
my $PROGENITURE = shift(@ARGV) || 3;
eval { main() }; # voir DESTROY ci-dessous pour comprendre pourquoi
die if $@ && $@ !~ /^SIG reçu/;
print "\nFini.\n";
exit;

sub main {
    my $mem = ShMem->allouer("Création Originale à " . localtime);
    my(@rejetons, $fils);
    $SIG{CHLD} = 'IGNORE';
    for (my $embryon = $PROGENITURE; $embryon > 0; $embryon--) {
        if ($fils = fork) {
           print "$$ engendra $fils\n";
           next;
        }
        die "fork impossible : $!" unless defined $fils;
        eval {
            while (1) {
                $mem->verrouiller();
                $mem->positionner("$$ " . localtime)
                unless $mem->consulter =~ /^$$\b/o;
                $mem->deverrouiller();
            }
        };
        die if $@ && $@ !~ /^SIG reçu/;
        exit; # mort du fils
    }
    while (1) {
        print "Le tampon vaut ", $mem->recuperer, "\n";
        sleep 1;
    }
}

Et voici le paquetage ShMem, que ce programme utilise. Vous n'avez qu'à l'épingler à la fin du programme ou le mettre dans son propre fichier (avec un « 1; » à la fin) et le charger avec require depuis le programme principal. (On trouve les deux modules d'IPC qu'il utilise à son tour dans la distribution standard de Perl.)

 
Sélectionnez
package ShMem;
use IPC::SysV qw(IPC_PRIVATE IPC_RMID IPC_CREAT S_IRWXU);
use IPC::Semaphore;
sub MAXBUF() { 2000 }

sub allouer { # constructeur
    my $classe = shift;
    my $valeur = @_ ? shift : '';

    my $clef = shmget(IPC_PRIVATE, MAXBUF, S_IRWXU) or die "shmget : $!";
    my $sem = IPC::Semaphore->new(IPC_PRIVATE, 1, S_IRWXU | IPC_CREAT)
                        or die "IPC::Semaphore->new : $!";
    $sem->setval(0,1)   or die "sem setval : $!";

    my $obj = bless {
        PROPRIETAIRE => $$,
        SHMKEY => $clef,
        SEMA => $sem,
    } => $classe;

    $obj->mettre($valeur);
    return $obj;
}

Maintenant, passons aux méthodes de collecte et de stockage. Les méthodes recolter et mettre verrouillent le tampon, contrairement aux méthodes consulter et positionner, ces dernières ne devraient donc être utilisées que lorsque l'objet est verrouillé manuellement — ce que vous devez faire lorsque vous voulez aller chercher une ancienne valeur et remettre une version modifiée, tout cela avec le même verrou. Le programme de démonstration fait ceci dans sa boucle while (1). La transaction complète doit se dérouler avec le même verrou sinon le test et le positionnement ne seraient pas atomiques et pourraient exploser.

 
Sélectionnez
sub recolter {
    my $obj = shift;
    $obj->verrouiller;
    my $valeur = $obj->consulter(@_);
    $obj->deverrouiller;
    return $valeur;
}
sub consulter {
    my $obj = shift;
    shmread($obj-&>{SHMKEY}, my $tampon='', 0, MAXBUF)
        or die "shmread : $!";
    substr($tampon, index($tampon, "\0")) = '';
    return $tampon;
}
sub mettre {
    my $obj = shift;
    $obj->verrouiller;
    $obj->positionner(@_);
    $obj->deverrouiller;
}
sub positionner {
    my($obj,$msg) = @_;
    shmwrite($obj->{SHMKEY}, $msg, 0, MAXBUF) or die "shmwrite : $!";
}
sub verrouiller {
    my $obj = shift;
    $obj->{SEMA}->op(0,-1,0) or die "semop : $!";
}
sub deverrouiller {
    my $obj = shift;
    $obj->{SEMA}->op(0,1,0) or die "semop : $!";
}

Finalement, la classe a besoin d'un destructeur pour que, lorsque l'objet disparaît, nous puissions manuellement désallouer la mémoire partagée et le sémaphore stocké à l'intérieur de l'objet. Sinon, ils survivront à leur créateur et vous devrez recourir à ipcs et ipcrm (ou un administrateur système) pour vous débarrasser d'eux. C'est pourquoi nous somme passés par les enrobages élaborés dans le programme principal pour convertir les signaux en exceptions : pour que tous les destructeurs soient lancés, que tous les objets d'IPC SysV soient désalloués et que les administrateurs système ne s'occupent pas de notre cas.

 
Sélectionnez
sub DESTROY {
    my $obj = shift;
    return unless $obj->{PROPRIETAIRE} == $$;   # évite les désallocations
                                                # redondantes
    shmctl($obj->{SHMKEY}, IPC_RMID, 0) or warn "shmctl RMID : $!";
    $obj->{SEMA}->remove()
        or warn "sema->remove : $!";
}

16-5. Sockets

Les mécanismes d'IPC dont nous avons discuté auparavant souffrent tous d'une importante restriction : ils sont conçus pour une communication entre des processus tournant sur le même ordinateur. (Même si les fichiers peuvent parfois être partagés entre les machines à travers des mécanismes comme NFS, le verrouillage échoue lamentablement sur la plupart des implémentations de NFS, ce qui annihile une grande part des plaisirs des accès concurrents.) Pour des communications génériques sur le réseau, les sockets sont la solution de choix. Bien que les sockets aient été inventées sur BSD, elles se sont rapidement répandues aux autres formes d'Unix et de nos jours, vous pouvez trouver une interface pour les sockets sur à peu près tous les systèmes d'exploitation viables ici-bas. Si vous n'avez pas de sockets sur votre machine, vous allez rencontrer d'énormes difficultés pour utiliser l'Internet.

Avec les sockets, vous pouvez communiquer à la fois avec des circuits virtuels (les flux ou streams TCP) et avec des datagrammes (les paquets UDP). Votre système vous permet peut-être d'en faire plus encore. Mais le type de programmation socket le plus courant utilise TCP sur des sockets du domaine Internet, c'est donc celui que nous détaillerons ici. De telles sockets fournissent des connexions fiables fonctionnant un peu comme deux pipes bidirectionnels qui ne seraient pas limités à une seule machine. Les deux applications reines (killer apps) de l'Internet, l'email et le surf sur le Web, reposent presque exclusivement sur des sockets TCP.

Vous utilisez aussi largement UDP sans le savoir. Chaque fois que votre machine essaie de trouver un site sur l'Internet, elle envoie des paquets UDP à votre serveur DNS pour lui demander la véritable adresse IP. Vous pourriez utiliser UDP vous-même lorsque vous voulez envoyer et recevoir des datagrammes. Les datagrammes sont plus économiques que les connexions TCP précisément parce qu'ils ne sont pas orientés connexion ; c'est-à-dire qu'ils ressemblent moins à un appel téléphonique qu'à un dépôt de lettre dans une boîte. Mais il manque également à UDP la fiabilité offerte par TCP, ce qui le rend plus approprié pour les situations où vous ne vous souciez pas si un ou deux paquets se retrouvent happés, broyés et engloutis. Ou lorsque vous savez qu'un protocole de plus haut niveau fera respecter un certain degré de redondance ou d'échec en douceur (ce que fait DNS).

D'autres choix sont disponibles, mais bien moins courants. Vous pouvez utiliser des sockets du domaine Unix, mais elles ne fonctionnent que pour les communications locales. Divers systèmes implémentent divers autres protocoles qui ne sont pas basés sur IP. Ceux-ci intéressent sans doute quelqu'un quelque part pour faire quelque chose, mais nous devons nous restreindre quelquefois.

Les fonctions Perl s'occupant des sockets ont les mêmes noms que les appels système correspondants en C, mais leurs arguments ont tendance à différer pour deux raisons : premièrement, les handles de fichiers de Perl fonctionnent différemment des descripteurs C ; deuxièmement, Perl connaît déjà la longueur de ses chaînes et il est donc inutile de passer cette information. Voir les détails sur chaque appel système relatif aux sockets au chapitre 29.

Avec du code Perl ancien, on devait utiliser des valeurs codées en dur pour passer des constantes aux fonctions des sockets, ce qui annihilait la portabilité. Comme la plupart des appels système, ceux relatifs aux sockets renvoient discrètement, mais poliment undef lorsqu'ils échouent, au lieu de lever une exception. Il est donc essentiel de vérifier les valeurs renvoyées par ces fonctions, puisque si vous les jetez à la poubelle, elles n'iront pas crier cet échec sur les toits. Si jamais vous voyez du code faisant quelque chose comme positionner explicitement $AF_INET = 2, vous savez que vous êtes dans les problèmes jusqu'au cou. Une approche d'une supériorité incommensurable consiste à utiliser le module Socket ou le module encore plus sympathique IO::Socket, tous deux standard. Ces modules fournissent diverses constantes et des fonctions d'aide dont vous aurez besoin pour mettre en place des clients et des serveurs. Pour une réussite optimale, vos programmes de sockets devraient toujours commencer ainsi (n'oubliez pas non plus d'ajouter l'option de vérification de marquage -T sur la ligne shebang (commençant par #!) pour les serveurs) :

 
Sélectionnez
#!/usr/bin/perl -w
use strict;
use sigtrap;
use Socket; # ou IO::Socket

Comme nous l'avons fait remarquer ailleurs, Perl est à la merci de vos bibliothèques C pour la plupart de ses fonctionnalités concernant le système, et tous les systèmes n'implémentent pas toutes les sortes de sockets. Il est probablement plus sûr de s'en tenir avec les opérations sur les sockets normales TCP et UDP. Par exemple, si vous voulez que votre code ait une chance d'être portable vers des systèmes auxquels vous n'avez pas pensé, ne vous attendez pas à ce qu'ils implémentent un protocole fiable avec des paquets séquencés. Ne vous attendez pas non plus à passer des descripteurs de fichiers ouverts entre des processus n'ayant aucun rapport les uns avec les autres sur des sockets du domaine Unix. (Si, si, vous pouvez vraiment faire cela sur de nombreuses machines Unix — voir recvmsg(2) dans votre page de manuel locale.)

Si vous voulez juste utiliser un service standard de l'Internet comme le mail, les forums de discussion, le service de noms de domaine, FTP, Telnet, le Web et ainsi de suite, alors plutôt que de repartir de zéro, essayez d'utiliser les modules existants de CPAN. Les modules tout faits, conçus pour cela, comprennent : Net::SMTP (ou Mail::Mailer), Net:NNTP, Net::DNS, Net::FTP, Net::Telnet et les divers modules relatifs à HTTP. Les familles de modules libnet et libww comprennent toutes deux de nombreux modules individuels pour la communication en réseau. Les zones de modules sur CPAN qui peuvent vous intéresser sont dans la section 5 sur la Communication en réseau et dans la section 15, sur les Utilitaires de serveurs et de démons.

Dans les paragraphes qui suivent, nous présentons plusieurs exemples de clients et de serveurs sans grande quantité d'informations sur chaque fonction utilisée, car ce serait en grande partie une recopie pure et simple de ce que nous avons déjà écrit au chapitre 29, Fonctions.

16-5-a. Clients en réseau

Utilisez les sockets du domaine Internet lorsque vous voulez une communication client/serveur fiable entre des machines potentiellement différentes.

Pour créer un client TCP se connectant à un serveur quelque part, il est d'ordinaire plus facile d'utiliser le module standard IO::Socket::INET :

 
Sélectionnez
use IO::Socket::INET;

$socket = IO::Socket::INET->new(PeerAddr => $hote_distant,
                                PeerPort => $port_distant,
                                Proto => "tcp",
                                Type => SOCK_STREAM)
    or die "N'a pas pu se connecter à $hote_distant:$port_distant : $!\n";
# envoie quelque chose sur la socket,
print $socket "Pourquoi ne m'appelles-tu plus jamais ?\n";

# lit la réponse distante,
$reponse = <$socket>;

# et termine la connexion lorsque nous avons fini
close($socket);

Un raccourci pour l'appel est suffisant lorsque vous n'avez que l'hôte et le port auxquels se connecter et que vous voulez utiliser les valeurs par défaut pour les autres champs :

 
Sélectionnez
$socket = IO::Socket::INET->new("www.google.org:80")
    or die "N'a pas pu se connecter au port 80 de google: $!";

Pour se connecter en utilisant le module Socket :

 
Sélectionnez
use Socket;

# crée une socket
socket(Serveur, PF_INET, SOCK_STREAM, getprotobyname('tcp'));

# construit l'adresse de la machine distante
$adr_internet = inet_aton($hote_distant)
    or die "N'a pas pu convertir $hote_distant en adresse Internet : $!\n";
$adr_p = sockaddr_in($port_distant, $adr_internet);

# se connecte
connect(Serveur, $adr_p)
    or die "N'a pas pu se connecter à $hote_distant:$port_distant : $!\n";

select((select(Serveur), $| = 1)[0]); # active la mise en tampon
                                      # des commandes
# envoie quelque chose sur la socket
print Serveur "Pourquoi ne m'appelles-tu plus jamais ?\n";

# lit la réponse distante
$reponse = <Serveur>;

# et termine la connexion lorsque nous avons fini
close(Serveur);

Si vous ne voulez fermer que votre côté de la connexion, pour que l'extrémité distante reste reçoive une fin de fichier, mais que vous puissiez continuer à lire les données venant du serveur, utilisez l'appel système syscall pour une demi-fermeture :

 
Sélectionnez
# plus d'écriture vers le serveur
shutdown(Serveur, 1);     # constante Socket::SHUT_WR dans la v5.6
16-5-b. Serveurs en réseau

Voici le serveur correspondant qui va avec. Il est assez simple grâce à la classe standard IO::Socket::INET :

 
Sélectionnez
use IO::Socket::INET;
$serveur = IO::Socket::INET->new(LocalPort => $port_serveur,
                                 Type => SOCK_STREAM,
                                 Reuse => 1,
                                 Listen => 10 ) # ou SOMAXCONN
    or die "N'a pas pu être un serveur TCP sur le port $port_serveur : $!\n";

while ($client = $serveur->accept()) {
    # $client est la nouvelle connexion
}

close($serveur);

Vous pouvez également écrire cela en utilisant le module Socket, de plus bas niveau :

 
Sélectionnez
use Socket;

# crée la socket
socket(Serveur, PF_INET, SOCK_STREAM, getprotobyname('tcp'));

# pour pouvoir redémarrer rapidement notre serveur
setsockopt(Serveur, SOL_SOCKET, SO_REUSEADDR, 1);

# construit l'adresse de la socket
$mon_adr = sockaddr_in($port_serveur, INADDR_ANY);
bind(Serveur, $mon_adr)
    or die "N'a pas pu s'attacher au port $port_serveur : $!\n";

# établit une file pour connexions entrantes
listen(Serveur, SOMAXCONN)
    or die "N'a pas pu écouter sur le port $port_serveur : $!\n";

# accepte et traite les connexions
while (accept(Client, Serveur)) {
    # Fait quelque chose avec la nouvelle connexion Client
}

close(Server);

Le client n'a pas besoin de s'attacher avec bind à une adresse particulière, par contre c'est le cas pour le serveur. Nous avons spécifié son adresse en tant que INADDR_ANY, ce qui signifie que les clients peuvent se connecter depuis n'importe quelle interface réseau disponible. Si vous voulez vous placer devant une interface particulière (comme le côté externe d'une passerelle ou d'une machine pare-feux (firewall)), utilisez plutôt l'adresse réelle de cette interface. (Les clients peuvent également faire cela, mais en ont rarement besoin.)

Si vous voulez savoir quelle machine s'est connectée avec vous, appelez getpeername sur la connexion du client. Ceci renvoie une adresse IP, que vous devrez convertir vous-même vers un nom (si vous le pouvez) :

 
Sélectionnez
use Socket;
$autre_extremite = getpeername(Client)
    or die "N'a pas pu identifier l'autre extrémité : $!\n";
($port, $adr_i) = unpack_sockaddr_in($autre_extremite);
$ip_reelle = inet_ntoa($adr_i);
$nom_hote_pretendu = gethostbyaddr($adr_i, AF_INET);

Ceci être usurpé (spoofable) de manière triviale, car le propriétaire de cette adresse IP peut initialiser ses tables d'inversion (reverse tables) pour qu'elles disent ce qu'il veut. Une petite mesure pour s'assurer de la validité de l'adresse IP consiste à faire la conversion inverse :

 
Sélectionnez
@recherche_noms = gethostbyname($nom_hote_pretendu)
    or die "N'a pas pu faire la conversion inverse de $nom_hote_pretendu : $!\n";
@ips_resolues = map{inet_ntoa($_)} $recherche_noms[ 4 .. $#recherche_noms ];
$usurpation_potentielle = !grep { $ip_reelle eq $_ } @ips_resolues;

Une fois qu'un client s'est connecté à votre serveur, ce dernier peut faire des entrées/sorties à la fois depuis et vers ce handle client. Mais pendant que le serveur est ainsi occupé, il ne peut plus servir de requêtes entrantes provenant d'autres clients. Pour éviter de rester bloqué sur un seul client à la fois, de nombreux serveurs se clonent, avec fork, pour s'occuper de chaque connexion entrante. (D'autres anticipent les fork ou multiplexent les entrées/sorties entre plusieurs clients en utilisant l'appel système select.)

 
Sélectionnez
REQUETE: 
while (accept(Client, Serveur)) {
    if ($pid_fils = fork) {
        close Client;  # le père ferme le handle inutilisé
        next REQUETE;
    }
    defined($pid_fils) or die "fork impossible : $!" ;

    close Serveur;     # le fils ferme le handle inutilisé

    select(Client);    # nouveau handle par défaut pour les affichages
    $| = 1;            # vidage de tampon automatique

    # le code par connexion des fils fait ses entrées/sorties avec
    # le  handle Client
    $entree = <Client>;
    print Client "affichage\n"; # ou STDOUT, c'est la même chose

    open(STDIN, "<<&Client")
            or die "impossible de dupliquer le client : $!";
    open(STDOUT, ">&Client")
            or die "impossible de dupliquer le client : $!";
    open(STDERR, ">&Client")
            or die "impossible de dupliquer le client : $!";

    # lance la calculatrice, ce n'est qu'un exemple
    system("bc -l");              # ou ce que vous préférez, pourvu
                                  # qu'il n'y ait pas de séquences
                                  # d'échappement du shell !
    print "fini\n"; # toujours au client
    close Client;
    exit; # empêche le fils de retourner à accept
}

Ce serveur clone un fils avec fork pour chaque requête entrante. De cette manière, il peut s'occuper de plusieurs requêtes à la fois, tant que vous pouvez créer d'autres processus. (Il se pourrait que vous vouliez fixer une limite à ceci.) Même si vous ne faites pas de fork, le listen permettra d'avoir jusqu'à SOMAXCONN (généralement cinq ou plus) connexions en attente. Chaque connexion utilise des ressources, toutefois moins qu'un processus. Les serveurs faisant des fork doivent prendre soin de nettoyer une fois que leurs fils (appelés des zombies dans le jargon d'Unix) ont terminé sinon ils rempliraient vite votre table de processus. Le code FOSSOYEUR, présenté au paragraphe « Signaux » s'en occupera pour vous ou vous pouvez affecter $SIG{CHLD} = 'IGNORE'.

Avant de lancer une autre commande, nous connectons l'entrée et la sortie (et la sortie d'erreurs) standard à la connexion du client. De cette manière, toute commande lisant depuis STDIN et écrivant vers STDOUT peut également parler à la machine distante. Sans la réaffectation, la commande ne pourrait pas trouver le handle du client — qui, par défaut, est de toute façon fermé après l'exec.

Lorsque vous écrivez un serveur en réseau, nous vous encourageons fortement à activer la vérification du marquage par -T même si vous ne tournez pas sous setuid ou setgid. C'est toujours une bonne idée pour les serveurs et tout autre programme qui est lancé pour le compte de quelqu'un d'autre (comme tous les scripts CGI), car cela réduit les chances que des étrangers soient capables de compromettre votre système. Voir la section Gérer des données non sûres au chapitre 23, Sécurité, pour plus d'informations à ce sujet.

Une autre chose à prendre en considération lors de l'écriture de programme Internet : plusieurs protocoles spécifient que la fin de ligne devrait être CRLF, ce qui peut se spécifier de différentes façons : "\015\012" ou "\xd\xa" ou même chr(13).chr(10). Depuis la version 5.6 de Perl, écrire v13.10 produit également la même chaîne. (Sur plusieurs machines, vous pouvez également employer "\r\n" pour signifier, CRLF mais ne le faites pas si vous voulez être portable vers les Mac, car la signification de \r et de \n est inversée !) Plusieurs programmes Internet acceptent en fait un simple "\012" comme fin de ligne, mais c'est parce que les programmes Internet essaient en général d'être tolérants dans ce qu'ils acceptent et rigides dans ce qu'ils émettent. (Si seulement nous arrivions à pousser les gens à agir de la sorte...)

16-5-c. Passage de message

Comme nous l'avons évoqué auparavant, les communications UDP impliquent une surcharge bien moindre, mais ne fournissent aucune fiabilité, puisqu'il n'y a aucune promesse que les messages arriveront dans l'ordre — ou même qu'ils arriveront tout court. On dit souvent qu'UDP signifie Unreliable Datagram Protocol (protocole de datagrammes non fiable).

Néanmoins, UDP apporte des avantages par rapport à TCP, y compris la capacité de faire de la diffusion large (broadcast) ou multiple (multicast) à un ensemble complet d'hôtes destinataires en une seule fois (généralement utilisée sur votre sous-réseau local). Si vous vous trouvez excessivement soucieux à propos de la fiabilité et que vous commencez à construire des vérifications dans votre système de messages, vous devriez probablement commencer par utiliser TCP. Il est vrai qu'il est plus coûteux de mettre en place et de se séparer d'une connexion TCP, mais si vous pouvez amortir cela sur plusieurs messages (ou un message très long), cela importe peu.

De toute façon, voici un exemple d'un programme UDP. Il contacte le port UDP time des machines passées comme paramètres dans la ligne de commande ou toutes les personnes qu'il peut trouver en utilisant l'adresse universelle de broadcast si aucun argument n'a été donné.(138) Toutes les machines n'ont pas de serveur d'horloge activé, mais celles pour qui c'est le cas vous renverront un entier de 4 octets, empaqueté dans l'ordre « réseau », représentant le nombre de secondes depuis 1900. Vous devez soustraire le nombre de secondes entre 1900 et 1970 pour fournir cette date aux fonctions de conversion localtime ou gmtime.

 
Sélectionnez
#!/usr/bin/perl
# retardhorloge - compare les horloges d'autres systèmes avec celle-ci
#                 sans arguments, broadcast à quiconque est à l'écoute.
#                 attend une réponse pendant une demi-seconde.

use v5.6.0;  # or une version supérieure
use warnings;
use strict;
use Socket;

unshift(@ARGV, inet_ntoa(INADDR_BROADCAST))
    unless @ARGV;

socket(my $sock_mess, PF_INET, SOCK_DGRAM, getprotobyname("udp"))
    or die "socket : $!";

# certaines machines ont besoin de ceci. Ça ne devrait pas faire de
# mal aux autres.
setsockopt($sock_mess, SOL_SOCKET, SO_BROADCAST, 1)
    or die "setsockopt : $!";

my $no_port = getservbyname("time", "udp")
    or die "pas de port udp time";

for my $cible (@ARGV) {
    print "Envoi à $cible:$no_port\n";
    my $adr_dest_p = sockaddr_in($no_port, inet_aton($cible));
    send($sock_mess, "x", 0, $adr_dest_p)
        or die "send : $!";
}

# le service d'horloge renvoie une heure sur 32 bits en secondes
# depuis 1900
my $DEPUIS_1900_A_ORIGINE = 2_208_988_800;
my $fmt_heure = "N"; # et ceci en format binaire
my $lg_heure = length(pack($fmt_heure, 1)); # n'importe quel nombre
                                            # convient

my $masque_entree = ''; # chaîne pour stocker les bits de fileno bits
                        # pour le select
vec($masque_entree, fileno($sock_mess), 1) = 1;

# n'attend une entrée qu'une demi-seconde
while (select(my $masque_sortie = $masque_entree, undef, undef, 0.5)) {
    defined(my $adr_src_p = recv($sock_mess, my $heure_bin, $lg_heure, 0))
        or die "recv : $!";
    my($port, $adr_ip) = sockaddr_in($adr_src_p);
    my $hote_emet = sprintf "%s [%s]",
                      gethostbyaddr($adr_ip, AF_INET) || 'UNKNOWN',
                      inet_ntoa($adr_ip);
    my $delta = unpack($fmt_heure, $heure_bin) -
                      $DEPUIS_1900_A_ORIGINE -time();
    print "L'horloge sur $hote_emet est en avance de $delta
           secondes sur celle-ci.\n"; 
}

17. Threads

La programmation parallèle est beaucoup plus compliquée qu'elle ne semble l'être. Imaginez que vous preniez une recette dans un livre de cuisine et que vous la convertissiez en quelque chose sur lequel plusieurs douzaines de chefs cuisiniers pourraient travailler en même temps. Vous pouvez adopter deux approches.

La première consiste à donner à chaque chef sa cuisine privée, complètement équipée avec son propre matériel et ses propres ustensiles. Pour les recettes qui peuvent être facilement décomposées et pour les aliments qui peuvent être transportés d'une cuisine à une autre, cette approche fonctionne bien, car elle empêche chaque chef d'empiéter sur la cuisine de son voisin.

Sinon, il vous suffit de mettre tous les chefs dans une seule cuisine et de les laisser se débrouiller en décidant qui utilise le mixer et quand. Cela peut se transformer en pagaille, surtout lorsque les couteaux de boucher commencent à voler.

Ces deux approches correspondent à deux modèles de programmation parallèle sur les ordinateurs. Le premier est le modèle multiprocessus, typique des systèmes Unix traditionnels, dans lequel chaque thread(139) de contrôle possède son propre jeu de ressources. Le second modèle est le modèle multithread, dans lequel chaque thread de contrôle partage des ressources avec tous les autres threads de contrôle. Ou ne les partage pas, comme cela peut parfois arriver (et comme cela doit parfois arriver).

Nous savons tous que les chefs aiment prendre le contrôle ; ça ne pose pas de problème, car les chefs ont besoin de prendre le contrôle pour accomplir ce que nous voulons qu'ils accomplissent. Mais les chefs doivent s'organiser, d'une manière ou d'une autre.

Perl implémente les deux modèles d'organisation. Dans ce chapitre nous les appellerons le modèle de processus et le modèle de thread.

17-1. Le modèle de processus

Nous ne nous étendrons pas trop sur le modèle de processus ici, simplement parce qu'il est dominant tout au long du reste de ce livre. Perl est issu des systèmes Unix, il a donc adopté la notion que chaque processus accomplit son propre travail. Si un processus veut démarrer un traitement parallèle, il doit logiquement démarrer un processus parallèle ; c'est-à-dire qu'il doit faire un fork pour avoir un nouveau processus poids lourd, qui ne partage par défaut que très peu de choses avec son processus père, si ce n'est certains descripteurs de fichiers. (Le père et le fils peuvent sembler partager beaucoup plus, mais une grande partie de l'état du processus père est dupliquée dans le processus fils que réellement partagée au sens logique du terme. Le système d'exploitation peut bien entendu faire preuve de paresse quand il s'agit de forcer cette séparation logique, auquel cas nous appelons cela une sémantique de copie lors d'écritures (copy-on-write), mais nous ne ferions pas de copie du tout, s'il n'y avait pas en premier lieu de séparation logique.)

Historiquement, cette vision industrielle du traitement multiprocessus a posé de petits problèmes sur les systèmes Microsoft, car Windows n'avait pas de modèle multiprocessus bien développé (et il ne se reposait pas souvent sur ce qu'il avait à cet égard pour la programmation parallèle). Il a typiquement adopté à la place une approche multithread.

Toutefois, au prix d'efforts héroïques, la version 5.6 de Perl implémente maintenant l'opération fork sur Windows en clonant un nouvel objet interpréteur à l'intérieur du même processus. Cela signifie que la plupart des exemples utilisant fork dans le reste de ce livre fonctionneront maintenant sur Windows. L'interpréteur cloné partage du code immuable avec les autres interpréteurs, mais reçoit sa propre copie des données avec lesquelles il s'amusera. (Bien entendu, il peut toujours y avoir des problèmes avec les bibliothèques C qui ne comprennent pas les threads.)

Cette approche du traitement multiprocessus a été baptisée ithreads, comme raccourci pour « threads d'interpréteurs ». L'émulation de fork pour les systèmes Microsoft a déclenché l'implémentation des ithreads. Toutefois, nous avons rapidement réalisé que, bien les autres interpréteurs tournaient sous des threads distincts, ils tournaient dans le même processus, il serait donc facile de faire partager des données à ces interpréteurs séparés, même s'ils ne les partageaient pas par défaut.

C'est exactement l'inverse du modèle multithread typique, dans lequel tout est partagé par défaut et où vous devez vous échiner à ne pas partager quelque chose. Mais vous ne devriez pas voir ces deux modèles comme étant complètement distincts l'un de l'autre, car ils essaient tous deux de construire un pont au-dessus de la même rivière ; ils ne font que le construire sur des berges opposées. La véritable solution à tout problème de traitement parallèle est de mêler un certain degré de partage dans un certain degré d'égoïsme.

Ainsi, le but à long terme est d'étendre le modèle d'ithreads pour permettre autant de partage que vous avez besoin ou que vous voulez. Toutefois, à l'heure où nous écrivons ces lignes, la seule interface pour les ithreads visible par l'utilisateur est l'appel fork sous les portages Microsoft de Perl. Nous pensons que cette approche produira éventuellement des programmes plus propres que l'approche multithread standard. À la base, il est plus facile de gérer une économie où vous présumez que chacun possède ce qu'il a, plutôt que de présumer que tout le monde possède tout. Ce n'est pas que l'on attend des gens qu'ils ne partagent pas dans une économie capitaliste, ou qu'ils ne détournent pas de fonds publics(140) dans une économie communiste. Ces choses tendent à s'équilibrer. Il arrive que l'on soit dans une économie socialiste. Mais avec d'importants groupes de personnes, le partage de tout par défaut ne fonctionne que si vous avez un « chef en chef » avec un grand couteau de boucher, pensant que tout lui appartient.

Bien entendu, le véritable gouvernement de tout ordinateur est géré par un dictateur fasciste connu sous le nom de système d'exploitation. Mais un dictateur avisé sait lorsqu'il faut faire croire aux gens qu'ils sont capitalistes — et lorsqu'il faut leur faire croire qu'ils sont communistes.

17-2. Le modèle de thread

Le modèle de thread pour le traitement parallèle a tout d'abord été introduit dans Perl comme une fonctionnalité expérimentale dans la version 5.005. (Nous voulons dire par « modèle de thread », des threads partageant des ressources de données par défaut, non les nouveaux ithreads de la version 5.6.) Par certains côtés, ce modèle de thread est toujours une fonctionnalité expérimentale même en 5.6, car Perl est un langage riche et le traitement multithread peut mettre un fouillis même dans le plus simple des langages. Il existe toujours des coins et recoins de la sémantique de Perl qui n'interagissent pas très bien avec la notion que tout soit partagé. Le nouveau modèle des ithreads est une tentative pour contourner ces problèmes et, à un moment à l'avenir, il se peut que le modèle de thread actuel soit englobé dans le modèle d'ithread (lorsque nous aurons une interface aux ithreads qui dira « partagez tout ce que vous pouvez par défaut »). Mais malgré ses problèmes, l'actuel modèle de thread « expérimental » continue d'être très utile dans plusieurs situations du mode réel où il est préférable d'être un cobaye plutôt que de ne pas avoir de threads. Des applications raisonnablement robustes peuvent être écrites en Perl avec des threads, mais vous devez faire très attention. Vous devriez au moins envisager d'utiliser plutôt fork, si vous pouvez trouver un moyen de résoudre votre problème avec des pipes à la place de structures de données partagées.

Mais certains algorithmes sont plus simples à exprimer si de multiples tâches peuvent accéder facilement et efficacement au même jeu de données.(141) Cela produit du code plus petit et plus simple. Et puisque le noyau n'a pas à copier des tableaux de pages pour les données (même s'il fait de la copie lors d'écriture (copy-on-write)) lors de la création de thread, il devrait être plus rapide de démarrer une tâche de cette manière. De même, le changement de contexte peut être plus rapide si le noyau n'a pas besoin d'échanger (swap) des tableaux de pages. (En fait, pour les threads au niveau de l'utilisateur, le noyau n'est pas impliqué du tout — bien que les threads au niveau de l'utilisateur aient bien entendu des problèmes que les threads du noyau n'ont pas.)

Voici pour les bonnes nouvelles. Maintenant, passons à d'autres plus sujettes à réclamations. Nous avons déjà mentionné que le traitement des threads était quelque peu expérimental en Perl, mais, même s'il ne l'était pas, la programmation avec des threads est assez traître. La capacité d'un flux d'exécution à placer, qu'on le veuille ou non, des trous dans l'espace des données d'un autre, est susceptible de provoquer des désastres, plus que vous ne pouvez l'imaginer. Vous pourriez vous dire : « C'est facile à corriger, je n'ai qu'à mettre des verrous sur les données partagées. » D'accord, le verrouillage des données partagées est indispensable, mais disposer de protocoles de verrouillage qui soient corrects est une difficulté de notoriété publique, avec des erreurs générant des verrous mortels (deadlocks) ou des résultats non déterministes. Si vous avez des problèmes de synchronisation dans votre programme, l'emploi de threads va non seulement les aggraver, mais les rendra également plus difficiles à localiser.

Vous êtes non seulement responsable de l'honnêteté de vos propres données partagées, mais également de celle des modules Perl et des bibliothèques C que vous appelez. Votre code Perl a beau être sécurisé au niveau des threads à 100 %, si vous appelez un module ou un sous-programme C qui ne soit pas sûr au niveau des threads, vous êtes grillé. Vous devriez présumer qu'aucun module n'est sûr au niveau des threads à moins de le prouver. Ceci inclut même certains des modules standard. Peut-être même la plupart d'entre eux.

Nous ne vous avons pas déjà découragé ? Non ? Alors nous mettrons l'accent sur le fait que vous êtes quelque peu à la merci des bibliothèques de votre système d'exploitation pour le traitement des threads lorsqu'il s'agit de politiques d'ordonnancement et de préemption. Certaines bibliothèques de threads ne font du basculement de thread que sur les appels système bloquants. Certaines bloquent le processus entier si un seul thread fait un appel système bloquant. Certaines ne basculent les threads que sur l'expiration d'un délai quantifiable (soit d'un thread, soit d'un processus). Certaines ne basculent les threads qu'explicitement.

Oh, et pendant qu'on y est, si votre processus reçoit un signal, déterminer à quel thread ce signal est délivré est complètement dépendant du système.

Pour faire de la programmation de thread en Perl, vous devez compiler une version spéciale de Perl en suivant les instructions fournies dans le fichier README.threads dans le répertoire des sources de Perl. Il est à peu près garanti que ce Perl spécial tournera moins vite que votre exécutable Perl standard.

Ne présupposez pas que, puisque vous savez comment les threads sont programmés dans d'autres modèles (POSIX, DEC, Microsoft, etc.), vous savez comment les threads fonctionnent en Perl. Comme avec d'autres choses en Perl, Perl est du Perl, non du C++ ou du Java ou quoi que ce soit d'autre. Par exemple, il n'y a pas de priorités de thread en temps réel (et aucun moyen de contourner leur absence). Il n'y a pas non plus de verrous d'exclusion mutuelle (mutex). Utilisez simplement le verrouillage ordinaire ou peut-être le module Thread::Semaphore ou les mécanismes de cond_wait.

Toujours pas découragé ? Bien ! Car les threads sont vraiment sympas. Vous pouvez prévoir de vous amuser.

17-2-a. Le module Thread

L'interface actuelle pour les threads en Perl est définie par le module Thread. De plus, un nouveau mot-clef de Perl a été ajouté, l'opérateur lock. Nous parlerons de lock ultérieurement dans ce chapitre. Les autres modules standard de thread sont construits à partir de cette interface de base.

Le module Thread fournit ces méthodes de classe :

Méthode

Utilisation

new

Construit un nouvel objet Thread.

self

Renvoie mon objet Thread courant

list

Renvoie la liste des objets Thread.

Et pour les objets Thread, il fournit ces méthodes d'objet :

Méthode

Utilisation

join

Moissonne un thread (propage les erreurs).

eval

Moissonne un thread (capture les erreurs).

equal

Compare l'identité de deux threads.

tid

Renvoie l'ID de thread interne.

De plus, le module Thread fournit ces fonctions que l'on peut importer :

Fonction

Utilisation

yield

Dit à l'ordonnanceur de lancer un thread différent.

async

Construit un nouveau thread via une fermeture.

cond_signal

Réveille exactement un thread qui fait un cond_wait() sur une variable.

cond_broadcast

Réveille tous les threads qui font un cond_wait() sur une variable.

cond_wait

Attend sur une variable jusqu'à être réveillé par un cond_signCal() ou un cond_broadcast() sur cette variable.

17-2-a-i. Création de thread

Vous pouvez engendrer un thread de deux manières, soit en utilisant la méthode de classe Thread->new, soit en utilisant la fonction async. Dans tous les cas, la valeur renvoyée est un objet Thread. Thread->new prend une référence de code indiquant la fonction à lancer et des arguments à passe à cette fonction :

 
Sélectionnez
use Thread;
...
$t = Thread->new(\&fonc, $arg1, $arg2);

Vous vous retrouvez souvent à vouloir passer une fermeture comme premier argument sans fournir d'arguments supplémentaires :

 
Sélectionnez
my $quelque_chose;
$t = Thread->new( sub { dit($quelque_chose) } );

Dans ce cas spécial, la fonction async donne un relief dans l'écriture (c'est-à-dire du sucre de syntaxe) :

 
Sélectionnez
use Thread qw(async);
...
my $quelque_chose;
$t = async {
    dit($quelque_chose);
};

Vous remarquerez que nous avons importé explicitement la fonction async. Bien entendu, vous pouvez également utiliser le nom pleinement qualifié Thread::async à la place, mais alors votre sucre de syntaxe n'est plus aussi doux. Puisqu'async ne prend qu'une fermeture, tout ce que vous voulez lui passer doit être une variable lexicale qui soit à ce moment dans la portée.

17-2-a-ii. Destruction de thread

Une fois commencé — et sujet aux lubies de votre bibliothèque de thread — le thread continuera à tourner tout seul jusqu'à ce que sa fonction de plus haut niveau (celle que vous avez passée au constructeur) renvoie un résultat. Si vous voulez terminer un thread plus tôt, il vous suffit de faire return depuis l'intérieur de cette fonction de plus haut niveau.(142)

Maintenant, tout est parfait pour que votre thread se termine en renvoyant un résultat, mais à qui le renvoyer ? Le thread qui a engendré ce thread-ci est vraisemblablement parti faire d'autres choses et n'attend plus de réponse à l'appel de la méthode. La réponse est assez simple : le thread attend jusqu'à ce que quelqu'un émette un appel de méthode qui fasse l'attente d'une valeur de retour. Cet appel de méthode s'appelle join (joindre), car il fait se rejoindre de manière conceptuelle deux threads en un seul :

 
Sélectionnez
$val_ret = $t->join();  # moissonne le thread $t

L'opération join est une réminiscence de waitpid sur un processus fils. Si le thread a déjà terminé, la méthode join rend la main immédiatement avec la valeur renvoyée par la fonction principale du thread. Si le thread n'a pas fini, join se comporte comme un appel bloquant qui interrompt le thread appelant indéfiniment. (Il n'y a pas de mécanisme de minuterie (time-out).) Lorsqu'éventuellement le thread s'achève, le join renvoie une valeur.

Toutefois, au contraire de waitpid, qui ne peut moissonner que les propres fils d'un processus, n'importe quel thread peut faire un join sur n'importe quel autre thread à l'intérieur du processus. C'est-à-dire qu'il n'est pas nécessaire que le thread qui fait le join soit le thread principal ou le thread parent. La seule restriction est qu'un thread ne peut pas faire de join sur lui-même (ce qui reviendrait à officier pour vos propres funérailles) et qu'un thread ne peut pas faire de join sur un thread qui a déjà répondu à un join (ce qui équivaudrait à deux directeurs des pompes funèbres se battant au-dessus de la dépouille). Si vous essayez de faire l'une de ces choses, une exception sera levée.

La valeur de retour de join ne doit pas forcément être une valeur scalaire — elle peut également être une liste :

 
Sélectionnez
use Thread 'async';

$t1 = async {
    my @trucs = getpwuid($>);
    return @trucs;
};

$t2 = async {
    my $mess = `cat /etc/motd`;
    return $mess;
};

@liste_ret = $t1->join();
$val_ret = $t2->join();

print "Le 1er fils a renvoyé @liste_ret\n";
print "Le 2ème fils a renvoyé $val_ret\n";

En fait, l'expression renvoyée par un thread est toujours évaluée dans un contexte de liste, même si join est appelé dans un contexte scalaire, auquel cas la dernière valeur de la liste est renvoyée.

17-2-a-iii. Capture des exceptions avec join

Si un thread se termine avec une exception non capturée, cela ne tuera pas immédiatement le programme entier. Cela serait embêtant. À la place, lorsqu'un join est lancé sur ce thread, le join lève lui-même l'exception. L'emploi de join sur un thread indique une volonté de propager toute exception levée par ce thread. Si vous préférez capturer l'exception à ce moment et à cet endroit, utilisez la méthode eval, qui comme la fonction interne du même nom, fait que l'exception est mise dans $@ :

 
Sélectionnez
$val_ret = $t->eval();           # capture les erreurs lorsque les
                                 # threads se rejoignent
if ($@) {
    warn "échec du thread : $@";
}
else {
    print "le thread a renvoyé $ret_val\n";
}

Bien qu'il n'y ait pas de règle à cet effet, vous pourriez vouloir adopter l'habitude de ne rejoindre un thread que depuis le thread qui a créé celui que vous rejoignez. C'est-à-dire que vous ne moissonnez un thread fils que depuis le thread père qui l'a engendré. Cela rend plus facile la conservation d'une trace indiquant quelles exceptions vous auriez besoin de capturer et quand le faire.

17-2-a-iv. La méthode detach

Comme autre solution pour arrêter les threads, si vous n'avez pas prévu de rejoindre un thread plus tard avec join pour obtenir sa valeur de retour, vous pouvez appeler la méthode detach sur ce thread pour que Perl le nettoie à votre place. C'est un peu comme lorsqu'un processus est hérité par le programme init sur Unix, sauf que le seul moyen de faire cela sur Unix est que le processus père meure.

La méthode detach ne met pas le thread « en arrière-plan » ; si vous essayez de sortir du programme principal et qu'un thread détaché est toujours en train de tourner, la fin du programme bloquera jusqu'à ce que le thread lui-même se termine. Mais plutôt, la méthode detach vous épargne les tâches de nettoyage. Elle indique purement et simplement à Perl de ne pas garder la valeur de retour, ni la valeur de sortie du thread après qu'il a fini. En un sens, detach dit à Perl de faire un join implicite lorsque le thread finit puis de jeter les résultats. Cela peut avoir de l'importance : si vous n'avez jamais fait de join, ni de detach, sur un thread qui renvoie une liste très grande, cet espace de stockage sera perdu jusqu'à la fin. En effet, Perl devrait s'agripper à lui dans la perspective lointaine (mais alors très lointaine, dans ce cas), où quelqu'un voudrait faire un join sur ce thread à un moment à l'avenir.

Une exception levée dans un thread fils détaché ne se propage plus non plus durant un join, puisque les threads ne s'uniront jamais. Utilisez sagement eval {} dans la fonction de plus haut niveau et trouvez d'autres moyens de rapporter les erreurs.

17-2-a-v. Identification des threads

Chaque thread de Perl possède un numéro d'identification de thread distinct, qui est celui que la méthode tid renvoie :

 
Sélectionnez
$son_num_tid = $t1->tid();

Un thread peut accéder à son propre objet thread par l'intermédiaire de l'appel Thread->self. Ne confondez pas cela avec l'ID du thread : pour calculer son propre ID, un thread fait ceci :

 
Sélectionnez
$mon_tid = Thread->self->tid(); # $$ pour les threads, pour ainsi dire

Pour comparer un objet thread avec un autre, employez l'une de ces techniques :

 
Sélectionnez
Thread::equal($t1, $t2)
$t1->equal($t2)
$t1->tid() == $t2->tid()
17-2-a-vi. Liste des threads courants

Vous pouvez obtenir une liste des objets threads courants dans le processus courant en utilisant l'appel de méthode de classe Thread->list. La liste comprend à la fois les threads en train de tourner et ceux qui ont fini, mais sur lesquels aucun join n'a encore été appliqué. Vous pouvez faire ceci depuis n'importe quel thread.

 
Sélectionnez
for my $t (Thread->list()) {
    printf "$t a un tid = %d\n", $t->tid();
}
17-2-a-vii. Priorité au processeur

Le module Thread implémente une fonction, que vous pouvez importer, appelée yield. Elle fait en sorte que le thread appelant abandonne le processeur. Malheureusement, les détails de ce mécanisme sont complètement dépendants votre version d'implémentation des threads. Néanmoins, abandonner le contrôle de la CPU occasionnellement est considérée comme un geste d'amabilité :

 
Sélectionnez
use Thread 'yield';
yield();

L'utilisation des parenthèses n'est pas une obligation. C'est même encore plus sûr, syntaxiquement parlant, car la faute de frappe apparemment inévitable « yeild » est alors interceptée :

 
Sélectionnez
use strict;
use Thread 'yield';
yeild;           # Le compilateur se désole, puis arrête tout
yield;           # Ok.
17-2-b. Accès aux données

Ce que nous savons passé en revue jusqu'ici n'était pas bien compliqué ; mais nous sommes sur le point d'y remédier. Rien de ce que nous avons fait ne mettait en pratique la nature parallèle des threads. L'accès aux données partagées change tout cela.

Le code mis dans un thread en Perl possède les mêmes limites en ce qui concerne la visibilité des données que tout autre morceau de code en Perl. On a toujours accès aux variables globales via des tables de symboles globales et aux variables lexicales via une portée lexicale (mémoire de travail, scratchpad).

Toutefois, le fait que de multiples threads de contrôle existent dans le programme génère des erreurs dans les travaux. Deux threads ne peuvent être autorisés à accéder à la même variable globale simultanément ou ils peuvent s'écraser l'un l'autre. (Le résultat de l'écrasement dépend de la nature de l'accès.) De même, deux threads ne peuvent être autorisés à accéder à la même variable lexicale simultanément, car les variables lexicales se comportent également comme les globales si elles sont déclarées en dehors de la portée de fermetures utilisées par des threads. Démarrer des threads via des références de sous-programmes (en utilisant Thread->new) plutôt que via des fermetures (en utilisant async) peut aider à limiter l'accès aux variables lexicales, si c'est ce que vous voulez. (Parfois ce n'est cependant pas le cas.)

Perl résout le problème pour certaines variables spéciales internes, comme $!, $_, @_ et les autres variables comme celles-ci, en en faisant des données spécifiques au thread. La mauvaise nouvelle est que toutes vos variables de paquetage, basiques, que vous utilisez quotidiennement, ne sont pas protégées des écrasements.

La bonne nouvelle est que vous n'avez généralement pas à vous inquiéter à propos de vos variables lexicales, en supposant qu'elles soient déclarées à l'intérieur du thread courant, puisque chaque thread instanciera l'entrée de sa propre portée lexicale, qui est distincte de celle de tout autre thread. Vous ne devez vous soucier des variables lexicales que si elles sont partagées entre les threads, par exemple en passant des références ou en référençant les variables lexicales à l'intérieur de fermetures utilisées par de multiples threads.

17-2-b-i. Synchronisation des accès avec lock

Lorsque plusieurs agents peuvent accéder au même élément en même temps, des collisions surviennent, exactement comme à un carrefour. Le verrouillage du carrefour est votre seule défense.

La fonction interne lock est le mécanisme de Perl correspondant à un feu tricolore pour le contrôle des accès. Bien que lock soit en quelque sorte un mot-clef, c'est un mot-clef timide, car la fonction interne n'est pas utilisée si le compilateur a déjà vu une définition sub lock {} dans le code de l'utilisateur. Ceci est destiné à assurer une compatibilité antérieure. Cependant, CORE::lock désigne toujours la fonction interne. (Dans un perl qui n'a pas été compilé pour le traitement des threads, l'appel de lock n'est pas une erreur ; c'est une opération sans danger ne faisant rien (no-op), du moins dans les versions récentes.)

Exactement de la même manière que l'opérateur flock ne bloque que d'autres instances de flock, et non les véritables entrées/sorties, l'opérateur lock également, ne bloque que d'autres instances de lock, et non les accès ordinaires aux données. Ce sont, en effet, des verrous consultatifs. Exactement comme les feux tricolores.(143)

Vous pouvez faire un lock sur des variables scalaires individuelles aussi bien que sur des tableaux ou des hachages entiers.

 
Sélectionnez
lock $var;
lock @valeurs;
lock %hachage;

Cependant, l'emploi de lock sur des valeurs agrégées ne verrouille pas implicitement les composants scalaires de ces valeurs agrégées :

 
Sélectionnez
lock @valeurs;         # dans le thread 1
...
lock $valeurs[23];     # dans le thread 2 --- ne verrouille pas !

Si vous verrouillez une référence, cela verrouille automatiquement l'accès au référant. C'est-à-dire que vous obtenez gratuitement une déréférence. Ceci est bien commode, car les objets sont toujours cachés derrière une référence et vous voulez souvent verrouiller des objets. (Et vous ne voulez presque jamais verrouiller des références.)

Le problème avec les feux tricolores, bien entendu, est qu'ils sont rouges la plupart du temps et que vous devez attendre. De même, lock est un appel bloquant — votre thread sera suspendu à cet endroit jusqu'à ce que le verrou soit accordé. Il n'y a pas de mécanisme de minuterie (time-out). Il n'y a pas non plus de mécanisme de déverrouillage, car les verrous ont une portée dynamique. Ils durent jusqu'à ce que leur bloc, leur fichier ou leur eval soit fini. Lorsqu'ils sortent de la portée, ils sont automatiquement libérés.

Les verrous sont également récursifs. Cela signifie que si vous verrouillez une variable dans une fonction et que cette fonction s'appelle récursivement pendant que vous détenez le verrou, le même thread pourra verrouiller avec succès la même variable à nouveau. Le verrou est finalement abandonné lorsque l'on est sorti de tous les niveaux détenant les verrous.

Voici une démonstration simple de ce qui peut se passer si vous n'avez pas verrouillé. Nous forçons un changement de contexte en utilisant yield pour montrer le genre de problème qui peut également arriver accidentellement avec un ordonnanceur préemptif :

 
Sélectionnez
use Thread qw/async yield/;
my $var = 0;
sub une_inflation {
    if ($var == 0) {
        yield;
        $var++;
    }
}

my $t1 = new Thread \&une_inflation;
my $t2 = new Thread \&une_inflation;

for my $t ($t1, $t2) { $t->join }
print "var vaut $var\n";

Ce code affiche toujours 2 (pour une certaine définition de « toujours »), car nous avons décidé de l'inflation après avoir vu que la variable valait 0, mais avant que nous puissions le faire, un autre thread a décidé d'en faire autant.

Nous pouvons résoudre cette collision par l'ajout trivial d'un verrou avant d'examiner $var. Maintenant ce code affiche toujours 1 :

 
Sélectionnez
sub une_inflation {
    lock $var;
    if ($var == 0) {
        yield;
        $var++;
    }
}

Souvenez-vous qu'il n'y a pas de fonction unlock explicite. Pour contrôler le déverrouillage, vous n'avez qu'à ajouter un autre niveau de portée, qui soit imbriqué, pour que le verrou soit relâché lorsque cette portée se termine :

 
Sélectionnez
sub une_inflation {
    {
        lock $var;
        if ($var == 0) {
            yield;
            $var++;
        }
    } # le verrou est relâché ici !
    # autre code avec $var déverrouillée
}
17-2-b-ii. Verrous mortels (deadlocks)

Le verrou mortel (deadlock) est le fléau des programmeurs de threads, car il est facile d'en créer par accident et difficile de l'éviter. Voici un exemple simple de verrou mortel :

 
Sélectionnez
my $t1 = async {
    lock $a; yield; lock $b;
    $a++; $b++
};
my $t2 = async {
    lock $b; yield; lock $a;
    $b++; $a++
};

La solution ici est que tous les protagonistes qui ont besoin d'un ensemble de verrous particulier s'en saisissent dans le même ordre.

C'est également une bonne chose de minimiser la durée pendant laquelle vous détenez des verrous. (Au moins, c'est une bonne chose de le faire pour des questions de performance. Mais si vous le faites pour réduire les risques de verrou mortel, tout ce que vous faites est de le rendre plus difficile à reproduire et à diagnostiquer le problème.)

17-2-b-iii. Verrouillage des sous-programmes

Vous pouvez également mettre un verrou sur un sous-programme :

 
Sélectionnez
lock &fonc;

Au contraire des verrous sur les données, qui ne sont que consultatifs, les verrous de sous-programmes sont obligatoires. Personne d'autre que le thread détenant le verrou peut entrer dans le sous-programme.

Considérez le code suivant, qui contient des situations de concurrence (race conditions) impliquant la variable $fini. (Les yield ne sont mis qu'à titre de démonstration.)

 
Sélectionnez
use Thread qw/async yield/;
my $fini = 0;
sub bricole {
    my $arg = shift;
    my $tid = Thread->self->tid;
    print "thread $tid : bricole $arg\n";
    yield;
    unless ($fini) {
        yield;
        $fini++;
        bricole($arg + 10);
    }
}

Si vous le lancez de cette façon :

 
Sélectionnez
my @t;
for my $i (1..3) {
    push @t, Thread->new(\&bricole, $i);
}
for (@t) { $_->join }
print "fini vaut $fini\n";

voici l'affichage (enfin, certaines fois — ce n'est pas déterministe) :

 
Sélectionnez
thread 1 : bricole 1
thread 2 : bricole 2
thread 3 : bricole 3
thread 1 : bricole 11
thread 2 : bricole 12
thread 3 : bricole 13
fini vaut 3

Toutefois, si vous le lancez de cette façon :

 
Sélectionnez
for my $i (1..3) {
    push @t, async {
        lock &bricole;
        bricole($i);
    };
}
for (@t) { $_->join }
print "fini vaut $fini\n";

voici l'affichage :

 
Sélectionnez
thread 1 : bricole 1
thread 1 : bricole 11
thread 2 : bricole 2
thread 3 : bricole 3
fini vaut 1
17-2-b-iv. L'attribut locked

Bien qu'il soit obligatoire d'obéir à un verrou de sous-programmes, rien ne force personne à les verrouiller en premier lieu. Mais certains sous-programmes aimeraient vraiment être capables d'exiger d'être verrouillés avant d'être appelés.

L'attribut de sous-programme locked remplit cette fonction. Il est plus rapide que l'appel, lock &fonc car il est connu à la compilation, non seulement à l'exécution. Mais le comportement est le même que lorsque nous l'avons verrouillé explicitement auparavant. Voici la syntaxe :

 
Sélectionnez
sub bricole : locked {
    # comme avant
}

Si vous avez un prototype de fonction, il vient entre le nom et tous les attributs :

 
Sélectionnez
sub bricole ($) : locked {
    # comme avant
}
17-2-b-v. Verrouillage des méthodes

Le verrouillage automatique d'un sous-programme est vraiment très pratique, mais il en fait parfois un peu trop. Lorsque vous invoquez une méthode d'objet, cela n'a généralement aucune importance si de multiples méthodes sont lancées simultanément pour autant qu'elles le soient toutes pour le compte d'objets différents. Vous aimeriez donc vraiment verrouiller plutôt l'objet sur lequel cette méthode est appelée. L'ajout d'un attribut method à la définition du sous-programme fait ceci :

 
Sélectionnez
sub bricole : locked method {
    # comme avant
}

Si ce code est appelé en tant que méthode, l'objet qui l'invoque est verrouillé, fournissant un accès séquentiel à cet objet, mais permettant à la méthode d'être appelée sur d'autres objets. Si la méthode n'est pas appelée sur un objet, l'attribut essaie toujours de faire ce qu'il faut : si vous appelez une méthode verrouillée comme une méthode de classe (Paquetage->new plutôt que $obj->new), la table de symboles du paquetage est verrouillée. Si vous appelez une méthode verrouillée comme un sous-programme normal, Perl lèvera une exception.

17-2-b-vi. Variables de condition

Une variable de condition permet à un thread d'abandonner le processeur jusqu'à ce qu'un certain critère soit satisfait. Les variables de conditions indiquent des points de coordination entre les threads lorsque vous avez besoin de plus de contrôle que ce qu'un seul verrou fournit. D'un autre côté, vous n'avez pas vraiment besoin de plus de temps système (overhead) qu'un verrou ne fournit et les variables de condition sont conçues dans cet esprit. Vous n'avez qu'à utiliser des verrous ordinaires plus des conditions ordinaires. Si la condition échoue, vous devrez alors prendre des mesures extraordinaires via la fonction cond_wait ; mais nous optimisons pour les chances de réussite, puisque dans une application bien conçue, nous ne devrions pas de toute façon nous trouver dans un goulot d'étranglement sur la condition courante.

Outre le verrouillage et le test, les opérations de base sur les variables de condition consistent à envoyer ou recevoir un événement « signal » (non un signal réel au sens de %SIG). Soit, vous suspendez votre exécution pour attendre la réception d'un événement, soit vous envoyez un événement pour réveiller d'autres threads attendant une condition particulière. Le module Thread fournit trois fonctions, que vous pouvez importer, pour faire cela : cond_wait, cond_signal et cond_broadcast. Ce sont les mécanismes primaires sur lesquels se basent des modules plus abstraits comme Thread::Queue et Thread::Semaphore. Il est souvent plus commode d'utiliser ces abstractions, lorsque c'est possible.

La fonction cond_wait() prend une variable déjà verrouillée par le thread courant, déverrouille cette variable, puis bloque jusqu'à ce qu'un autre thread fasse un cond_signal ou un cond_broadcast pour la même variable verrouillée.

La variable bloquée par cond_wait est reverrouillée après que le retour de cond_wait. Si de multiples threads font un cond_wait sur la même variable, tous rebloquent sauf un car ils ne peuvent regagner le verrou sur la variable. Ainsi, si vous n'utilisez que cond_wait pour la synchronisation, abandonnez le verrou dès que possible.

La fonction cond_signal prend une variable déjà verrouillée par le thread courant et débloque l'un des threads qui étaient actuellement dans un cond_wait sur cette variable. Si plus d'un thread bloquait dans un cond_wait sur cette variable, un seul est débloqué et vous ne pouvez pas prédire lequel. Si aucun thread ne bloquait dans un cond_wait sur cette variable, l'événement est éliminé.

La fonction cond_broadcast fonctionne comme, cond_signal mais elle débloque toutes les threads bloqués dans un cond_wait sur la variable verrouillée, non un seul thread. (Bien entendu, c'est toujours le cas s'il n'y a qu'un seul thread détenant le verrou.)

La fonction cond_wait est destinée à être le genre de chose qu'un thread n'emploie en dernier ressort que si la condition qu'il veut n'est pas remplie. Le cond_signal et le cond_broadcast indiquent que la condition est en train de changer. Le déroulement des actions est supposé être le suivant : verrouillez, puis vérifiez pour voir si la condition que vous voulez est remplie ou non ; si c'est le cas, c'est bon, sinon, faites un cond_wait jusqu'à ce que ce soit bon. L'accent devrait être mis sur le fait d'éviter de bloquer autant que possible. (C'est généralement un bon conseil lorsque l'on manipule des threads.)

Voici un exemple où le contrôle passe d'un thread à l'autre. Ne soyez pas bluffé par le fait que les conditions réelles sont reléguées dans la partie droite des modificateurs d'instruction ; cond_wait ne sera jamais appelé sauf si la condition que nous attendons est fausse.

 
Sélectionnez
use Thread qw(async cond_wait cond_signal);
my $var_wait = 0;
async {
    lock $var_wait;
    $var_wait = 1;
    cond_wait $var_wait until $var_wait == 2;
    cond_signal($var_wait);
    $var_wait = 1;
    cond_wait $var_wait until $var_wait == 2;
    $var_wait = 1;
    cond_signal($wait_var);
};

async {
    lock $var_wait;
    cond_wait $var_wait until $var_wait == 1;
    $var_wait = 2;
    cond_signal($var_wait);
    cond_wait $var_wait until $var_wait == 1;
    $var_wait = 2;
    cond_signal($wait_var);
    cond_wait $var_wait until $var_wait == 1;
};
17-2-c. Autres modules de thread

Plusieurs modules sont construits au-dessus de la primitive cond_wait.

17-2-c-i. Files d'attente (queues)

Le module standard Thread::Queue fournit deux moyens de passer des objets entre des threads sans s'inquiéter des verrous ou de la synchronisation. Cette interface est bien plus facile :

Méthode

Utilisation

new

Construit un nouvel objet Thread::Queue.

enqueue

Pousse un ou plusieurs scalaires à la fin de la file.

dequeue

Enlève le premier scalaire du début de la file. La méthode dequeue bloque
s'il n'y a plus d'éléments.

Remarquez comme une file d'attente est similaire à un pipe ordinaire, sauf qu'au lieu d'envoyer des octets, vous arrivez à passer des scalaires purs et durs, y compris des références et des objets consacrés avec bless.

Voici un exemple dérivé de la page de manuel de perlthrtut :

 
Sélectionnez
use Thread qw/async/;
use Thread::Queue;

my $Q = Thread::Queue->new();
async {
    while (defined($donnee = $Q->dequeue)) {
        print "$donnee retiré de la file\n";
    }
};

$Q->enqueue(12);
$Q->enqueue("A", "B", "C");
$Q->enqueue($thr);
sleep 3;
$Q->enqueue(\%ENV);
$Q->enqueue(undef);

Et voici ce que vous obtenez comme affichage :

 
Sélectionnez
12 retiré de la file A retiré de la file
B retiré de la file
C retiré de la file
Thread=SCALAR(0x8117200) retiré de la file
HASH(0x80dfd8c) retiré de la file

Remarquez comment $Q était dans la portée lorsque le thread asynchrone a été lancé via une fermeture de async. Les threads respectent les mêmes règles concernant les portées que n'importe quoi d'autre en Perl. L'exemple ci-dessus n'aurait pas fonctionné si $Q avait été déclaré après l'appel à async.

17-2-c-ii. Sémaphores

Thread::Semaphore vous fournit des objets sémaphores, sûrs vis-à-vis des threads et capables de compter, pour implémenter vos opérations favorites p() et v(). Comme la plupart d'entre nous n'associent pas ces opérations avec les mots néerlandais passeer (« passer ») et verlaat (« laisser »), le module appelle ces opérations « down » (diminuer) et « up » (augmenter), respectivement. (On trouvera dans la littérature, « wait » (attendre) ou « signal » (signaler).) Les méthodes suivantes sont implémentées :

Méthode

utilisation

new

Construit un nouvel objet Thread::Semaphore.

down

Alloue un ou plusieurs éléments.

up

Désalloue un ou plusieurs éléments.

La méthode new crée un nouveau sémaphore et initialise son compteur au nombre spécifié. Si aucun nombre n'est spécifié, le compteur du sémaphore est positionné à 1. (Le nombre représente une certaine réserve d'élément qui peut « s'épuiser » si tous les éléments sont désalloués.)

 
Sélectionnez
use Thread::Semaphore;
$mutex = Thread::Semaphore->new($MAX);

La méthode down décrémente le compteur du sémaphore du nombre spécifié, ou de 1 si aucun nombre n'est donné. Elle peut être interprétée comme une tentative d'allocation d'une partie ou de la totalité d'une ressource. Si le compteur du sémaphore descend en dessous de zéro, cette méthode bloque jusqu'à ce qu'elle compteur soit supérieur ou égal à la quantité que vous avez demandée. Appelez-le comme ceci :

 
Sélectionnez
$mutex->down();

La méthode up incrémente le compteur du sémaphore du nombre spécifié, ou de 1 si aucun nombre n'est donné. Elle peut être interprétée comme une tentative de désallocation d'une certaine quantité d'une ressource précédemment allouée. Ceci débloque au moins un thread qui était bloqué en essayant de faire un down sur le sémaphore, pourvu que le up augmente le compteur du sémaphore au-dessus de ce que le down essaie de le diminuer. Appelez-le comme ceci :

 
Sélectionnez
$mutex->up();
17-2-c-iii. Autres modules standard pour le traitement des threads

Thread::Signal vous permet de démarrer un thread qui est désigné pour recevoir les signaux %SIG de votre processus. Ceci traite le problème toujours vexant des signaux qui ne sont pas fiables tels qu'ils sont actuellement implémentés dans Perl et leur utilisation imprudente peut de temps en temps causer des core dumps.

Ces modules sont toujours en développement et peuvent ne pas entraîner l'effet escompté sur votre système. D'un autre côté, ils peuvent produire les résultats désirés. Si ce n'est pas le cas, c'est parce que quelqu'un comme vous ne les a pas corrigés. Peut-être que quelqu'un comme vous devrait se lancer et donner un coup de main.

18. Compilation

Si vous êtes venu ici pour trouver un compilateur Perl, vous serez peut-être surpris d'apprendre que vous en avez déjà un — votre programme perl (généralement /usr/bin/perl) renferme déjà un compilateur Perl. Ce n'est peut-être pas ce que vous pensiez et si tel est le cas, vous serez heureux d'apprendre que nous fournissons également des générateurs de code (que certaines personnes bien pensantes appellent « compilateurs ») et nous en discuterons vers la fin de ce chapitre. Mais nous voulons tout d'abord parler de ce que nous pensons être Le Compilateur. Il y aura inévitablement dans ce chapitre, une certaine quantité de petits détails qui intéresseront certains et ennuieront d'autres. Si vous pensez que vous n'êtes pas intéressé, considérez cela comme l'occasion d'entraîner vos facultés de lecture à grande vitesse.

Imaginez que vous êtes un chef d'orchestre ayant commandé la partition pour une œuvre immense. Lorsque le coffret arrive, vous trouvez plusieurs douzaines de livrets, un pour chaque membre de l'orchestre, ne contenant que sa propre partition. Mais il manque curieusement votre copie principale avec toutes les partitions. Encore plus curieusement, les partitions que vous avez écrites vous-même sont retranscrites en français au lieu d'être en notation musicale. Avant que vous puissiez mettre en place un concert susceptible d'être présenté au public, ou même simplement donner la musique à jouer à votre orchestre, il vous faudra traduire les descriptions en prose vers le système normal de notes et de mesures. Puis vous aurez besoin de compiler les partitions individuelles en une seule gigantesque pour que vous ayez une idée du programme dans son ensemble.

De même, lorsque vous remettez le code source de votre script Perl à l'exécutable perl pour qu'il l'exécute, il n'est pas plus utile à l'ordinateur que ne l'était la description en français de la symphonie aux musiciens. Avant que votre programme puisse tourner, Perl a besoin de compiler(144) ces directives ressemblant à un langage naturel vers une représentation symbolique spéciale. Votre programme ne tourne pourtant toujours pas, car le compilateur ne fait que compiler. Comme la partition du chef d'orchestre, même après que votre programme a été converti en un format d'instructions convenant à l'interprétation, il a encore besoin d'un agent actif pour interpréter ces instructions.

18-1. Cycle de vie d'un programme Perl

Vous pouvez décomposer le cycle de vie d'un programme Perl en quatre phases distinctes, chacune avec ses propres étapes indépendantes. La première et la dernière sont les plus intéressantes et les deux du milieu sont optionnelles. Ces phases sont décrites à la figure 18-1.

Image non disponible
Figure 18-1. Le cycle de vie d'un programme Perl

1. La phase de compilation

  • Durant la phase 1, la phase de compilation, le compilateur convertit votre programme vers une structure de données appelée un arbre syntaxique. En plus des techniques traditionnelles d'analyse syntaxique, Perl en emploie une bien plus puissante : il utilise les blocs BEGIN pour guider toute compilation plus approfondie. Les blocs BEGIN sont passés à l'interpréteur pour être lancés dès qu'ils ont été analysés, ce qui les lance effectivement dans l'ordre du premier arrivé (FIFO : first in, first out). Ceci comprend toutes les déclarations use et no ; celles-ci ne sont que des blocs BEGIN déguisés. Tous les blocs CHECK, INIT et END sont programmés par le compilateur pour une exécution différée.
  • Les déclarations lexicales sont notées, mais leurs affectations ne sont pas exécutées. Toutes les constructions eval BLOC, s///e et les expressions régulières non interpolées sont compilées ici et les expressions constantes sont préévaluées. Le compilateur a maintenant fini, à moins qu'il ne soit rappelé au travail plus tard. À la fin de cette phase, l'interpréteur est à nouveau appelé pour exécuter tous les blocs CHECK programmés dans l'ordre du dernier arrivé (LIFO : last in, first out). La présence ou l'absence d'un bloc CHECK détermine si nous allons ensuite à la phase 2 ou si nous sautons directement à la phase 4.

2. La phase de génération de code (optionnelle)

  • Les blocs CHECK sont installés par les générateurs de code ; cette phase optionnelle n'intervient donc que si vous avez utilisé explicitement l'un de ces générateurs de code (décrit plus tard dans Générateurs de codes). Ceux-ci convertissent le programme compilé (mais non encore lancé) soit en code source en C, soit en bytecode Perl sérialisé — une séquence de valeurs exprimant des instructions Perl internes. Si vous choisissez de générer du code source en C, il peut éventuellement produire un fichier appelé une image exécutable en langage machine natif.(145)
  • Arrivé à ce point, votre programme se met en hibernation. Si vous avez fait une image exécutable, vous pouvez directement aller à la phase 4 ; sinon, vous devez reconstruire les bytecodes lyophilisés en phase 3.

3. La phase de reconstruction de l'arbre syntaxique (optionnelle)

  • Pour réanimer le programme, son arbre syntaxique doit être reconstruit. Cette phase n'existe que si la génération de code a eu lieu et si vous avez choisi de générer du bytecode. Perl doit tout d'abord reconstituer ses arbres syntaxiques à partir de cette séquence de bytecode avant que le programme puisse être lancé. Perl ne se lance pas directement à partir des bytecodes ; ce serait trop lent.

4. La phase d'exécution

  • Finalement, ce que vous attendiez tous : faire tourner votre programme.(146) L'interpréteur prend l'arbre syntaxique (qu'il obtient directement depuis le compilateur ou indirectement depuis la génération de code et la reconstruction consécutive de l'arbre) et l'exécute. (Ou, si vous avez généré un fichier image exécutable, il peut être lancé en tant que programme indépendant puisqu'il contient un interpréteur Perl intégré.)
  • Au début de cette phase, avant que ce ne soit à votre programme principal de se lancer, tous les blocs INIT programmés sont exécutés dans l'ordre du premier arrivé (FIFO). Puis, votre programme principal est lancé. L'interpréteur peut faire à nouveau appel au compilateur comme cela est nécessaire lorsqu'il rencontre un eval CHAÎNE, un do FICHIER ou une instruction require, une construction s///ee ou une reconnaissance de motif avec une variable interpolée contenant une assertion de code valide.
  • Lorsque votre programme principal a fini, tous les blocs END différés sont finalement exécutés, cette fois-ci dans l'ordre du dernier arrivé (LIFO). Le bloc vu en tout premier sera exécuté en dernier et ensuite vous aurez fini. (Les blocs END ne sont court-circuités que si vous appelez un exec ou si votre processus est balayé par une erreur catastrophique non interceptée. Les exceptions ordinaires ne sont pas considérées comme étant catastrophiques.)

Maintenant nous allons discuter de ces phases plus en détail et dans un ordre différent.

18-2. Compilation du code

Perl est toujours dans l'un des deux modes d'opération : soit il est en train de compiler votre programme, soit il est en train de l'exécuter — jamais les deux en même temps.

Tout au long de ce livre, nous nous référons à certains événements comme se déroulant à la compilation (compile time) ou alors nous disons que « le compilateur Perl fait ceci ou cela ». À d'autres endroits, nous mentionnons qu'autre chose se déroule à l'exécution (run time) ou que « l'interpréteur Perl fait ceci ou cela ». Bien que vous pouvez continuer à vivre en pensant que « Perl » désigne à la fois le compilateur et l'interpréteur, comprendre lequel de ces deux rôles est en train de jouer Perl à un moment donné est essentiel pour comprendre pourquoi certaines choses se déroulent comme elles le font et non autrement. L'exécutable perl implémente les deux rôles : tout d'abord le compilateur, puis l'interpréteur. (D'autres rôles sont également possibles ; perl est aussi un optimisateur et un générateur de code. Parfois, c'est même un tricheur — mais tout cela dans la joie et la bonne humeur.)

Il est également important de comprendre la distinction entre la phase de compilation et la compilation (compilation time) ainsi qu'entre la phase d'exécution et l'exécution (run time). Une « phase » est un concept à grande échelle. Alors que la compilation et l'exécution sont des concepts à petite échelle. Une phase de compilation donnée fait surtout des travaux de compilation, mais elle fait également quelques travaux d'exécution via les blocs BEGIN. Une phase d'exécution donnée fait surtout des travaux d'exécution, mais elle fait également quelques travaux de compilation par l'intermédiaire d'opérateurs comme eval CHAÎNE.

Dans le déroulement typique des événements, le compilateur Perl lit le source de votre programme en entier avant que l'exécution ne commence. C'est ici que Perl analyse les déclarations, les instructions et les expressions pour s'assurer qu'elles sont syntaxiquement valides.(147) S'il trouve une erreur de syntaxe, le compilateur tente de la corriger pour qu'il puisse rendre compte des erreurs suivantes dans le code source. Parfois, ceci fonctionne et parfois non ; les erreurs de syntaxe ont une tendance fâcheuse à déclencher une cascade de fausses alertes. Perl abandonne, avec un sentiment de frustration, après environ 10 erreurs.

En plus de l'interpréteur qui traite les blocs BEGIN, le compilateur traite votre programme avec la connivence de trois agents notionnels. Ceux-ci sont parfois appelés « lexèmes », mais vous les rencontrerez plus souvent sous le nom de tokens dans les textes parlant des langages de programmation. L'analyseur lexical est parfois appelé un tokener ou un scanner et ce qu'il fait est parfois appelé de la lexicalisation (lexing) ou de la tokenisation (tokening). L'analyseur syntaxique (parser) essaie alors de dégager un sens des groupes de tokens en les assemblant dans des constructions plus larges, comme des expressions et des instructions, basées sur la grammaire du langage Perl. L'optimisateur réarrange et réduit ces regroupements plus larges en séquences plus efficaces. Il choisit ses optimisations précautionneusement, sans perdre de temps sur des optimisations marginales, car le compilateur Perl doit être rapide comme l'éclair puisqu'il est utilisé en mode load-and-go (c'est-à-dire que l'exécution d'un programme est toujours précédée de sa compilation).

Ceci ne se produit pas dans des étapes indépendantes, mais d'un seul coup avec énormément de dialogues croisés entre les agents. L'analyseur lexical a parfois besoin d'indications de la part de l'analyseur syntaxique pour lever les ambiguïtés sur les différents types possibles des tokens qu'il regarde. (Curieusement, la portée lexicale est l'une des choses que l'analyseur lexical ne comprend pas, car c'est l'autre sens de « lexique ».) L'optimisateur a également besoin de garder une trace de ce que l'analyseur syntaxique est en train de faire, car certaines optimisations ne peuvent être faites avant que l'analyseur syntaxique n'ait atteint un certain point, tel que la fin d'une expression, d'une instruction, d'un bloc ou d'un sous-programme.

Vous pouvez trouver étrange que le compilateur Perl fasse toutes ces choses en une fois plutôt que les unes après les autres, mais il s'agit certainement du même processus confus que lorsque vous essayez de comprendre un langage naturel à la volée alors que vous êtes en train de l'écouter ou de le lire. Vous n'attendez pas la fin d'un chapitre pour comprendre ce que la première phrase voulait dire. Vous pourriez penser aux correspondances suivantes :

Langage informatique

Langage naturel

Caractère

Lettre

Token

Morphème

Terme

Mot

Expression

Proposition

Instruction

Phrase

Bloc

Paragraphe

Fichier

Chapitre

Programme

Histoire

En supposant que l'analyse syntaxique se passe bien, le compilateur estime que ce que vous lui avez passé en entrée est une histoire valide — heu, pardon, un programme valide. Si vous utilisez l'option -c lorsque vous lancez votre programme, il affiche un message « syntax OK » et se termine. Sinon, le compilateur passe les fruits de ses efforts aux autres agents. Ces « fruits » se présentent sous forme d'un arbre syntaxique. Chaque fruit de l'arbre — ou chaque nœud, comme on les appelle — représente l'un des codes d'opération (opcodes) internes de Perl et les branches de l'arbre représentent le schéma d'évolution de cet arbre. Éventuellement, les nœuds seront enchaînés de manière linéaire, les uns après les autres, pour indiquer l'ordre dans lequel le système d'exécution devra les passer en revue.

Chaque code d'opération est la plus petite unité d'instruction exécutable que Perl peut appréhender. Vous pourriez rencontrer une expression telle que $a = -($b + $c) comme étant une seule instruction, mais Perl l'appréhende comme six codes d'opération séparés. Présenté sous un format simplifié, l'arbre syntaxique pour cette expression ressemblerait à la figure 18-2. Les numéros représentent l'ordre de parcours que le système d'exécution de Perl suivra éventuellement.

Image non disponible
Figure 18-2. Ordre de parcours des codes d'opération de $a = -($b +$c)

Perl n'est pas un compilateur travaillant en une seule passe comme certains pourraient le penser. (Les compilateurs travaillant en une seule passe sont très doués pour rendre les choses faciles à l'ordinateur et difficiles au programmeur.) Il s'agit réellement d'un compilateur multipasse, faisant des optimisations et consistant en au moins trois passes logiques différentes, qui dans la pratique s'entremêlent. Les passes 1 et 2 sont lancées en alternance alors que le compilateur fait des allers-retours à toute vitesse le long de l'arbre syntaxique durant sa construction ; la passe 3 se produit dès qu'un sous-programme ou qu'un fichier est entièrement analysé. Voici ces différentes passes :

Passe 1 : Analyse ascendante (Bottom-up parsing)

  • Durant cette passe, l'arbre syntaxique est construit par l'analyseur syntaxique yacc(1) en utilisant les tokens qu'on lui donne à partir de l'analyseur lexical sous-jacent (qui pourrait être considéré comme une autre phase logique à titre individuel). « Ascendante » signifie seulement que l'analyseur syntaxique connaît les feuilles de l'arbre avant ses branches et sa racine. Il calcule vraiment les choses de bas en haut dans la figure 18-2, puisque nous avons dessiné la racine en haut, dans la pure tradition idiosyncratique des informaticiens. (Et des linguistes.)
  • Alors que chaque nœud contenant un code d'opération est construit, une vérification de bon sens par code d'opération est effectuée pour contrôler si la sémantique est valide, comme le bon nombre et le type correct des arguments utilisés pour appeler les fonctions internes. Alors que chaque sous-section de l'arbre prend forme, l'optimisateur estime les transformations qu'il peut appliquer au sous-arbre entier se trouvant maintenant sous son emprise. Par exemple, une fois qu'il sait qu'une liste de valeurs est passée à une fonction prenant un certain nombre d'arguments, il peut se défaire du code d'opération qui enregistre le nombre d'arguments pour les fonctions en acceptant un nombre variable. Une optimisation plus importante, connue sous le nom de précalcul des expressions constantes (constant folding), est décrite plus loin dans ce paragraphe. Cette passe construit également l'ordre de parcours des nœuds utilisé plus tard pour l'exécution, ce qui est une astuce vraiment géniale, car le départ du parcours n'est presque jamais le nœud supérieur. Le compilateur réalise une boucle temporaire des codes d'opération, avec le nœud supérieur pointant sur le premier code d'opération à parcourir. Lorsque le nœud supérieur est incorporé dans quelque chose de plus gros, cette boucle de codes d'opération est brisée, uniquement pour engendrer une boucle plus grosse avec le nouveau nœud supérieur. La boucle est éventuellement brisée pour de bon lorsque le nœud de départ est poussé dans une autre structure comme un descripteur de sous-programme. L'appelant du sous-programme peut toujours retrouver ce premier code d'opération bien qu'il soit dans les profondeurs de l'arbre, comme c'est le cas dans la figure 18-2. L'interpréteur n'a aucun besoin de revenir en bas de l'arbre pour arriver à comprendre par où commencer.

Passe 2 : Optimisateur descendant (Top-down optimizer)

  • Une personne lisant un extrait d'un code Perl (ou d'un code français, pour ce qui nous préoccupe ici) ne peut déterminer le contexte sans examiner les éléments lexicaux l'entourant. Vous ne pouvez parfois pas décider ce qui se passe réellement jusqu'à ce que vous ayez plus d'informations. Que cela ne vous afflige pas pour autant, car vous n'êtes pas seul : le compilateur non plus. Dans cette passe, le compilateur redescend le sous-arbre qu'il vient de construire pour appliquer des optimisations locales, dont la plus remarquable est la propagation de contexte. Le compilateur marque les nœuds avec les contextes appropriés (vide, scalaire, liste, référence ou lvalue) imposés par le nœud courant. Les codes d'opération superflus sont annulés, mais non supprimés, car il est maintenant trop tard pour reconstruire l'ordre d'exécution. Nous compterons sur la troisième passe pour les enlever de l'ordre d'exécution provisoire déterminé par la première passe.

Passe 3 : Optimisateur à lucarne (Peephole optimizer)

  • Certaines unités de code ont leur propre espace de stockage dans lequel elles conservent leurs variables de portée lexicale. (De tels espaces sont appelés mémoires de travail ou scratchpads dans le jargon de Perl.) Ces unités comprennent les eval CHAÎNE, les sous-programmes et les fichiers entiers. D'une manière importante du point de vue de l'optimisateur, elles ont chacune leur propre point d'entrée, ce qui signifie qu'alors que nous connaissons l'ordre d'exécution à partir d'ici, nous ne pouvons pas connaître ce qui s'est passé auparavant, car la construction a pu être appelée de n'importe où. Ainsi, lorsque l'une de ces unités a fini d'être analysée, Perl lance un optimisateur regardant par une lucarne, sur ce code. Au contraire des deux passes précédentes, qui parcouraient la structure de branche de l'arbre syntaxique, cette passe-ci parcourt le code dans un ordre d'exécution linéaire, puisqu'elle constitue à la base la dernière opportunité de le faire avant que nous isolions la liste de codes d'opération de l'analyseur syntaxique. La plupart des optimisations ont déjà été effectuées dans les deux premières passes, mais certaines ne le pouvaient pas.
  • Des optimisations variées de dernière minute se produisent ici, y compris l'assemblage de l'ordre d'exécution final en court-circuitant les codes d'opération nuls et le fait de détecter lorsque diverses juxtapositions de codes d'opération peuvent être réduites à quelque chose de plus simple. L'identification d'une série de concaténations de chaînes est une optimisation importante puisque vous aimeriez vraiment éviter de copier la même chaîne encore et encore à chaque fois que vous ajoutez un petit quelque chose à la fin. Cette passe ne fait pas que de l'optimisation ; il réalise également une grande quantité de travail « réel » : l'interception de mots simples (barewords), la génération d'avertissements sur des constructions douteuses, la recherche de code peu susceptible d'être atteint, la résolution de clefs de pseudohachages et la recherche de sous-programmes appelés avant que leurs prototypes n'aient été compilés.

Passe 4 : Génération de code (Code generation)

  • Cette passe est optionnelle; elle n'est pas utilisée dans le déroulement normal des événements. Mais si l'un des trois générateurs de code — B::Bytecode, B::C ou B::CC — est invoqué, on accède une dernière fois à l'arbre syntaxique. Les générateurs de code émettent soit les bytecodes Perl sérialisés utilisés plus tard pour reconstruire l'arbre syntaxique, soit du code littéral C représentant l'état de l'arbre syntaxique à la compilation.
  • La génération de code C est disponible en deux variétés différentes. B::C reconstruit simplement l'arbre syntaxique et le lance en utilisant la boucle habituelle runops() que Perl lui-même utilise durant l'exécution. Tandis que B:CC produit un équivalent C linéarisé et optimisé du chemin emprunté par le code d'exécution (qui ressemble à une table de branchements ( jump table) géante) et l'exécute.

Pendant la compilation, Perl optimise votre code de manières très, très, diverses. Il réarrange le code pour le rendre plus efficace à l'exécution. Il supprime le code qui ne pourra jamais être atteint durant l'exécution, comme un bloc if (0) ou les blocs elsif et else avec un bloc if (1). Si vous utilisez des variables lexicalement typées, déclarées avec my NomClasse $var ou our NomClasse $var et que le paquetage NomClasse avait été initialisé avec le pragma use fields, les accès aux champs constants depuis le pseudohachage sous-jacent sont vérifiés pour y détecter des fautes de frappe et convertis en accès à un tableau. Si vous donnez à l'opérateur sort une routine de comparaison suffisamment simple, comme {$a <=> $b} ou {$b cmp $a}, celle-ci est remplacée par un appel à du code C compilé.

L'optimisation la plus impressionnante de Perl est probablement la manière dont il résout les expressions constantes dès que possible. Considérez par exemple l'arbre syntaxique présenté à la figure 18-2. Si les nœuds 1 et 2 avaient tous deux été des littéraux ou des fonctions constantes, les nœuds 1 à 4 auraient été remplacés par le résultat de ce calcul, ce qui aurait donné quelque chose comme la figure 18-3.

Image non disponible
Figure 18-3 : Précalcul des constantes

On appelle ceci le précalcul d'expressions constantes (constant folding). Ce précalcul ne se limite pas à des cas simples comme la conversion à la compilation de 2**10 en 1024. Il résout également les appels de fonctions — à la fois les fonctions internes et les sous-programmes déclarés par l'utilisateur remplissant les critères de la section Inclure par référence les fonctions constantes au chapitre 6, Sous-programmes. En réminiscence des compilateurs FORTRAN, célèbres pour leur connaissance de leurs propres fonctions intrinsèques, Perl sait également laquelle de ses fonctions internes appeler durant la compilation. C'est pourquoi lorsque vous essayez de prendre le log de 0.0 ou la racine carrée (sqrt) d'une constante négative, vous encourez une erreur de compilation et non d'exécution, et l'interpréteur n'est pas lancé du tout.(148)

Même les expressions arbitrairement complexes sont résolues très tôt, en déclenchant parfois la suppression de blocs entiers comme celui-ci :

 
Sélectionnez
if (2 * sin(1)/cos(1) < 3 && une_fonc() { n_importe_quoi() }

Aucun code n'est généré pour ce qui ne pourra jamais être évalué. Comme la première partie est toujours fausse, une_fonc et n_importe_quoi ne seront jamais appelés. (Ne vous attendez donc pas à faire un goto sur des étiquettes à l'intérieur de ce bloc, car il n'existera même pas à l'exécution.) Si une_fonc était une fonction constante susceptible d'être incluse par référence (inlinable), sa valeur aurait juste été insérée comme si c'était une constante littérale. Vous auriez alors encouru un avertissement « Useless use of a constant in void context » (Utilisation inutile d'une constante dans un contexte vide). Ceci pourrait vous surprendre si vous n'aviez pas réalisé que c'était une constante. Toutefois, si n_importe_quoi était la dernière instruction évaluée dans une fonction appelée dans un contexte non vide (tel que l'optimisateur le détermine), vous n'auriez jamais vu l'avertissement.

Vous pouvez voir le résultat final de l'arbre syntaxique construit après toutes les étapes d'optimisation avec perl -Dx. (L'option -D exige une version spéciale de Perl, permettant le débogage) Voir également le paragraphe sur B::Deparse décrit ci-dessous.

En fin de compte, le compilateur Perl travaille dur (mais pas trop dur) pour optimiser le code afin que, lorsqu'arrive le moment de l'exécution, celle-ci soit accélérée. C'est le moment où votre programme est lancé, alors occupons-nous en maintenant.

18-3. Exécution du code

À première vue, les programmes Sparc ne tournent que sur des machines Sparc, les programmes Intel ne tournent que sur des machines Intel et les programmes Perl ne tournent que sur des machines Perl. Une machine Perl possède des qualités qu'un programme Perl trouverait idéales dans un ordinateur : une mémoire qui s'alloue et se désalloue automatiquement, des types de données fondamentaux qui sont des chaînes, des tableaux et des hachages dynamiques et n'ont pas de limite de taille et des systèmes qui se comportent à peu près toujours de la même façon. Le travail de l'interpréteur Perl est de faire que n'importe quel ordinateur sur lequel il tourne, apparaisse comme l'une de ces machines Perl idéales.

Cette machine fictive présente l'illusion d'un ordinateur spécialement conçu pour ne rien faire tourner d'autre que des programmes Perl. Chaque code d'opération (opcode) produit par le compilateur est une commande fondamentale dans cet ensemble d'instructions émulées. Au lieu d'un compteur de programme matériel, l'interpréteur ne garde que la trace du code d'opération courant à exécuter. Au lieu d'un pointeur de pile matériel, l'interpréteur possède sa propre pile virtuelle. Cette pile est très importante, car la machine virtuelle Perl (que nous refusons d'appeler une PVM(149)) est une machine reposant sur des opérations de pile. Les codes d'opération de Perl sont appelés en interne des codes PP (abréviation de codes « push-pop », « empiler-dépiler »), car ils manipulent la pile virtuelle de l'interpréteur pour trouver tous les opérandes, traiter les valeurs temporaires et stocker tous les résultats.

Si vous avez jamais programmé en Forth ou en PostScript ou utilisé une calculatrice scientifique HP avec une saisie en RPN (« Reverse Polish Notation », notation polonaise inversée), vous savez comment fonctionne une machine basée sur des opérations de pile. Même si ce n'est pas le cas, le concept est simple : pour ajouter 3 et 4, vous faites les choses dans l'ordre 34+ au lieu de l'ordre conventionnel 3+4. Ce qui signifie en termes d'opérations de pile que vous empilez 3, puis 4 et + dépile ensuite les deux arguments, les additionne et remet 7 dans la pile, où il restera jusqu'à ce que vous en fassiez quelque chose.

Comparé au compilateur Perl, l'interpréteur Perl est un programme rigoureux, presque ennuyeux. Tout ce qu'il fait est de parcourir pas à pas les codes d'opération compilés, un à un, et de les acheminer vers l'environnement d'exécution de Perl, c'est-à-dire, vers la machine virtuelle Perl. C'est juste un tas de code C, n'est-ce pas ?

En fait, ce n'est pas du tout ennuyeux. Une machine virtuelle Perl garde la trace d'une grande quantité de contextes dynamiques à votre place pour que vous n'ayez pas à le faire. Perl entretient un bon nombre de piles, que vous n'avez pas à connaître, mais que nous énumérerons ici, seulement pour vous épater :

pile d'opérandes (operand stack)

  • Il s'agit de la pile dont nous avons déjà parlé.

pile de sauvegardes (save stack)

  • Là où sont sauvegardées les valeurs déclarées locales en attendant d'être restaurées. Plusieurs routines internes déclarent également des valeurs comme étant locales sans que vous le sachiez.

pile de portées (scope stack)

  • Le contexte dynamique poids plume, qui contrôle le moment où la pile de sauvegardes doit être dépilée.

pile de contextes (context stack)

  • Le contexte dynamique poids lourd ; qui a appelé qui pour en arriver là où vous êtes. La fonction caller parcourt cette pile. Les fonctions de contrôle de boucle examinent cette pile pour trouver quelle boucle contrôler. Lorsque vous épluchez la pile de contexte, la pile de portées est épluchée en conséquence, ce qui restaure toutes vos variables locales à partir de la pile de sauvegardes, même si vous aviez quitté le contexte précédent par le biais de procédés infâmes comme de lever une exception et sortir avec un longjmp(3).

pile de changements d'environnement ( jumpenv stack)

  • La pile des contextes de longjmp(3) qui nous permettent de lever des exceptions ou de quitter le programme de manière expéditive.

pile de retours (return stack)

  • Là d'où nous sommes partis lorsque nous sommes entrés dans ce sous-programme.

pile de repère (mark stack)

  • Là où commence, dans la pile d'opérandes, la liste d'arguments courante pour les fonctions avec un nombre variable d'arguments (variadic).

piles de mémoires de travail lexicales récursives (recursive lexical pad stacks)

  • Là où sont stockés les variables lexicales et les autres « registres de mémoires de travail » (scratch register) lorsque les sous-programmes sont appelés récursivement.

Bien entendu, il y a la pile C dans laquelle sont stockées toutes les variables C. Perl essaie en fait d'éviter de se reposer sur la pile du C pour le stockage de valeurs sauvegardées, puisque longjmp(3) court-circuite la restauration appropriée de telles valeurs.

Tout ceci pour dire que la vision habituelle que l'on a d'un interpréteur, un programme qui en interprète un autre, est vraiment inadéquate pour décrire ce qui se passe ici. Oui, il existe du code C implémentant des codes d'opération, mais lorsque nous parlons d'« interpréteur », nous voulons dire quelque chose de plus que cela, de la même manière que lorsque nous parlons de « musicien », nous voulons dire quelque chose de plus qu'un ensemble d'instructions d'ADN changeant les notes en sons. Les musiciens sont des organismes réels, vivants et ont une « condition ». Il en va de même pour les interpréteurs.

En particulier, tout ce contexte dynamique et lexical, ainsi que les tables de symboles globales, plus l'arbre syntaxique, plus un thread d'exécution, est ce que nous appelons un interpréteur. En tant que contexte d'exécution, un interpréteur commence son existence avant même que le compilateur ne démarre et il peut se lancer dans une forme rudimentaire pendant même que le compilateur est en train de construire le contexte de l'interpréteur. En fait, c'est précisément ce qui arrive lorsque le compilateur appelle l'interpréteur pour exécuter des blocs BEGIN et d'autres choses dans le même genre. Et l'interpréteur peut se retourner et utiliser le compilateur pour se construire davantage. Chaque fois que vous définissez un autre sous-programme ou que vous chargez un autre module, la machine virtuelle Perl particulière que nous appelons un interpréteur se redéfinit elle-même. Vous ne pouvez pas vraiment dire si c'est le compilateur ou l'interpréteur qui a le contrôle, car ils coopèrent pour contrôler le processus d'amorçage (bootstrap process) que nous appelons communément « lancer un script Perl ». C'est comme lorsqu'on amorce le cerveau d'un enfant. Est-ce l'ADN qui le fait ou les neurones ? Un peu des deux, selon nous, avec quelques interventions de programmeurs externes.

Il est possible de lancer de multiples interpréteurs dans le même processus ; ils peuvent ou non partager des arbres syntaxiques, selon qu'ils ont démarré en clonant un interpréteur existant ou en construisant un nouvel interpréteur à partir de zéro. Il est également possible de lancer de multiples threads dans un seul interpréteur, auquel cas ils ne partagent pas seulement des arbres syntaxiques, mais également les symboles globaux — voir le chapitre 17, Threads.

Mais la plupart des programmes Perl n'utilisent qu'un seul interpréteur Perl pour exécuter leur code compilé. Et alors que vous pouvez lancer de multiples interpréteurs Perl indépendants dans un seul processus, l'API actuelle pour ceci n'est accessible qu'en C.(150) Chaque interpréteur Perl individuel joue le rôle d'un processus complètement séparé, mais ne coûte pas autant que la création d'un nouveau processus complet. C'est ainsi que l'extension mod_perl d'Apache obtient de telles performances : lorsque vous lancez un script CGI sous mod_perl, ce script a déjà été compilé en codes d'opération Perl, évitant ainsi le besoin de le recompiler — mais, ce qui est plus important encore, en évitant le besoin de démarrer un nouveau processus, ce qui constitue le véritable goulot d'étranglement. Apache initialise un nouvel interpréteur Perl dans un processus existant et passe à cet interpréteur le code précédemment compilé pour qu'il l'exécute. Bien entendu, c'est un peu plus compliqué que cela — c'est toujours un peu plus compliqué. Pour plus d'informations sur mod_perl, voir Writing Apache Modules with Perl and C (O'Reilly, 1999).

Plusieurs autres applications, comme nvi, vim et innd, peuvent intégrer des interpréteurs Perl ; nous ne pouvons pas espérer les lister toutes ici. Il existe un bon nombre de produits commerciaux qui ne font même pas la publicité pour le moteur Perl qu'ils ont intégré. Ils l'utilisent seulement en interne, car il accomplit leur travail avec classe.

18-4. Sorties du compilateur

Ainsi, si Apache peut compiler un programme Perl maintenant et l'exécuter plus tard, pourquoi pas vous ? Apache et d'autres programmes contenant des interpréteurs Perl intégrés le font facilement — ils ne stockent jamais l'arbre syntaxique dans un fichier externe. Si cette approche vous convient et que vous cela ne vous embête pas d'utiliser l'API C pour en bénéficier, vous pouvez faire la même chose. Voir le paragraphe « Intégrer du Perl » au chapitre 21, Mécanismes internes et accès externes pour apprendre comment accéder à Perl depuis une infrastructure C l'entourant.

Si vous ne voulez pas prendre cette route ou si vous avez d'autres besoins, vous disposez alors de plusieurs options. Au lieu d'alimenter immédiatement l'interpréteur Perl avec les codes d'opération issus du compilateur Perl, vous pouvez invoquer l'une des nombreuses autres sorties (backends). Ces sorties peuvent sérialiser et stocker les codes d'opération compilés dans un fichier externe ou même les convertir en deux variétés différentes de code C.

Merci de bien prendre conscience que les générateurs de code sont tous des utilitaires extrêmement expérimentaux qui ne devraient pas espérer fonctionner dans un environnement de production. En fait, ils ne devraient même pas espérer fonctionner dans un environnement de non-production, sauf peut-être une fois tous les 36 du mois. Maintenant que nous avons assez rabaissé vos illusions pour que toute réussite les surpasse nécessairement, nous pouvons vous parler en toute sécurité du fonctionnement des sorties du compilateur.

Certains des modules de sortie (backend) sont des générateurs de code, comme B::Bytecode, B::C et B::CC. Les autres sont réellement des analyseurs de code et des outils de débogage, comme B::Deparse, B::Lint et B::Xref. Au-delà de ces sorties, la distribution standard inclut plusieurs autres modules de bas niveau d'un intérêt potentiel pour ceux désirant devenir auteurs d'outils Perl de développement de code. D'autres modules de sortie peuvent être trouvés sur CPAN, comprenant (au moment ou ces lignes sont écrites) B::Fathom, B::Graph, B::JVM::Jasmin et B::Size.

Lorsque vous utilisez le compilateur Perl pour autre chose que l'alimentation de l'interpréteur, le module O (c'est-à-dire en utilisant le fichier O.pm) se tient entre le compilateur et les modules de sorties variés. Vous n'appelez pas les sorties directement : vous appelez plutôt la partie intermédiaire, qui à son tour appelle la sortie désignée. Ainsi, si vous aviez un module appelé B::Backend, vous l'invoqueriez dans un script donné de cette manière :

 
Sélectionnez
% perl -MO=Backend NOM_SCRIPT

Certaines sorties prennent des options, spécifiées comme ceci :

 
Sélectionnez
% perl -MO=Backend OPTS NOM_SCRIPT

Certaines sorties ont déjà leur propre frontal pour invoquer leurs parties intermédiaires pour vous afin que vous n'ayez pas à vous souvenir de leur M.O. En particulier, perlcc(1) invoque ce générateur de code, qui peut être encombrant à lancer.

18-5. Générateurs de code

Les trois sorties actuelles qui convertissent des codes d'opération Perl en un autre format sont toutes emphatiquement expérimentales. (Oui, nous l'avons déjà dit, mais nous ne voulons pas que vous l'oubliiez.) Même lorsqu'il arrive qu'ils produisent en sortie quelque chose qui se lance correctement, les programmes résultants peuvent prendre plus d'espace disque, plus de mémoire et plus de temps CPU qu'ils ne le feraient d'ordinaire. Ceci est un sujet encore en recherche et développement. Les choses vont s'améliorer.

18-5-a. Le générateur de bytecode

Le module B::Bytecode écrit les codes d'opération de l'arbre syntaxique dans un encodage indépendant de la plate-forme. Vous pouvez prendre un script Perl compilé en bytecode et copier cela sur une autre machine où Perl est installé.

La commande standard, bien qu'actuellement expérimentale, perlcc(1) sait comment convertir du code source Perl en un programme Perl compilé en bytecode. Tout ce que vous devez faire est :

 
Sélectionnez
% perlcc -B script_src > script_pby

Et vous devriez maintenant être capable d'« exécuter » directement le script_pby résultant. Le début de ce fichier ressemble à quelque chose comme ceci :

 
Sélectionnez
#!/usr/bin/perl
use ByteLoader 0.04;
PLBCi386-linux^@0.04^@^D^@^@^@^D^@^@^@0x1234^@^G
^C^@^@^@^@^F^A^C^A^@^@^@^G^@^C^B^@^@^@^G^@^C^C^@^@^@^G^@^C^D^
^@^@^G^@^C^E^@^@^@^G^@^C^F^@^@^@^G^@^C^G^@^@^@^G^@^C^H^@^@^@^
...

Vous devriez trouver un petit en-tête de script suivi par des données purement binaires. Cela peut ressembler à de la pure magie, mais son mystra, heu. . . mystère n'est pas très puissant. Le module ByteLoader utilise une technique appelée filtre de source pour altérer le code source avant que Perl n'ait une chance de le voir. Un filtre de source est un genre de préprocesseur s'appliquant à tout ce qui se trouve en dessous de lui dans le fichier courant. Au lieu d'être limité à des transformations simplistes comme les préprocesseurs de macros comme le sont cpp(1) et m4(1), il n'y a ici aucune limitation. Les filtres de source ont été utilisés pour enrichir la syntaxe de Perl, pour compresser ou crypter du code source et même pour écrire des programmes Perl en latin. E perlibus unicode; cogito, ergo substr; carp dbm, et al. Heu, avertissement du scriptor, désolé, du scribe latin.

Le module ByteLoader est un filtre de source qui sait comment désassembler les codes d'opération sérialisés pour reconstruire l'arbre syntaxique original. Le code Perl reconstitué est intégré dans l'arbre syntaxique actuel sans utiliser le compilateur. Lorsque l'interpréteur rencontre ces codes d'opération, il n'a qu'à les exécuter comme s'ils étaient ici depuis toujours, n'attendant que ça.

18-5-b. Les générateurs de code C

Les autres générateurs de codes, B::C et B::CC, produisent tous deux du code C au lieu de codes d'opération Perl sérialisés. Le code qu'ils génèrent est loin d'être lisible et si vous essayez de le lire, vous risquez de devenir aveugle. Ce n'est pas quelque chose que vous pouvez utiliser pour insérer quelques morceaux de code Perl traduit en C dans un programme C plus vaste. Pour faire cela, voir le chapitre 21.

Le module B::C ne fait qu'écrire les structures de données C nécessaires pour recréer l'environnement d'exécution Perl complet. Vous obtenez un interpréteur Perl dédié avec toutes les structures de données internes préinitialisées. En un certain sens, le code généré ressemble à celui que produit B::Bytecode. Les deux sont une traduction littérale des arbres de codes d'opération que construit le compilateur, mais là où B::Bytecode les laisse sous forme symbolique pour être recréés plus tard et branchés dans un interpréteur Perl déjà lancé, B::C laisse ces codes d'opération en C. Lorsque vous compilez ce code C avec votre compilateur C et que vous faites l'édition de liens avec la bibliothèque Perl, le programme résultant n'aura pas besoin d'un interpréteur Perl installé sur le système cible. (Il pourra cependant avoir besoin de bibliothèques partagées, si vous n'avez pas fait une édition de liens statique pour tous les objets.) Cependant, ce programme n'est pas vraiment différent de l'interpréteur Perl normal qui lance votre script. Il est seulement précompilé dans une image exécutable de manière isolée.

Le module B::CC essaie toutefois d'en faire plus. Le début du fichier source C qu'il génère ressemble un peu à ce que produit B::C(151) mais, en définitive, toute ressemblance s'arrête là. Dans le code de B::C, vous avez une énorme table de codes d'opération en C qui est manipulée exactement de la même manière que l'interpréteur l'aurait fait tout seul, alors que dans le code C généré par B::CC est présenté dans l'ordre correspondant au flux d'exécution de votre programme. Il possède même une fonction C correspondant à chaque fonction de votre programme. Une certaine quantité d'optimisations basées sur le type des variables est accomplie ; quelques benchmarks peuvent tourner deux fois plus vite que dans l'interpréteur standard. Ceci est le plus ambitieux des générateurs de code actuels, celui qui représente le plus grand espoir pour l'avenir. Et ce n'est pas une coïncidence si c'est également le moins stable des trois.

Les étudiants en informatique recherchant des projets de thèse n'ont pas besoin de chercher ailleurs. Il y a ici une montagne de diamants attendant d'être affinés.

18-6. Outils de développement de code

Le module O possède de nombreux modi operandi intéressants autres que l'alimentation des générateurs de codes désespérément expérimentaux. En fournissant un accès relativement indolore à la sortie du compilateur Perl, ce module facilite l'écriture d'autres outils ayant besoin de tout savoir à propos d'un programme Perl.

Le module B::Lint est ainsi baptisé d'après lint(1), le vérificateur de programmes C. Il inspecte les programmes à la recherche de constructions douteuses qui égarent souvent les débutants, mais qui, normalement, ne déclenchent pas d'avertissements. Appelez le module directement :

 
Sélectionnez
% perl -MO=Lint,all mon_prog

Seules quelques vérifications sont actuellement définies, comme l'emploi d'un tableau dans un contexte scalaire implicite, le fait de se fier à des variables par défaut et l'accès à des identificateurs d'un autre paquetage, commençant par _ (normalement privés). Voir B::Lint(3) pour plus de détails.

Le module B::Xref génère des listes avec les références croisées des déclarations et des utilisations de toutes les variables (à la fois globales et de portée lexicale), de tous les sous-programmes et des tous les formats dans un programme, répartis par fichier et par sous-programme. Appelez le module de cette manière :

 
Sélectionnez
% perl -MO=Xref mon_prog > mon_prog.pxref

Voici, par exemple, un extrait de compte-rendu :

 
Sélectionnez
Subroutine analyse_argv
Package (lexical)
    $allume           i113, 114
    $opt              i113, 114
    %config_getopt    i107, 113
    @args_config      i112, 114, 116, 116
Package Getopt::Long
    $ignorecase       101
    &GetOptions       &124
Package main
    $Options          123, 124, 141, 150, 165, 169
    %$Options         141, 150, 165, 169
    &verifie_lecture  &167
    @ARGV             121, 157, 157, 162, 166, 166

Ceci montre que la routine analyse_argv avait quatre variables lexicales propres ; elle accédait également à des identificateurs globaux des paquetages main et Getopt::Long.

Les nombres représentent les lignes où l'élément en question a été utilisé : un i au début, indique que cet élément est utilisé pour la première fois à ce numéro de ligne et un & au début signifie qu'un sous-programme a été appelé ici. Les déréférences sont listées séparément, c'est pourquoi figurent à la fois $Options et %$Options.

B::Deparse est un module offrant un affichage agréable (pretty printer) pouvant démystifier du code Perl et vous aider à comprendre quelles transformations l'optimisateur a effectué sur votre code. Par exemple, ceci montre les valeurs utilisées par défaut par Perl pour diverses constructions :

 
Sélectionnez
% perl -MO=Deparse -ne 'for (1 .. 10) { print if -t }'
LINE: while (defined($_ = <ARGV>)) {
    foreach $_ (1 .. 10) {
        print $_ if -t STDIN;
    }
}

L'option -p ajoute des parenthèses pour que vous puissiez voir l'idée que Perl se fait de la préséance :

 
Sélectionnez
% perl -MO=Deparse,-p -e 'print $a ** 3 + sqrt(2) / 10 ** -2 ** $c'
print((($a ** 3) + (1.4142135623731 / (10 ** (-(2 ** $c))))));

Vous pouvez utiliser -q pour voir en quelles opérations élémentaires les chaînes interpolées sont compilées :

 
Sélectionnez
% perl -MO=Deparse,-q -e '"Un $nom et quelques @ARGV\n"'
'Un ' . $nom . ' et quelques ' . join($", @ARGV) . "\n";

Et ceci montre comment Perl compile vraiment une boucle for avec trois arguments en une boucle while :

 
Sélectionnez
% perl -MO=Deparse,-x3 -e 'for ($i=0;$i<10;$i++) { $x++ }'
$i= 0;
while ($i < 10) {
    ++$x;
}
continue {
    ++$i
}

Vous pouvez même appeler B::Deparse sur un fichier de bytecode Perl produit par perlcc -B et B::Deparse le décompile alors pour vous. Cependant, lire des codes d'opération Perl sérialisés est peut-être un jeu d'enfant, mais ce n'est pas le cas lorsqu'il s'agit d'un cryptage élevé.

18-7. Compilateur d'avant-garde, interpréteur rétro

Il y a un temps pour penser à tout ; parfois ce temps est derrière nous et parfois il est devant. Parfois il se situe entre les deux. Perl ne prétend pas savoir quand penser, il offre donc au programmeur un certain nombre d'options pour qu'il lui dise quand il faut penser. D'autres fois, il sait qu'une certaine sorte de pensée est nécessaire, mais il n'a aucune idée de ce qu'il doit penser, il a donc besoin d'un moyen de le demander à votre programme. Votre programme répond à ce genre de questions en définissant des sous-programmes avec des noms appropriés à ce que Perl essaie de trouver.

Non seulement le compilateur peut appeler l'interpréteur lorsqu'il veut avancer dans sa pensée, mais l'interpréteur peut également appeler le compilateur en retour lorsqu'il veut réviser l'histoire. Votre programme peut utiliser plusieurs opérateurs pour rappeler le compilateur. Comme le compilateur, l'interpréteur peut également appeler des sous-programmes nommés lorsqu'il veut se renseigner sur certaines choses. À cause de tous ces échanges entre le compilateur, l'interpréteur et votre programme, vous devez être conscient des choses qui se passent et quand elles se passent. Nous parlerons tout d'abord du moment où les sous-programmes nommés sont déclenchés.

Au chapitre 10, Paquetages, nous avons vu comment un sous-programme AUTOLOAD d'un paquetage était déclenché lorsqu'une fonction indéfinie dans ce paquetage était appelée. Au chapitre 12, Objets, nous avons fait connaissance avec la méthode DESTROY qui est invoquée lorsque la mémoire occupée par un objet est sur le point d'être récupérée par Perl. Enfin au chapitre 14, Variables liées, nous avons abordé plusieurs fonctions appelées implicitement lorsqu'on accédait à une variable liée.

Ces sous-programmes suivent tous la convention selon laquelle, si un sous-programme est déclenché automatiquement par le compilateur ou l'interpréteur, nous écrivons son nom en majuscules. Associés avec les différentes étapes de la vie de votre programme, il existe quatre autres sous-programmes de ce type, appelés BEGIN, CHECK, INIT et END. Le mot-clef sub est optionnel devant leurs déclarations. Peut-être devraient-ils être plutôt appelés « blocs », car d'une certaine manière ils ressemblent plus à des blocs nommés qu'à de véritables sous-programmes.

Par exemple, au contraire des sous-programmes ordinaires, il n'y a aucun mal à déclarer ces blocs plusieurs fois, puisque Perl garde une trace du moment où les appeler, vous n'avez donc pas à les appeler par leur nom. (Ils se différencient également des sous-programmes ordinaires en ce que shift et pop se comportent comme si vous étiez dans le programme principal et manipulent donc @ARGV par défaut et non @_.)

Ces quatre types de blocs s'exécutent dans cet ordre :

BEGIN

  • S'exécute ASAP (as soon as parsed(152)) à chaque fois qu'on les rencontre pendant la compilation et avant de compiler le reste du fichier.

CHECK

  • S'exécute lorsque la compilation est achevée, mais avant que le programme ne démarre. (CHECK peut vouloir dire « checkpoint » (point de contrôle) ou « double-check » (double vérification) ou même simplement « stop ».)

INIT

  • S'exécute au début de l'exécution, juste avant que le flux principal de votre programme ne commence.

END

  • S'exécute à la fin de l'exécution, juste après que le programme a fini.

Si vous déclarez plus d'un de ces blocs avec le même nom, même dans des modules séparés, les BEGIN s'exécutent tous avant les CHECK, qui eux-mêmes s'exécutent tous avant les INIT, qui à leur tour s'exécutent tous avant les END — qui finalement sont les derniers à s'exécuter avant de mourir, après que votre programme a fini. De multiples BEGIN et INIT s'exécutent dans l'ordre dans lequel ils ont été déclarés (FIFO, first in, first out : premier arrivé, premier servi) et les CHECK et END s'exécutent dans l'ordre inverse dans lequel ils ont été déclarés (LIFO, last in, first out : dernier arrivé, premier servi).

Ceci est probablement plus clair avec une démonstration :

 
Sélectionnez
#!/usr/bin/perl -l
print   "démarre l'exécution principale ici\n";
die     "l'exécution principale meurt ici\n";
die     "XXX: jamais atteint\n";
END     { print "1er END: fini d'exécuter" }
CHECK   { print "1er CHECK: fini de compiler" }
INIT    { print "1er INIT: commence à exécuter" }
END     { print "2ème END: fini d'exécuter" }
BEGIN   { print "1er BEGIN: toujours en train de compiler" }
INIT    { print "2ème INIT: commence à exécuter" }
BEGIN   { print "2ème BEGIN: toujours en train de compiler" }
CHECK   { print "2ème CHECK: fini de compiler" }
END     { print "3ème END: fini d'exécuter" }

Lorsqu'il s'exécute, ce programme de démonstration affiche ceci :

 
Sélectionnez
1er BEGIN: toujours en train de compiler
2ème BEGIN: toujours en train de compiler
2ème CHECK: fini de compiler
1er CHECK: fini de compiler
1er INIT: commence à exécuter
2ème INIT: commence à exécuter
démarre l'exécution principale ici
l'exécution principale meurt ici
3ème END: fini d'exécuter
2ème END: fini d'exécuter
1er END: fini d'exécuter

Puisqu'un bloc BEGIN s'exécute immédiatement, il peut entrer dans les déclarations, les définitions et les importations de sous-programmes avant même que le reste du fichier ne soit compilé. Ceci peut altérer la manière dont le compilateur analyse le reste du fichier courant, tout particulièrement si vous importez des définitions de sous-programmes. Du moins, déclarer un sous-programme lui permet d'être utilisé comme un opérateur de liste, rendant les parenthèses optionnelles. Si le sous-programme importé est déclaré avec un prototype, les appels à ce sous-programme peuvent être analysés comme les fonctions internes et peuvent même supplanter les fonctions internes du même nom afin de leur donner une sémantique différente. La déclaration use n'est qu'un bloc BEGIN qui s'y croit.

Les blocs END, par contraste, s'exécutent aussi tard que possible : lorsque votre programme quitte l'interpréteur Perl, même si c'est le résultat d'un die non intercepté ou d'une autre exception fatale. Il existe deux situations dans lesquelles un bloc END (ou une méthode DESTROY) est court-circuité. Il ne s'exécute pas si, au lieu de se terminer, le processus courant se métamorphose d'un programme à un autre via exec. Un processus balayé de la surface de la Terre par un signal non intercepté court-circuite également ses routines END. (Voir le pragma use sigtrap décrit au chapitre 31, Modules de pragmas, pour un moyen facile de convertir les signaux interceptables en exceptions. Pour des informations générales sur les signaux, voir la section Signaux au chapitre 16, Communication interprocessus.) Pour éviter le traitement de tous les END, vous pouvez appeler POSIX::_exit ou faire un kill -9, $$ ou simplement exec de n'importe quel programme inoffensif, comme /bin/true sur les systèmes Unix.

À l'intérieur d'un bloc END, $? contient le statut avec lequel le programme va sortir en faisant exit. Vous pouvez modifier $? depuis l'intérieur du bloc END pour changer la valeur de sortie du programme. Faites attention au changement accidentel de $? en lançant un autre programme avec system ou des apostrophes inverses.

Si vous avez plusieurs blocs END à l'intérieur d'un fichier, ils s'exécutent dans l'ordre inverse de leur définition. C'est-à-dire que le dernier bloc END défini est le premier à s'exécuter lorsque votre programme a fini. Cette inversion permet aux blocs BEGIN et END correspondants d'être imbriqués comme vous le souhaiteriez, si vous les aviez associés par paires. Par exemple, si le programme principal et un module qu'il charge ont leur propre paire de sous-programmes BEGIN et END, comme ceci :

 
Sélectionnez
BEGIN { print "le programme principal a commencé" }
END { print "le programme principal a fini" }
use Module;

et dans ce module, vous avez ces déclarations :

 
Sélectionnez
BEGIN { print "le module a commencé" }
END { print "le module a fini" }

alors le programme principal sait que son BEGIN arrivera toujours en premier et son END toujours en dernier. (Oui, BEGIN est vraiment un bloc à la compilation, mais des arguments identiques s'appliquent à des paires de blocs INIT et END à l'exécution.) Ce principe est vrai récursivement pour tout fichier qui en inclut un autre lorsque les deux ont des déclarations comme celles-ci. Cette propriété d'imbrication implique que ces blocs fonctionnent bien en tant que constructeurs et destructeurs. Chaque module peut avoir ses propres fonctions d'initialisation et de démolition que Perl appellera automatiquement. De cette manière, le programmeur n'a pas à se se rappeler si une bibliothèque particulière est utilisée, ni quelle initialisation spéciale ou quel code de nettoyage doit être invoqué, ni quand l'invoquer. Les déclarations du module s'occupent de ces événements.

Si vous pensez à un eval CHAÎNE comme à un rappel (c'est-à-dire, un appel en arrière) du compilateur par l'interpréteur, alors vous pourriez penser à un BEGIN comme à un appel en avant de l'interpréteur par le compilateur. Les deux suspendent temporairement l'activité et basculent le mode d'opération. Lorsque nous disons qu'un bloc BEGIN s'exécute aussi tôt que possible, nous voulons dire qu'il s'exécute dès qu'il est complètement défini, avant même que le reste du fichier qui le contient ne soit analysé. Les blocs BEGIN sont par conséquent exécutés à la compilation, jamais à l'exécution. Une fois qu'un bloc BEGIN s'est exécuté, il devient immédiatement indéfini et le code qu'il a utilisé est renvoyé à l'espace mémoire de Perl. Vous ne pourriez pas appeler un bloc BEGIN comme un sous-programme même si vous essayiez, car à l'heure où vous le feriez, il serait déjà parti.

De même que les blocs BEGIN, les blocs INIT s'exécutent juste avant que l'exécution de Perl ne commence, dans un ordre FIFO ( first in, first out : premier arrivé, premier servi). Par exemple, les générateurs de codes documentés dans perlcc utilisent des blocs INIT pour initialiser et résoudre les pointeurs vers les XSUB. Les blocs INIT sont vraiment comme les blocs BEGIN, sauf qu'ils permettent au programmeur de distinguer les constructions qui se déroulent à la compilation de celles qui se produisent à l'exécution. Lorsque vous lancez un script directement, ceci n'est pas extrêmement important, car le compilateur est de toute façon invoqué à chaque fois ; mais lorsque la compilation est séparée de l'exécution, la distinction peut devenir cruciale. Le compilateur peut n'être invoqué qu'une seule fois et l'exécutable résultant peut l'être plusieurs fois.

De même que les blocs END, les blocs CHECK s'exécutent dès que la phase de compilation de Perl se finit, mais avant que la phase d'exécution ne commence, dans un ordre LIFO (last in, first out : dernier arrivé, premier servi). Les blocs CHECK sont utiles pour « libérer » le compilateur tout comme les blocs END sont utiles pour libérer votre programme. En particulier, les sorties (backends) utilisent toutes des blocs CHECK comme le point à partir duquel invoquer leurs générateurs de code respectifs. Tout ce qu'elles ont besoin de faire est de mettre un bloc CHECK dans leur propre module et il s'exécutera au moment approprié, vous n'avez donc pas à installer de CHECK dans votre programme. Pour cette raison, vous n'écrirez que très rarement un bloc CHECK vous-même, à moins que vous n'écriviez de tels modules.

Récapitulant tout ceci, le tableau 18-1 énumère les diverses constructions en détaillant le moment où elles compilent le code représenté par « . . . » et celui où elles l'exécutent.

Tableau 18-1.

Bloc ou expression

Compile pendant la phase de

Capture les erreurs de compilation

Exécute pendant la phase de

Capture les erreurs d'exécution

Politique de déclenchement d'appel

use ...

C

Non

C

Non

Maintenant

no ...

C

Non

C

Non

Maintenant

BEGIN {...}

C

Non

C

Non

Maintenant

CHECK {...}

C

Non

C

Non

Tard

INIT {...}

C

Non

E

Non

Tôt

END {...}

C

Non

E

Non

Tard

eval {...}

C

Non

E

Oui

Inclus (inline)

eval "..."

E

Oui

E

Oui

Inclus (inline)

truc(...)

C

Non

E

Non

Inclus (inline)

sub truc {...}

C

Non

E

Non

Appel n'importe quand

eval "sub {...}"

E

Oui

E

Non

Appel plus tard

s/motif/.../e

C

Non

E

Non

Inclus (inline)

s/motif/"..."/ee

E

Oui

E

Oui

Inclus (inline)

Maintenant que vous connaissez la partition, nous espérons que vous serez capable de composer et de jouer vos morceaux de Perl avec plus d'assurance.

19. L'interface de la ligne de commande

Ce chapitre montre comment faire pointer Perl dans la bonne direction avant d'ouvrir le feu avec. Il existe diverses façons de faire viser Perl, mais les deux moyens primaires sont par le biais d'options (switches) dans la ligne de commande et par le biais de variables d'environnement. Les options sont le moyen le plus immédiat et le plus précis d'ajuster une commande particulière. Les variables d'environnement sont plus souvent employées pour mettre en place la politique globale.

19-1. Traitement des commandes

Il est heureux que Perl se soit développé dans le monde Unix, car cela signifie que sa syntaxe d'appel fonctionne assez bien sous les interpréteurs de commandes des autres systèmes d'exploitation. La plupart des interpréteurs de commandes savent comment traiter une liste de mots en arguments, et ne voient pas d'inconvénient à ce qu'un argument commence par un signe moins. Bien entendu, il existe cependant quelques cas où vous vous faites piéger lorsque vous passez d'un système à l'autre. Vous ne pouvez pas utiliser les apostrophes sous MS-DOS comme vous le faites sous Unix, par exemple. Et sur les systèmes comme VMS, il est nécessaire d'employer un emballage logiciel (wrapper) sautant à travers divers cerceaux pour émuler l'indirection des entrées/sorties d'Unix. Nous posons notre joker quant à l'interprétation des jokers (wildcards). Mais une fois ces problèmes résolus, Perl traite ses options et ses arguments à peu près de la même manière sur n'importe quel système d'exploitation.

Même lorsque vous ne disposez pas d'un interpréteur de commandes per se, il est assez facile d'exécuter un programme Perl depuis un autre programme, écrit dans n'importe quel langage. Le programme appelant peut non seulement passer des arguments de la manière traditionnelle, mais il peut également passer des informations via des variables d'environnement et, si votre système d'exploitation l'implémente, des descripteurs de fichiers hérités (voir Passage de handles de fichiers au chapitre 16, Communication interprocessus). Même des mécanismes de passage d'argument plus exotiques peuvent facilement être encapsulés dans un module, amenés ensuite dans votre programme Perl via une simple directive use.

Perl analyse les options de la ligne de commande de la manière standard.(153) C'est-à-dire qu'il s'attend à ce que les options (les mots commençant par un moins) viennent en premier sur la ligne de commande. Le nom du script vient généralement ensuite, suivi par les arguments additionnels à passer au script. Certains de ces arguments supplémentaires peuvent eux-mêmes ressembler à des options, auquel cas ils doivent être traités par le script, car Perl abandonne l'analyse des options dès qu'il voit une non-option, ou l'option spéciale « - » qui vaut dire « Je suis la dernière option ».

Perl vous procure une certaine souplesse en ce qui concerne ce que vous fournissez au programme. Pour les petites tâches rapides et brouillonnes, vous pouvez programmer Perl entièrement depuis la ligne de commande. Pour les travaux plus importants, plus permanents, vous pouvez fournir un script Perl dans un fichier séparé. Perl recherche à compiler et à lancer un script que vous pouvez spécifier avec l'une de ces trois manières :

  1. Spécifié ligne par ligne via des options -e sur la ligne de commande. Par exemple :

     
    Sélectionnez
    % perl -e "print 'Salut, tout le monde.'"
    Salut, tout le monde.
  2. Contenu dans le fichier spécifié par le premier nom de fichier sur la ligne de commande. Les systèmes supportant la notation #! sur la première ligne d'un script exécutable invoquent les interpréteurs de cette façon à votre insu.

  3. Passé implicitement via l'entrée standard. Cette méthode ne fonctionne que s'il n'y a pas de noms de fichier en argument ; pour passer des arguments à un script en entrée standard, vous devez utiliser la méthode 2, spécifier explicitement un « -» pour le nom du script. Par exemple :
 
Sélectionnez
% echo "print qq(Salut, @ARGV.)" | perl -tout le monde
Salut, tout le monde.

Avec les méthodes 2 et 3, Perl commence par analyser le fichier d'entrée depuis le début — à moins d'avoir spécifié l'option -x, ce qui lui fait rechercher la première ligne commençant par #! contenant le mot « perl », d'où il démarre. Cela sert à lancer un script inclus dans un message plus grand. En ce cas, vous pourriez indiquer la fin du script par le token __END__.

Que vous utilisiez ou non -x, la ligne #! est toujours examinée pour y trouver des options au moment de l'analyse syntaxique de la ligne. Ainsi, si vous vous trouvez sur une plate-forme qui n'autorise qu'un argument sur la ligne #! ou pis, qui ne reconnaît même pas la ligne #! comme étant spéciale, vous pouvez toujours conserver un comportement cohérent des options, quelle que soit la manière dont Perl est invoqué, même si -x a été utilisé pour trouver le début du script.

Attention : comme les versions plus anciennes d'Unix interrompent sans rien dire l'interprétation par le noyau de la ligne #! après 32 caractères, certaines options peuvent traiter votre programme correctement ou non ; vous pourriez même obtenir un « - » sans sa lettre, si vous n'êtes pas prudent. Assurez-vous que tous vos scripts tombent avant ou après cette limite de 32 caractères. La plupart des options ne se soucient pas d'être traitées plusieurs fois, mais un « - » au lieu d'une option complète pousserait Perl à essayer de lire son code source depuis l'entrée standard plutôt que depuis votre script. Et une option -I tronquée pourrait également provoquer des résultats étranges. Toutefois, certaines options tiennent compte du fait qu'elles soient traitées deux fois, comme les combinaisons de -l et de -0. Mettez toutes les options après la limite de 32 caractères (le cas échéant) ou remplacez l'emploi de -0CHIFFRES par BEGIN{$/ = "\0CHIFFRES";}. Évidemment, si vous n'êtes pas sur un système UNIX, vous êtes sûr de ne pas avoir ce genre de problème.

L'analyse des options sur la ligne #! démarre après que « perl » est mentionné pour la première fois. Les séquences « -* » et « - » sont spécifiquement ignorées au bénéfice des utilisateurs d'emacs et, si vous vous en sentez l'envie, vous pouvez écrire :

 
Sélectionnez
#!/bin/sh --# -*-perl -*- -p
eval 'exec perl $0 -S ${1+"$@"}'
    if 0;

et Perl ne verra que l'option -p. La jolie séquence « -*-perl -*- » indique à emacs de démarrer en mode Perl ; elle n'est pas nécessaire si vous n'utilisez pas emacs. Le sale -S est expliqué plus loin dans la description de cette option.

Une astuce identique met en jeu le programme env(1), si vous en disposez :

 
Sélectionnez
#!/usr/bin/env perl

Les exemples précédents utilisent un chemin relatif jusqu'à l'interpréteur Perl, obtenant ainsi la version trouvée en premier dans le chemin de l'utilisateur. Si vous voulez une version particulière de Perl, écrivez, perl5.6.1 et placez ceci directement dans le chemin de la ligne #!, soit avec le programme env, soit avec le sale -S, soit avec un traitement ordinaire de #!.

Si la ligne #! ne contient pas le mot « perl », le programme cité après le #! est exécuté à la place de l'interpréteur Perl. Par exemple, supposons que vous ayez ici un script ordinaire pour le Bourne shell qui dirait :

 
Sélectionnez
#!/bin/sh
echo "Je suis un script shell"

Si vous donnez ce fichier à Perl, celui-ci lancera /bin/sh pour vous. C'est un peu bizarre, mais cela facilite la vie à ceux dont les machines ne reconnaissent pas #!, parce qu'ils peuvent — en positionnant leur variable d'environnement SHELL — indiquer à un programme (comme un gestionnaire de courrier) que leur shell est /usr/bin/perl et Perl enverra le programme à l'interpréteur qui leur convient, même si leur noyau est trop stupide pour le faire tout seul.

Mais revenons aux scripts Perl qui sont vraiment des scripts Perl. Après avoir localisé votre script, Perl compile le programme complet sous une forme interne (voir le chapitre 18, Compilation). S'il y a une erreur de compilation, l'exécution du script n'est même pas lancée. (Au contraire du script shell typique ou du fichier de commande, qui pourrait s'exécuter partiellement avant de trouver une erreur de syntaxe.) Si le script est syntaxiquement correct, il est exécuté. S'il arrive à la fin sans avoir rencontré un opérateur exit ou die, un exit(0) implicite est fourni par Perl pour indiquer le succès de l'opération à celui qui vous appelait. (Au contraire du programme C typique, où vous êtes susceptible d'obtenir un statut de sortie aléatoire si votre programme ne fait que se terminer de la manière normale.)

19-1-a. #! et isolement (quotation) sur les systèmes non-Unix

La technique #!, propre à Unix, peut être simulée sur d'autres systèmes :

Macintosh

  • Un programme Perl sur Macintosh aura la paire Type/Creator appropriée, de façon à ce qu'un double clic invoque l'application Perl.

MS-DOS

  • Créez un fichier batch pour lancer votre programme et codifiez-le dans ALTERNATIVE_SHEBANG. Voir le fichier dosish.h dans les sources de la distribution Perl pour plus d'informations à ce sujet.

OS/2

  • Ajoutez cette ligne :
 
Sélectionnez
extproc perl -S -vos_options
  • en tant que première ligne des fichiers *.cmd (-S contourne un bogue dans la manière dont cmd.exe gère « extproc »).

VMS

  • Mettez ces lignes :
 
Sélectionnez
$ perl -mesopt 'f$env("procedure")' 'p1' 'p2' 'p3' 'p4' 'p5' 'p6' 'p7' 'p8' !
$ exit++ + ++$status != 0 and $exit = $status = undef;
  • au début de votre programme, où -mesopt représente toutes les options de ligne de commande que vous voulez passer à Perl. Vous pouvez désormais invoquer le programme directement en tapant perl programme, en tant que procédure DCL en disant @programme ou implicitement via DCL$PATH en utilisant juste le nom du programme. Cette incantation est un peu difficile à mémoriser, mais Perl l'affichera pour vous si vous tapez perl "-V:startperl". Si vous n'arrivez pas à vous souvenir de ça — hé bien, c'est la raison pour laquelle vous avez acheté ce livre.

Win??

  • Lorsque vous utilisez la distribution ActiveState de Perl, sous certaines variantes de la suite de systèmes d'exploitation Windows de Microsoft (c'est-à-dire, Win95, Win98, Win00,(154) WinNT, mais pas Win3.1), la procédure d'installation de Perl modifie la base de registre de Windows pour associer l'extension .pl avec l'interpréteur Perl.
  • Si vous installez un autre portage de Perl, y compris celui du répertoire Win32 de la distribution de Perl, vous devrez alors modifier vous-même la base de registre de Windows.
  • Remarquez que l'utilisation d'une extension .pl implique que vous ne pourrez plus faire la différence entre un programme exécutable Perl et un fichier de bibliothèque Perl (« perl library »). Vous pourriez plutôt utiliser .plx pour un programme Perl afin d'éviter ceci. Ce n'est plus très ennuyeux aujourd'hui que la plupart des modules Perl se trouvent dans des fichiers .pm.

Les interpréteurs de commandes les systèmes non-Unix ont des idées extraordinairement différentes des shells Unix concernant l'isolement (quoting). Il vous faudra apprendre quels sont les caractères spéciaux de votre interpréteur de commandes (on trouve couramment *, \ et ") et comment protéger les espaces et ces caractères spéciaux pour lancer des scripts sur une seule ligne (one-liners) via l'option -e. Vous pourriez également avoir à changer un seul % en %% ou le protéger avec une séquence d'échappement, s'il s'agit d'un caractère spécial pour votre shell.

Sur certains systèmes, vous devrez peut-être changer les apostrophes en guillemets. Mais ne faites pas cela sur les systèmes Unix ou Plan9 ou n'importe quel système où tourne un shell à la Unix, comme les systèmes du MKS Toolkit ou du paquetage Cygwin produit par les gens de chez Cygnus, appartenant maintenant à Redhat. Le nouvel émulateur d'Unix, Interix, produit par Microsoft, commence également à devenir, hum, hum, intérixant.

Par exemple, sur Unix et Mac OS X, utilisez :

 
Sélectionnez
% perl -e 'print "Salut tout le monde\n"'

Sur Macintosh (avant MAC OS X), utilisez :

 
Sélectionnez
print "Salut tout le monde\n"

puis lancez « Mon_script » ou Shift-Command-R.

Sur VMS, utilisez :

 
Sélectionnez
$ perl -e "print ""Salut tout le monde\n"""

ou encore, avec qq// :

 
Sélectionnez
$ perl -e "print qq(Salut tout le monde\n)"

Et sur MS-DOS, etc., utilisez :

 
Sélectionnez
A:> perl -e "print \"Salut tout le monde\n\""

ou utilisez qq// pour choisir vos propres caractères d'isolement :

 
Sélectionnez
A:> perl -e "print qq(Salut tout le monde\n)"

Le problème est qu'aucun de ces exemples n'est fiable : cela dépend de l'interpréteur de commandes que vous utilisez. Si le shell était 4DOS, ceci fonctionnerait probablement mieux :

 
Sélectionnez
perl -e "print <Ctrl-x>"Salut tout le monde\n<Ctrl-x>""

Le programme CMD.EXE dans Windows NT semble avoir récupéré beaucoup de fonctionnalités du shell standard Unix lorsque tout le monde avait le dos tourné, mais essayez de trouver de la documentation sur ses règles concernant l'isolement (quoting rules) !

Sur Macintosh,(155) tout ceci dépend de l'environnement que vous utilisez. MPW, qui peut être utilisé comme un shell pour MacPerl, est très proche des shells Unix pour son support de différentes variantes d'isolement, si ce n'est qu'il utilise allègrement les caractères non ASCII de Macintosh comme caractères de contrôle.

Il n'y a pas de solution générale à tout ceci. C'est tout bonnement une belle pagaille. Si vous n'êtes pas sur un système Unix, mais que vous voulez faire des choses avec la ligne de commande, vous avez plus vite fait d'acquérir un meilleur interpréteur de commandes que celui que votre constructeur vous a fourni, ce qui ne devrait pas poser trop de difficultés.

Ou écrivez le tout en Perl et oubliez les scripts sur une seule ligne.

19-1-b. Emplacement de Perl

Bien que cela puisse paraître évident, Perl n'est utile que lorsque les utilisateurs peuvent facilement le trouver. Autant que possible, il est bon d'avoir à la fois /usr/bin/perl et /usr/ local/bin/perl comme liens symboliques vers le véritable binaire. Si cela n'est pas possible, les administrateurs système sont fortement encouragés à mettre Perl et les utilitaires associés dans un répertoire généralement présent dans le PATH standard d'un utilisateur ou dans un quelconque autre endroit qui soit évident et pratique.

Dans ce livre, nous utilisons la notation standard #!/usr/bin/perl sur la première ligne d'un programme, pour désigner le mécanisme fonctionnant sur votre système, quel qu'il soit. Si vous avez besoin de lancer une version particulière de Perl, utilisez un chemin spécifique :

 
Sélectionnez
#!/usr/local/bin/perl5.6.0

Si vous voulez juste utiliser une version au moins égale à un certain numéro de version, mais que vous vous fichez des versions supérieures, placez une instruction telle que celle-ci vers le début de votre programme :

 
Sélectionnez
use v5.6.0;

(Remarque : les versions plus anciennes de Perl utilisaient des numéros comme 5.005 ou 5.004_05. De nos jours, nous les verrions plutôt comme 5.5.0 et 5.4.5, mais les versions de Perl antérieures à 5.6.0 ne comprendraient pas cette notation.)

19-1-c. Options

Une option de la ligne de commande ne comportant qu'un seul caractère sans son argument peut toujours être combinée (reliée) avec l'option suivante.

 
Sélectionnez
#!/usr/bin/perl -spi.bak # comme -s -p -i.bak

Les options sont également appelées des flags ou des drapeaux ou des switchs (au singulier switch). Peu importe le nom que vous leur donnez, voici celles que Perl reconnaît :

--

  • Termine le traitement des options, même si l'argument suivant commence par un moins. Il n'a aucun autre effet.

-0NUM_OCT
-0

  • Spécifie le séparateur d'enregistrements ($/) comme étant un nombre octal. Si NUM_OCT n'est pas présent, le séparateur est le caractère nul (c'est-à-dire le caractère 0, soit " en Perl). D'autres options peuvent précéder ou suivre le nombre octal.
  • Par exemple, si vous disposez d'une version de find(1) pouvant afficher les noms de fichier terminés par le caractère nul, vous pouvez écrire ceci :
 
Sélectionnez
% find . -name '*.bak' -print0 | perl -n0e unlink
  • La valeur spéciale 00 fait que Perl lira les fichiers en mode paragraphe, ce qui est équivalent à positionner la variable $/ à "". La valeur 0777 fait que Perl gobera des fichiers entiers d'un seul coup. Cela revient à indéfinir la variable $/. Nous employons 0777 puisqu'il n'existe pas de caractère ASCII de cette valeur. (Malheureusement, il existe un caractère Unicode de cette valeur, \N{LATIN SMALL LETTER O WITH STROKE AND ACUTE}, mais quelque chose nous dit que vous ne l'utiliserez pas comme séparateur pour vos enregistrements.)

-a

  • Active le mode autosplit, mais seulement lorsque cette option est utilisée avec un -n ou un -p. Une commande split implicite est d'abord exécutée sur le tableau @F dans la boucle while implicite produite par le -n ou le -p. Ainsi :
 
Sélectionnez
% perl -ane 'print pop(@F), "\n";'
  • est équivalent à :
 
Sélectionnez
while (<>) {
    @F = split(' ');
    print pop(@F), "\n";
}
  • Un séparateur de champ différent peut être spécifié en passant une expression régulière pour split à l'option -F. Par exemple, ces deux appels sont équivalents :
 
Sélectionnez
% awk -F: '$7 && $7 !~ /^\/bin/' /etc/passwd
% perl -F: -lane 'print if $F[6] && $F[6] !~ m(^/bin)' /etc/passwd

-c

  • Fait vérifier par Perl la syntaxe du script avant de sortir sans exécuter ce qu'il vient de compiler. Techniquement, Perl en fera un peu plus que ça : il exécutera tous les blocs BEGIN ou CHECK et toutes les directives use, puisque ceux-ci sont censés se produire avant l'exécution du programme. Cependant, les blocs END ou INIT ne sont plus exécutés. L'ancien comportement, toutefois rarement utile, peut toujours être obtenu en mettant :
 
Sélectionnez
BEGIN { $^C = 0; exit; }
  • à la fin de votre script principal.

-C

  • Active l'utilisation par Perl des API de caractères larges (wide-character) sur le système cible, si elles sont implémentées (avec la version 5.6.0, cela ne fonctionne que sur les plates-formes Microsoft). La variable spéciale ${^WIDE_SYSTEM_CALLS} reflète l'état de cette option.

-d

  • Lance le script sous le débogueur Perl. Voir le chapitre 20, Le débogueur Perl.

-d:MODULE

  • Lance le script sous le contrôle d'un module de débogage ou de traçage installé dans la bibliothèque Perl sous le nom de Devel::MODULE. Par exemple, -d:DProf exécute le script avec le profileur Devel::DProf. Voir également le paragraphe sur le débogage au chapitre 20.

-DLETTRES
-DNOMBRE

  • Active les drapeaux de débogage. (Ceci ne fonctionne que si le débogage est compilé dans votre Perl, comme décrit ci-dessous.) Vous pouvez spécifier soit un NOMBRE valant la somme des bits désirés, soit une liste de LETTRES. Pour voir comment il exécute votre script, employez, par exemple, -D14 ou -Dslt. Une autre valeur utile est -D1024 ou -Dx, qui liste l'arbre syntaxique compilé. Et -D512 ou -Dr affiche les expressions régulières compilées. La valeur numérique est disponible en interne grâce à la variable spéciale $^D. Le tableau 19-1 liste les valeurs de bits assignées :
Tableau 19-1. Options de -D

Bit

Lettre

Signification

1

p

Mise en tokens et analyse syntaxique.

2

s

Instantanés de la pile.

4

l

Traitement de la pile d'étiquettes.

8

t

Trace de l'exécution.

16

o

Résolution des méthodes et des surcharges.

32

c

Conversions chaînes/numérique.

64

p

Affichage des commandes de préprocesseur pour -P.

128

m

Allocation mémoire.

256

F

Traitements des formats.

512

R

Analyse et exécution d'expressions régulières.

1024

X

Vidage mémoire (dump) de l'arbre syntaxique.

2048

U

Vérification du marquage.

4096

L

Fuites de mémoire (nécessite de compiler Perl avec -DLEAKTEST).

8192

H

Vidage mémoire (dump) de hachage— usurpe values().

16384

X

Allocation de mémoires de travail (scratchpad).

32768

D

Nettoyage

-5536

S

Synchronisation de threads.

  • Tous ces drapeaux exigent un exécutable Perl ayant été spécialement compilé pour le débogage. Toutefois, comme ce n'est pas le cas par défaut, vous serez incapable d'utiliser l'option -D sauf si votre administrateur système ou vous-même avez compilé cette version spéciale de Perl pour le débogage. Voir le fichier INSTALL dans le répertoire des sources de Perl pour plus de détails, mais, pour résumer, vous devez passer -DDEBUGGING à votre compilateur C lorsque vous compilez Perl lui-même. Ce drapeau est automatiquement positionné si vous incluez l'option -g lorsque Configure vous pose des questions à propos des drapeaux de l'optimisateur et du débogueur.
  • Si vous essayez seulement d'obtenir une impression de chaque ligne de code Perl au fur et à mesure qu'il s'exécute (de la même manière que sh -x le fait pour les scripts shell), vous ne pouvez pas utiliser l'option -D de Perl. À la place, faites ceci :
 
Sélectionnez
# Syntaxe du Bourne shell
$ PERLDB_OPTS="NonStop=1 AutoTrace=1 frame=2" perl -dS programme

# Syntaxe de csh
% (setenv PERLDB_OPTS "NonStop=1 AutoTrace=1 frame=2"; perl -dS programme)
  • Voir le chapitre 20 pour les détails et d'autres possibilités.

-e CODE_PERL

  • Peut être utilisé pour entrer une ou plusieurs lignes de script. Si -e est utilisé, Perl ne cherchera pas le nom du programme dans la liste d'arguments. L'argument CODE_PERL est traité comme s'il se terminait par un saut de ligne, ainsi, plusieurs commandes -e peuvent être données pour former un programme de plusieurs lignes. (Assurez-vous de mettre des points-virgules comme vous le feriez dans un programme normal stocké dans un fichier.) Ce n'est pas parce que -e fournit un saut de ligne à chaque argument que vous devez utiliser plusieurs options -e; si votre shell autorise l'isolement (quoting) sur plusieurs lignes, comme sh, ksh ou bash, vous pouvez alors passer un script sur plusieurs lignes comme un seul argument de -e :
 
Sélectionnez
$ perl -e 'print "Salut, ";
          print "@ARGV !\n";' la compagnie
Salut, la compagnie !
  • Avec csh, il vaut probablement mieux utiliser plusieurs options -e :
 
Sélectionnez
% perl -e 'print "Salut, ";' \
       -e 'print "@ARGV !\n";' la compagnie
Salut, la compagnie !
  • Les sauts de ligne, qu'ils soient implicites ou explicites, sont pris en compte dans la numérotation des lignes, ainsi, le second print se trouve à la ligne 2 du script de -e, dans les deux cas.

-FMOTIF

  • Spécifie le motif selon lequel s'effectue l'éclatement (split) lorsque le mode autosplit est activé via l'option -a (sinon, n'a aucun effet). Le motif peut être entouré par des slash (//), des guillemets ("") ou des apostrophes (''). Si ce n'est pas le cas, il sera automatiquement mis entre apostrophes. Souvenez-vous que pour passer des apostrophes ou des guillemets d'isolement via un shell, vous devrez isoler ces apostrophes et la manière dont vous pouvez le faire dépend du shell.

-h

  • Affiche un résumé des options de ligne de commande de Perl.

-iEXTENSION
-i

  • Spécifie que les fichiers traités par la construction <> doivent être édités sur place. Ceci est réalisé en renommant le fichier d'entrée, en ouvrant le fichier de sortie d'après le nom d'origine et en sélectionnant ce fichier de sortie comme fichier par défaut pour les appels à print, printf et write.(156) L'EXTENSION est utilisée pour modifier le nom de l'ancien fichier afin d'en faire une copie de sauvegarde. Si aucune EXTENSION n'est fournie, aucune sauvegarde n'est effectuée et le fichier actuel est écrasé. Si l'EXTENSION ne contient pas de *, la chaîne est alors ajoutée à la fin du nom de fichier actuel. Si l'EXTENSION contient un ou plusieurs caractères *, chaque * est alors remplacé par le nom du fichier actuellement en cours de traitement. En termes de Perl, vous pourriez penser à ceci comme :
 
Sélectionnez
($sauvegarde = $extension) =~ s/\*/$nom_fichier/g;
  • Ceci vous permet d'utiliser un préfixe pour le fichier de sauvegarde, au lieu — ou même en complément — d'un suffixe :
 
Sélectionnez
% perl -pi'orig_*' -e 's/truc/machin/' xyz # sauvegarde dans 'orig_xyz'
  • Vous pouvez même mettre les copies de sauvegarde des fichiers originaux dans un autre répertoire (à condition que le répertoire existe déjà) :
 
Sélectionnez
% perl -pi'ancien/*.orig' -e 's/truc/machin/' xyz # sauvegarde dans
'ancien/xyz.orig'
  • Les scripts sur une seule ligne sont équivalents dans chaque paire :
 
Sélectionnez
% perl -pi -e 's/truc/machin/' xyz         # écrase le fichier actuel
% perl -pi'*' -e 's/truc/machin/' xyz      # écrase le fichier actuel

% perl -pi'.orig' -e 's/truc/machin/' xyz  # sauvegarde dans 'xyz.orig'
% perl -pi'*.orig' -e 's/truc/machin/' xyz # sauvegarde dans 'xyz.orig'
  • Depuis le shell, écrire :
 
Sélectionnez
% perl -p -i.orig -e "s/truc/machin/;"
  • revient au même que d'utiliser le programme :
 
Sélectionnez
#!/usr/bin/perl -pi.orig
s/truc/machin/;
  • Ce qui est un raccourci pratique pour le programme éminemment plus long :
 
Sélectionnez
#!/usr/bin/perl
$extension = '.orig';
LIGNE: while (<>) {
    if ($ARGV ne $ancien_argv) {
        if ($extension !~ /\*/) {
            $sauvegarde = $ARGV . $extension;
        }
        else {
            ($sauvegarde = $extension) =~ s/\*/$ARGV/g;
        }
        unless (rename($ARGV, $sauvegarde)) {
            warn "impossible de renommer $ARGV en $sauvegarde : $!\n";
            close ARGV;
            next;
        }
        open(ARGV_SORTIE, ">$ARGV");
        select(ARGV_SORTIE);
        $ancien_argv = $ARGV;
    }
    s/truc/machin/;
}
continue {
    print; # ceci écrit dans le fichier de nom original
}
select(STDOUT);
  • Ce code un peu long est virtuellement identique au simple code sur une seule ligne avec l'option -i, hormis le fait que la forme -i n'a pas besoin de comparer $ARGV à $ancien_argv pour savoir quand le nom de fichier a changé. Mais elle utilise bien ARGV_SORTIE pour le handle de fichier sélectionné et restaure, après la boucle, l'ancien STDOUT comme handle de fichier de sortie par défaut. Comme le code ci-dessus, Perl crée le fichier de sauvegarde sans tenir compte du fait que la sortie ait réellement changé ou non. Voir la description de la fonction eof sans parenthèses pour des exemples sur la manière de détecter la fin de chaque fichier d'entrée, au cas où vous voudriez ajouter des lignes à chaque fichier ou remettre à zéro la numérotation des lignes.
  • Si, pour un fichier donné, Perl est incapable de créer le fichier de sauvegarde comme l'EXTENSION le spécifie, il émettra un avertissement à cet effet et continuera à traiter les autres fichiers.
  • Vous ne pouvez pas utiliser -i pour créer des répertoires ou pour enlever des extensions à des fichiers. Vous ne pouvez pas non plus l'utiliser avec un ~ indiquant le répertoire maison (home directory) — ce qui est d'autant mieux, car certaines personnes aiment utiliser ce caractère pour leurs fichiers de sauvegarde :
 
Sélectionnez
% perl -pi~ -e 's/truc/machin/' fichier1 fichier2 fichier3...
  • Enfin, l'option -i n'arrête pas l'exécution de Perl si aucun nom de fichier n'est donné sur la ligne de commande. Lorsque cela arrive, aucune sauvegarde n'est effectuée puisqu'on ne peut pas déterminer quel est le fichier original et le traitement s'applique de STDIN vers STDOUT comme on pouvait s'y attendre.

-IREPERTOIRE

  • Les répertoires spécifiés par -I sont ajoutés au début de @INC, qui contient le chemin de recherche des modules. -I dit aussi au préprocesseur où il faut chercher les fichiers d'inclusion. Le préprocesseur C est invoqué par -P ; il recherche par défaut dans /usr/include et /usr/lib/perl. À moins que vous ne soyez sur le point d'utiliser le préprocesseur C (ce que quasiment plus personne ne fait), vous feriez mieux d'utiliser la directive use lib dans votre script. Cependant, comme use lib, l'option -I ajoute implicitement les répertoires spécifiques à la plate-forme. Voir use lib au chapitre 31, Modules de pragmas pour plus de détails.

-lNUM_OCT
-l

  • Active le traitement de fin de ligne automatique. Son effet est double : premièrement, il fait automatiquement un chomp sur la terminaison de ligne quand il est employé avec -n ou -p et deuxièmement, il positionne $\ à la valeur de NUM_OCT afin que toute instruction d'affichage ajoute un terminateur de ligne de valeur ASCII NUM_OCT. Si NUM_OCT est omis, -l positionne $\ à la valeur actuelle de $/, généralement un saut de ligne. Par exemple, pour couper les lignes à quatre-vingts colonnes, écrivez ceci :
 
Sélectionnez
% perl -lpe 'substr($_, 80) = ""'
  • Remarquez que l'affectation $\ = $/ est faite lorsque l'option est traitée, le séparateur d'enregistrements d'entrée peut donc être différent de celui de sortie si l'option -l est suivie d'une option -0 :
 
Sélectionnez
% gnufind / -print0 | perl -ln0e 'print "trouvé $_" if -p'
  • Cela positionne $\ à la valeur saut de ligne, puis $/ au caractère nul. (Remarquez que 0 aurait été interprété comme faisant partie de l'option -l s'il avait suivi immédiatement le -l. C'est pourquoi nous avons inséré l'option -n entre les deux.)

-m et -M

  • Ces options chargent un MODULE comme si vous aviez exécuté un use, à moins que vous n'ayez spécifié -MODULE au lieu de MODULE, auquel cas no est invoqué. Par exemple, -Mstrict est identique à use strict, alors que -M-strict est identique à no strict.
  • -mMODULE

    • Exécute use MODULE() avant d'exécuter votre script.
  • -MMODULE-M'MODULE...'

    • Exécute use MODULE avant d'exécuter votre script. La commande est formée par simple interpolation du reste de l'argument suivant le -M, vous pouvez donc isoler l'argument par des apostrophes pour ajouter du code supplémentaire après le nom du module, par exemple, -M'module qw(truc machin)'.
  • -MMODULE=arg1,arg2...

    • Un peu de sucre de syntaxe vous permet d'écrire également -Mmodule=truc,machin comme raccourci pour -M'module qw(truc machin)'. Cette possibilité dispense des apostrophes lorsqu'on importe des symboles. Le code réellement généré par -Mmodule=truc,machin est :

       
      Sélectionnez
      use module split(/,/, q{truc,machin})
  • Remarquez que la forme avec = élimine la distinction entre -m et -M, mais il vaut mieux utiliser la forme en majuscules pour éviter toute confusion. Vous ne pouvez utiliser les options -M et -m que depuis une véritable invocation de Perl depuis la ligne de commande, non comme des options trouvées sur la ligne #!. (Hé, si vous alliez le mettre dans un fichier, pourquoi ne pas simplement écrire à la place le use ou le no équivalent ?)

-n

  • Pousse Perl à se comporter comme si la boucle suivante encadrait le script, ce qui le fait itérer sur les noms de fichiers en argument à la manière de sed -n ou de awk :
 
Sélectionnez
LINE:
while (<>) {
    ...     # le script vient ici
}
  • Vous pouvez utiliser LINE (LIGNE en anglais) comme une étiquette de boucle depuis l'intérieur de votre script, même si vous ne pouvez pas voir la véritable étiquette dans votre fichier.
  • Remarquez que les lignes ne sont pas affichées par défaut. Voir -p pour ce faire. Voici un moyen efficace de détruire tous les fichiers datant de plus d'une semaine :
 
Sélectionnez
find . -mtime +7 -print | perl -nle 'unlink;'
  • Cette opération est plus rapide que l'option -exec de find(1), car vous n'avez pas à lancer un processus à chaque nom de fichier trouvé. Par une coïncidence stupéfiante, les blocs BEGIN et END peuvent être utilisés pour prendre le contrôle avant ou après la boucle implicite, exactement comme dans awk.

-p

  • Pousse Perl à se comporter comme si la boucle suivante encadrait le script, ce qui le fait itérer sur les noms de fichier en argument à la manière de sed :

     
    Sélectionnez
    LINE:
    while (<>) {
        ...         # le script vient ici
    }
    continue {
        print or die "-p destination: $!\n";
    }
  • Vous pouvez utiliser LINE (LIGNE en anglais) comme étiquette de boucle depuis l'intérieur de votre script, même si vous ne pouvez pas voir la véritable étiquette dans votre fichier.

  • Si un fichier désigné par un argument ne peut pas être ouvert pour une raison ou pour une autre, Perl vous en avertit et passe au fichier suivant. Remarquez que les lignes sont affichées automatiquement. Toujours par une coïncidence stupéfiante, les blocs BEGIN et END peuvent être utilisés pour prendre le contrôle avant ou après la boucle implicite, exactement comme dans awk.

-P

  • Pousse votre script à passer par le préprocesseur C avant la compilation par Perl. (Comme les commentaires et les directives de cpp(1) commencent tous par le caractère #, il vaut mieux éviter de commencer les commentaires par des mots-clefs du préprocesseur C comme « if », « else » ou « define ».) Que vous utilisiez l'option -P ou non, Perl accorde toujours de l'attention aux directives #line pour contrôler le numéro de ligne et le nom de fichier, n'importe quel préprocesseur peut donc informer Perl sur de telles choses. Voir la section Générer du Perl dans d'autres langages au chapitre 24, Techniques couramment employées.

-s

  • Permet une analyse rudimentaire de la ligne de commande afin de rechercher les options suivant le nom du script, mais précédant tout nom de fichier ou une terminaison d'options --. Toute option trouvée est éliminée de @ARGV et une variable du même nom que l'option est positionnée dans Perl. Aucun accolement d'options n'est permis, car les options de plusieurs caractères sont autorisées.
  • Le script suivant affiche « vrai » si, et seulement si, le script est invoqué avec une option -truc.
 
Sélectionnez
#!/usr/bin/perl -s
if ($truc) { print "vrai\n"; }
  • Si l'option est de la forme -xxx=yyy, la variable $xxx est positionnée à ce qui suit ce signe égal dans l'argument (« yyy » en l'occurrence). Le script suivant affiche « vrai » si et seulement si le script est invoqué avec une option -truc=machin.
 
Sélectionnez
#!/usr/bin/perl -s
if ($truc eq 'machin') { print "vrai\n"; }

-S

  • Pousse Perl à utiliser la variable d'environnement PATH pour rechercher le script (à moins que le nom du script ne contienne des séparateurs de répertoires). Généralement, cette option est utilisée pour émuler le démarrage par #! sur les plates-formes qui ne l'implémentent pas. Sur de nombreuses plates-formes disposant d'un shell compatible avec le Bourne Shell ou le C shell, vous pouvez utiliser ceci :
 
Sélectionnez
#!/usr/bin/perl
eval "exec /usr/bin/perl -S $0 $*"
        if $lance_sous_un_shell;
  • Le système ignore la première ligne et donne le script à /bin/sh, qui essaye d'exécuter le script Perl comme un shell script. Le shell exécute la seconde ligne comme une commande shell normale et démarre donc l'interpréteur Perl. Sur certains systèmes, $0 ne contient pas toujours le nom de chemin complet et le -S indique alors à Perl de rechercher le script si nécessaire. Après que Perl l'a trouvé, il analyse les lignes et les ignore, car la variable $lance_sous_un_shell n'est jamais vraie. ${1+"$@"} vaudrait mieux que, $* mais cette expression ne fonctionne pas si le script est interprété par csh. Afin de lancer sh au lieu de csh, certains systèmes doivent remplacer la ligne #! par une ligne ne contenant qu'un deux-points, qui sera poliment ignoré par Perl. D'autres systèmes ne peuvent contrôler cela et ont besoin d'un montage totalement déviant pouvant fonctionner sous csh, sh ou perl, comme ce qui suit :
 
Sélectionnez
eval '(exit $?0)' && eval 'exec /usr/bin/perl -S $0 ${1+"$@"}'    &
    eval 'exec /usr/bin/perl -S $0 $argv:q'
        if 0;
  • Oui. C'est laid, tout autant que les systèmes qui fonctionnent(157) ainsi. Sur certaines plates-formes, l'option -S pousse également Perl à ajouter des suffixes au nom de fichier pendant qu'il le recherche. Par exemple, sur les plates-formes Win32, les suffixes .bat et .cmd sont ajoutés si la recherche du fichier original échoue et que le nom ne se termine pas déjà par l'un de ces deux suffixes. Si votre version de Perl a été construite avec le débogage activé, vous pouvez utiliser l'option -Dp de Perl pour suivre la progression de la recherche. Si le nom de fichier fourni contient des séparateurs de répertoires (même simplement en tant que nom de chemin relatif, et non un chemin absolu) et si le fichier n'est pas trouvé, les plates-formes ajoutant implicitement des extensions de fichiers (non Unix) le feront et chercheront le fichier avec l'ajout de ces extensions, l'une après l'autre.
  • Sur les plates-formes comme DOS, si le script ne contient pas de séparateur de répertoires, il sera tout d'abord recherché dans le répertoire actuel avant de l'être dans le PATH. Sur les plates-formes Unix, le script sera recherché strictement dans PATH, à cause des problèmes de sécurité engendrés par l'exécution accidentelle de quelque chose dans le répertoire de travail courant sans en faire explicitement la demande.

-T

  • Force l'activation des vérifications « de marquage » afin que vous puissiez les tester. Ces vérifications ne sont généralement lancées qu'en exécutant en setuid ou setgid. Il vaut mieux les activer explicitement pour les programmes qui tournent sous le nom de quelqu'un d'autre, comme les programmes CGI. Voir le chapitre 23, Sécurité.
  • Remarquez que, pour des raisons de sécurité, Perl doit voir cette option assez tôt ; cela signifie habituellement qu'elle doit se trouver au début de la ligne de commande ou de la ligne #!. Si elle n'est pas trouvée assez tôt, Perl s'en plaint.

-u

  • Provoque un core dump de Perl après avoir compilé le script. Vous pouvez alors, en théorie, prendre ce core dump et le transformer en fichier exécutable grâce au programme undump (qui n'est pas fourni). Le démarrage est ainsi accéléré au détriment de l'espace disque (que vous pouvez minimiser en faisant strip sur l'exécutable). Si vous voulez exécuter une partie du script avant le vidage, utilisez plutôt l'opérateur dump de Perl. Remarque : la disponibilité de undump dépend des plates-formes ; il peut ne pas exister pour certains portages de Perl. Il a été remplacé par le nouveau générateur de code Perl-vers-C, qui est bien plus portable (mais toujours expérimental).

-U

  • Permet à Perl d'effectuer des opérations dangereuses. Actuellement, les seules opérations « dangereuses » sont la suppression de répertoires en tant que super-utilisateur et le lancement de programmes en setuid alors que les vérifications fatales de marquage sont transformées en avertissements. Remarquez que les avertissements doivent être activés pour engendrer les avertissements de vérifications de marquage.

-v

  • Affiche la version et le niveau de correctif (patch level) de votre exécutable Perl, ainsi que quelques informations supplémentaires.

-V

  • Affiche un résumé des principales valeurs de configuration de Perl et la valeur actuelle de @INC.

-V:NOM

  • Affiche sur STDOUT la valeur de la variable de configuration indiquée. Le NOM peut contenir des caractères d'expressions régulières, comme « . » pour correspondre à n'importe quel caractère ou « .* » pour correspondre à toute séquence de caractères optionnelle.
 
Sélectionnez
% perl -V:man.dir
man1dir='/usr/local/man/man1'
man3dir='/usr/local/man/man3'

% perl -V:'.*threads'
d_oldpthreads='undef'
use5005threads='define'
useithreads='undef'
usethreads='define'
  • Si vous demandez une variable de configuration qui n'existe pas, sa valeur sera rapportée comme « UNKNOWN » (INCONNUE). Les informations de configuration sont accessibles depuis l'intérieur d'un programme en utilisant le module Config, bien que les motifs ne soient pas implémentés pour les indices des hachages :
 
Sélectionnez
% perl -MConfig -le ' print $Config{man1dir}'
/usr/local/man/man1
  • Voir le module Config au chapitre 32, Modules standard.

-w

  • Affiche des messages d'avertissement concernant les variables qui ne sont mentionnées qu'une fois et sur les valeurs scalaires qui sont utilisées avant d'être positionnées. Prévient également des redéfinitions de sous-programme et des références à des handles de fichier indéfinis ou de ceux, ouverts en lecture seule, sur lesquels vous essayez d'écrire. Avertit également si vous utilisez des valeurs en tant que nombres alors qu'elles ne semblent pas numériques ou si vous utilisez un tableau comme s'il s'agissait d'un scalaire ou si vos sous-programmes rentrent dans plus de 100 récursions, ainsi que d'innombrables choses du même genre. Voir toutes les entrées marquées « (W) » au chapitre 33, Messages de diagnostic.
  • Cette option ne fait que positionner la variable globale $^W. Elle n'a pas d'effet sur les avertissements lexicaux — voir pour cela les options -W et -X. Vous pouvez activer ou désactiver des avertissements spécifiques via le pragma use warnings, décrit au chapitre 31.

-W

  • Active de manière inconditionnelle et permanente tous les avertissements tout au long du programme, même si les avertissements étaient désactivés localement en utilisant no warnings ou $^W = 0. Ceci comprend tous les fichiers chargés par use, require ou do. Pensez-y comme l'équivalent Perl de la commande lint(1).

-xREPERTOIRE
-x

  • Indique à Perl d'extraire un script qui est inclus dans un message. Les lignes résiduelles du début sont éliminées jusqu'à la première commençant par #! et contenant la chaîne « perl ». Toutes les options significatives de cette ligne après le mot « perl » seront appliquées. Si un nom de répertoire est spécifié, Perl basculera vers ce répertoire avant de lancer le script. L'option -x permet seulement d'éliminer les lignes superflues du début, pas les lignes superflues de la fin. Le script doit se terminer par __END__ ou par __DATA__ s'il y a des lignes superflues à ignorer après la fin du script. (Le script peut traiter au besoin tout ou partie des résidus restants via le handle de fichier DATA. Il pourrait même en théorie se déplacer avec seek au début du fichier et traiter les résidus du début.)

-X

  • Désactive de manière inconditionnelle et permanente tous les avertissements, exactement à l'opposé de ce que fait l'option -W.

19-2. Variables d'environnement

En plus des diverses options modifiant explicitement le comportement de Perl, vous pouvez positionner diverses variables d'environnement pour influencer divers comportements sous-jacents. La manière de positionner ces variables d'environnement est dépendante du système, mais une astuce que vous devriez connaître si vous employez sh, ksh ou bash, est que vous pouvez positionner temporairement une variable d'environnement pour une seule commande, comme s'il s'agissait d'une drôle de sorte d'option. Elle doit être positionnée devant la commande :

 
Sélectionnez
$ PATH='/bin:/usr/bin' perl ma_programmette

Vous pouvez faire quelque chose de similaire avec un sous-shell dans csh ou tcsh :

 
Sélectionnez
% (setenv PATH='/bin:/usr/bin'; perl ma_programmette)

Sinon, vous auriez généralement positionné les variables d'environnement dans un fichier dont le nom ressemblerait à .cshrc ou .profile dans votre répertoire maison. Sous csh et tcsh, vous auriez écrit :

 
Sélectionnez
% setenv PATH='/bin:/usr/bin'

Et sous sh, ksh et bash, vous auriez écrit :

 
Sélectionnez
$ PATH='/bin:/usr/bin'; export PATH

D'autres systèmes auront d'autres moyens pour les positionner d'une manière semi-permanente. Voici les variables d'environnement auxquelles Perl accorde de l'attention :

HOME

  • Utilisée si chdir est appelé sans argument.

LC_ALL, LC_CTYPE, LC_COLLATE, LC_NUMERIC, PERL_BADLANG

  • Variables d'environnement contrôlant comment Perl gère les données spécifiques aux langages naturels particuliers. Voir la documentation en ligne de perllocale.

LOGDIR

  • Utilisée si chdir est appelé sans argument et si HOME n'est pas positionnée.

PATH

  • Utilisée lors de l'exécution de sous-processus et dans la recherche du programme si l'option -S est utilisée.

PERL5LIB

  • Une liste de répertoires, séparés par des deux-points, dans lesquels rechercher les fichiers de bibliothèque de Perl avant de les chercher dans la bibliothèque standard et dans le répertoire courant. Tous les répertoires spécifiques à l'architecture aux endroits spécifiés sont automatiquement inclus s'ils existent. Si la variable d'environnement PERL5LIB n'est pas définie, on consulte PERLLIB pour assurer une compatibilité antérieure avec les versions plus anciennes.
  • Lors d'une exécution avec vérification du marquage (soit parce que le programme s'exécutait en setuid ou setgid, soit parce que l'option -T était utilisée), aucune des deux variables de recherche de bibliothèque n'est utilisée. De tels programmes doivent employer le pragma use lib dans cet objectif.

PERL5OPT

  • Options de ligne de commande par défaut. Les options dans cette variable sont considérées comme faisant partie de toute ligne de commande Perl. Seules les options -[DIMUdmw] sont autorisées. Lors d'une exécution avec vérifications de marquage (car le programme s'exécutait en setuid ou setgid ou si l'option -T était utilisée), cette variable est ignorée. Si PERL5OPT commence par -T, le marquage sera activé et toutes les options suivantes seront ignorées.

PERL5DB

  • La commande utilisée pour charger le code du débogueur. La valeur par défaut est :
 
Sélectionnez
BEGIN { require 'perl5db.pl' }
  • Voir le chapitre 20 pour d'autres utilisations de cette variable.

PERL5SHELL

  • (seulement sur les portages Microsoft) Peut être positionnée à une possibilité de shell que Perl doit utiliser en interne pour exécuter les commandes via des apostrophes inverses ou via system. La valeur par défaut est cmd.exe /x/c sur WinNT et command.com /c sur Win95. La valeur est considérée comme étant délimitée par des espaces. Préfixez tout caractère devant être isolé (comme un espace ou un antislash) par un antislash.
  • Remarquez que Perl n'utilise pas COMSPEC pour cela, car COMSPEC a un haut degré de variabilité entre les utilisateurs, ce qui amène à des soucis de portabilité. De plus, Perl peut utiliser un shell qui ne convienne pas à un usage interactif et le fait de positionner COMSPEC à un tel shell peut interférer avec le fonctionnement correct d'autres programmes (qui regardent habituellement COMSPEC pour trouver un shell permettant l'utilisation interactive).

PERLLIB

  • Une liste de répertoires, séparés par des deux-points, dans lesquels rechercher les fichiers de bibliothèque de Perl avant de les chercher dans la bibliothèque standard et dans le répertoire courant. Si PERL5LIB est définie, PERLLIB n'est pas utilisée.

PERL_DEBUG_MSTATS

  • Appropriée seulement si Perl est compilé avec la fonction malloc incluse dans la distribution de Perl (c'est-à-dire si perl -V:d_mymalloc vaut « define »). Si elle est positionnée, cela provoque un affichage de statistiques sur la mémoire après l'exécution. Si sa valeur est un entier supérieur à un, les statistiques sur la mémoire sont également affichées après la compilation.

PERL_DESTRUCT_LEVEL

  • Appropriée seulement si votre exécutable perl a été construit en activant le débogage, ceci contrôle le comportement de la destruction globale des objets et autres références.

À part celles-ci, Perl lui-même n'utilise pas d'autres variables d'environnement, sauf pour les rendre disponibles au programme s'exécutant et aux processus fils que lance ce programme. Certains modules, standard ou autres, peuvent se soucier d'autres variables d'environnement. Par exemple, le pragma use re utilise PERL_RE_TC et PERL_RE_COLORS, le module Cwd utilise PWD et le module CGI utilise les nombreuses variables d'environnement positionnées par votre démon HTTP (c'est-à-dire, votre serveur Web) pour passer des informations au script CGI.

Les programmes exécutés en setuid feraient bien d'exécuter les lignes suivantes avant de faire quoi que ce soit d'autre, simplement pour que les gens restent honnêtes :

 
Sélectionnez
$ENV{PATH} = '/bin:/usr/bin'; # ou ce dont vous avez besoin
$ENV{SHELL} = '/bin/sh' if exists $ENV{SHELL};
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};

Voir le chapitre 23 pour plus de détails.

20. Le débogueur Perl

Avant tout, avez-vous essayé le pragma use warnings ?

Si vous invoquez Perl avec l'option -d, votre programme tournera sous le débogueur Perl. Celui-ci fonctionne comme un environnement Perl interactif, offrant une invite (prompting) pour des commandes de débogage qui vous permettent d'examiner le code source, de positionner des points d'arrêt, d'afficher la pile de vos appels de fonctions, de changer la valeur des variables et ainsi de suite. Toute commande non reconnue par le débogueur est directement exécutée (en utilisant eval) en tant que code Perl dans le paquetage du code actuellement en train d'être débogué. (Le débogueur utilise le paquetage DB pour ses propres informations d'état, afin d'éviter de piétiner les vôtres.) C'est si formidablement pratique que les gens lancent souvent le débogueur tout seul pour tester des constructions Perl interactivement pour voir ce qu'elles font. Dans ce cas, le programme que vous dites à Perl de déboguer n'a pas d'importance, nous en choisirons donc un sans grande signification :

 
Sélectionnez
% perl -de 42

En Perl, le débogueur n'est pas un programme séparé de celui qui est débogué, comme il l'est d'habitude dans un environnement de programmation typique. L'option -d indique plutôt au compilateur d'insérer des informations sur le source dans les arbres syntaxiques qu'il est sur le point de passer à l'interpréteur. Cela signifie que votre code doit d'abord compiler correctement pour que le débogueur puisse s'en occuper. Puis si c'est le cas, l'interpréteur précharge un fichier de bibliothèque Perl spécial contenant le débogueur lui-même.

 
Sélectionnez
% perl -d /chemin/du/programme

Le programme s'arrêtera immédiatement avant la première instruction à l'exécution (mais voir la section Utilisation du débogueur concernant les instructions à la compilation) et vous demande d'entrer une commande de débogueur. Chaque fois que le débogueur s'arrête et vous montre une ligne de code, il affiche la ligne qu'il est sur le point d'exécuter, non celle qu'il vient d'exécuter.

Lorsque le débogueur rencontre une ligne, il vérifie d'abord s'il y a un point d'arrêt, affiche la ligne (si le débogueur est en mode trace), accomplit toutes les actions (créées avec la commande a décrite plus loin dans Commandes du débogueur) et finalement affiche une invite (prompt) à l'utilisateur s'il existe un point d'arrêt ou si le débogueur est en mode pas-à-pas. Sinon, il évalue la ligne normalement et continue avec la ligne suivante.

20-1. Utilisation du débogueur

L'invite du débogueur ressemble à :

 
Sélectionnez
DB<8>

ou même :

 
Sélectionnez
DB< <17> >

où le numéro indique combien de commandes vous avez effectuées. Un mécanisme d'historique ressemblant à celui de csh permet d'accéder à des commandes antérieures par leur numéro. Par exemple, !17 répéterait la commande numéro 17. Le nombre de signes inférieur/supérieur indique la profondeur du débogueur. Vous en obtenez plus d'un, par exemple, si vous vous trouvez déjà à un point d'arrêt et que vous affichez ensuite le résultat d'un appel de fonction qui comporte elle-même un point d'arrêt.

Si vous voulez entrer une commande multiligne, comme une définition de sous-programme comportant plusieurs instructions, vous pouvez protéger le saut de ligne qui termine normalement la commande du débogueur par un antislash. En voici un exemple :

 
Sélectionnez
DB<1> for (1..3) {     \
    cont:    print "ok\n"; \
    cont: }
    ok
    ok
    ok

Admettons que vous vouliez démarrer le débogueur sur un petit programme de votre cru (appelons-le puce_du_chameau(158)) et arrêtons-le dès qu'il arrive sur une fonction appelée infeste. Voici comment vous feriez cela :

 
Sélectionnez
% perl -d puce_du_chameau

Loading DB routines from perl5db.pl version 1.07
Editor support available.

Enter h or 'h h' for help, or `man perldebug' for more help.

main::(puce_du_chameau:2): parasite('bacterie', 4);
DB<1>

Le débogueur arrête votre programme juste avant la première instruction à l'exécution (mais voir ci-dessous les instructions à la compilation) et vous demande d'entrer une commande. Encore une fois, dès que le débogueur s'arrête pour vous montrer une ligne de code, il affiche celle qui est sur le point d'être exécutée, non celle qui vient de l'être. La ligne affichée peut ne pas ressembler exactement à celle présente dans votre fichier source, particulièrement si vous l'avez lancé par l'intermédiaire d'un préprocesseur.

Vous aimeriez maintenant vous arrêter dès votre programme arrive à la fonction infeste, vous y établissez donc un point d'arrêt comme ceci :

 
Sélectionnez
DB<1> b infeste
DB<2> c

Le débogueur continue maintenant jusqu'à ce qu'il tombe sur cette fonction, où il affiche ceci :

 
Sélectionnez
main::infeste(puce_du_chameau:8):     my $microbes = int rand(3);

Pour examiner une « fenêtre » de code source autour du point d'arrêt, utilisez la commande w :

 
Sélectionnez
DB<2> w
5     }
6
7      sub infeste {
8==>b      my microbes = int rand(3);
9:         our $Maitre;
10:        contaminer($Maitre);
11:        warn "bain nécessaire"
12            if $Maitre && $Maitre>isa("Humain");
13
14:        print "$microbes attrapés\n";

DB<2>

Comme vous pouvez le voir grâce au marqueur ==>, votre ligne courante est la numéro 8 et, grâce au b qui s'y trouve, vous savez qu'elle comporte un point d'arrêt. Si vous aviez positionné une action, il y aurait également eu un a. Les numéros avec un deux-points peuvent comporter des points d'arrêt, les autres ne le peuvent pas.

Pour voir qui a appelé qui, demandez la trace arrière de la pile (stack backtrace) en utilisant la commande T :

 
Sélectionnez
DB<2> T
$ = main::infeste called from file 'Deplacement.pm' line 4
@ = Deplacement::pattes(1, 2, 3, 4) called from file 'puce_du_chameau' line 5
. = main::parasite('bacterie', 4) called from file 'puce_du_chameau' ligne 2

Le premier caractère ($, @ ou .) indique si la fonction a été appelée respectivement dans un contexte scalaire, de liste ou vide. Il y a trois lignes, car vous étiez à une profondeur de trois fonctions lorsque vous avez lancé la trace arrière de la pile. Voici ce que signifie chaque ligne :

  • La première ligne dit que vous vous trouviez dans la fonction main::infeste lorsque vous avez lancé la trace de la pile. Elle vous indique que la fonction a été appelée dans un contexte scalaire depuis la ligne numéro 4 du fichier Deplacement.pm.
    Elle montre également qu'elle a été appelée sans aucun argument, ce qui veut dire qu'elle a été appelée en tant que &infeste au lieu de la manière normale infeste().
  • La deuxième ligne montre que la fonction Deplacement::pattes a été appelée dans un contexte de liste depuis la ligne numéro 5 du fichier puce_du_chameau, avec quatre arguments.
  • La troisième ligne montre que main::parasite a été appelée dans un contexte vide depuis la ligne numéro 2 de puce_du_chameau.

Si vous avez des instructions exécutables lors de la phase de compilation, comme du code venant de blocs BEGIN ou CHECK ou des instructions use, elles ne seront pas stoppées de manière ordinaire par le débogueur, alors que les require et les blocs INIT le sont, puisqu'ils interviennent après la transition vers la phase d'exécution (voir le chapitre 18, Compiler). Les instructions de la phase de compilation peuvent être tracées avec l'option AutoTrace positionnée dans PERLDB_OPTS.

Vous pouvez exercer un léger contrôle sur le débogueur Perl depuis l'intérieur de votre programme Perl lui-même. Vous pourriez faire ceci en positionnant, par exemple, un point d'arrêt automatique à un certain sous-programme lorsqu'un programme particulier s'exécute sous le débogueur. Cependant, depuis votre propre code Perl, vous pouvez transférer de nouveau le contrôle au débogueur en utilisant l'instruction suivante, qui est inoffensive si le débogueur n'est pas lancé :

 
Sélectionnez
$DB::single = 1;

Si vous positionnez $DB::single à 2, cela équivaut à la commande n, tandis qu'une valeur de 1 émule la commande s. La variable $DB::trace devrait être mise à 1 pour simuler le t.

Une autre façon de déboguer un module consiste à positionner un point d'arrêt au chargement :

 
Sélectionnez
DB<7> b load c:/perl/lib/Carp.pm
Will stop on load of 'c:/perl/lib/Carp.pm'.

puis de redémarrer le débogueur en utilisant la commande R. Pour un contrôle plus fin, vous pouvez utiliser b compile nom_sous_programme pour s'arrêter aussitôt qu'un sous-programme particulier est compilé.

20-2. Commandes du débogueur

Lorsque vous tapez des commandes dans le débogueur, vous n'avez pas besoin de les terminer par un point-virgule. Utilisez un antislash pour continuer les lignes (mais uniquement dans le débogueur).

Puisque le débogueur utilise eval pour exécuter des commandes, les initialisations avec my, our et local disparaîtront une fois la commande terminée. Si une commande du débogueur coïncide avec une quelconque fonction de votre propre programme, vous n'avez qu'à préfixer l'appel de fonction avec quelque chose qui ne ressemble pas à une commande du débogueur, comme un ; ou un + au début.

Si la sortie d'une commande interne du débogueur défile au-delà des limites de votre écran, vous n'avez qu'à préfixer la commande avec un symbole de pipe afin qu'elle passe par votre pager :

 
Sélectionnez
DB<1> |h

Le débogueur comprend une grande quantité de commandes et nous les avons divisées (quelque peu arbitrairement) dans les catégories suivantes : pas à pas et lancement, points d'arrêt, traçage, affichage, localisation de code, exécution automatique de commandes et, bien entendu, divers.

La commande la plus importante est peut-être h, qui fournit de l'aide. Si vous tapez hh à l'invite du débogueur, vous obtiendrez une page d'aide compacte conçue pour tenir sur un seul écran. Si vous tapez h COMMANDE, vous obtiendrez de l'aide sur cette COMMANDE du débogueur.

20-2-a. Pas à pas et lancement

Le débogueur fonctionne en parcourant votre programme pas-à-pas (stepping), ligne par ligne. Les commandes suivantes vous permettent de contrôler ce que vous court-circuitez et où vous vous arrêtez.

s
s EXPR

  • La commande s du débogueur avance dans le programme d'un seul pas. C'est-à-dire que le débogueur exécutera la prochaine ligne de votre programme jusqu'à atteindre une autre instruction, en descendant dans les appels de sous-programmes si nécessaire. Si la prochaine ligne à exécuter implique un appel de fonction, le débogueur s'arrête alors à la première ligne à l'intérieur de cette fonction. Si une EXPR est fournie et qu'elle comprend des appels de fonctions, celles-ci seront également parcourues pas à pas.

n
n EXPR

  • La commande n exécute des appels de sous-programme, sans les parcourir pas à pas, jusqu'au début de la prochaine instruction du même niveau (ou d'un niveau plus haut). Si une EXPR est fournie et qu'elle comprend des appels de fonctions, celles-ci seront exécutées en s'arrêtant avant chaque instruction.

<ENTRÉE>

  • Si vous ne faites qu'appuyer sur la touche <ENTRÉE> à l'invite du débogueur, la commande s ou n précédente est répétée.

.

  • La commande . renvoie le pointeur interne du débogueur vers la dernière ligne exécutée et affiche cette ligne.

r

  • Cette commande continue jusqu'au retour du sous-programme actuellement exécuté. Elle affiche la valeur renvoyée si l'option PrintRet est positionnée, ce qui est le cas par défaut.
20-2-b. Points d'arrêt

b
b LIGNE
b CONDITION
b LIGNE CONDITION
b NOM_SOUS_PROGRAMME
b NOM_SOUS_PROGRAMME CONDITION
b postpone NOM_SOUS_PROGRAMME
b postpone NOM_SOUS_PROGRAMME CONDITION
b compile NOM_SOUS_PROGRAMME
b load NOM_FICHIER

  • La commande b du débogueur positionne un point d'arrêt avant la LIGNE, indiquant au débogueur d'arrêter le programme à cet endroit pour que vous puissiez fureter autour. Si LIGNE est omise, la commande positionne un point d'arrêt sur la ligne qui est sur le point d'être exécutée. Si CONDITION est spécifiée, elle est évaluée à chaque fois que l'instruction est atteinte : un point d'arrêt n'est déclenché que si la CONDITION est vraie. Les points d'arrêt ne peuvent être positionnés que sur les lignes commençant par une instruction exécutable. Remarquez que les conditions n'emploient pas if :
 
Sélectionnez
b237 $x >30
b 237 ++$compteur237 < 11
b 33 /motif/i
  • La forme b NOM_SOUS_PROGRAMME positionne un point d'arrêt (qui peut être conditionnel) avant la première ligne du sous-programme cité. NOM_SOUS_PROGRAMME peut être une variable contenant une référence de code ; si c'est le cas, CONDITION n'est pas implémentée.
  • Il existe plusieurs manières de positionner un point d'arrêt sur du code qui n'a même pas encore été compilé. La forme b postpone positionne un point d'arrêt (qui peut être conditionnel) à la première ligne de NOM_SOUS_PROGRAMME après qu'il a été compilé.
  • La forme b compile positionne un point d'arrêt sur la première instruction devant être exécutée après que NOM_SOUS_PROGRAMME a été compilé. Remarquez qu'au contraire de la forme avec postpone, cette instruction se trouve à l'extérieur du sous-programme en question, car ce dernier n'a pas encore été appelé, il a seulement été compilé.
  • La forme b load positionne un point d'arrêt sur la première ligne exécuté du fichier. Le NOM_FICHIER devrait être un nom de chemin complet tel qu'on l'aurait trouvé dans les valeurs de %INC.

d
d LIGNE

  • Cette commande supprime le point d'arrêt de la LIGNE; si celle-ci est omise, la commande supprime le point d'arrêt sur la ligne qui est sur le point d'être exécutée.

D

  • Cette commande supprime tous les points d'arrêt.

L

  • Cette commande liste tous les points d'arrêt et toutes les actions.

c
C LIGNE

  • Cette commande continue l'exécution, en insérant éventuellement un point d'arrêt, actif une seule fois, à la LIGNE spécifiée.
20-2-c. Traçage

T

  • Cette commande produit une trace arrière de la pile (backtrace stack).

t
t EXPR

  • Cette commande bascule l'activation ou la désactivation du mode trace, qui imprime chaque ligne de votre programme au moment où elle est évaluée. Voir également l'option AutoTrace, présentée plus loin dans ce chapitre. Si une EXPR est fournie, le débogueur tracera son exécution. Voir également plus loin la section Exécution autonome.

W
W EXPR

  • Cette commande ajoute EXPR en tant qu'expression globale de surveillance. (Une expression de surveillance est une expression qui provoquera un point d'arrêt lorsque sa valeur changera.) Si aucune EXPR n'est fournie, toutes les expressions de surveillance sont supprimées.
20-2-d. Affichage

Le débogueur de Perl comprend plusieurs commandes pour examiner des structures de données pendant que votre programme est immobilisé à un point d'arrêt.

p
p EXPR

  • Cette commande est identique à print DB::OUTEXPR dans le paquetage courant. En particulier, comme il s'agit de la propre fonction print de Perl, les structures de données imbriquées et les objets ne sont pas affichés — utiliser la commande x pour ce faire. Le handle DB::OUT imprime sur votre terminal (ou peut-être une fenêtre d'éditeur) quel que soit l'endroit où la sortie standard a pu être redirigée.

x

x EXPR

  • La commande x évalue son expression dans un contexte de liste et affiche le résultat, agréablement présenté (pretty-printed). C'est-à-dire que les structures de données imbriquées sont imprimées récursivement et avec les caractères invisibles convenablement encodés.

V
V PAQUETAGE
V PAQUETAGE VARS

  • Cette commande affiche toutes les variables (ou quelques-unes, lorsque vous spécifiez VARS) dans le PAQUETAGE spécifié (main par défaut) en utilisant une présentation agréable (pretty printer). Les hachages affichent leurs clefs et leurs valeurs, les caractères de contrôles sont présentés lisiblement, les structures de données imbriquées sont imprimées de manière lisible et ainsi de suite. Ceci est identique à un appel de la commande x sur chaque variable possible, sauf que x fonctionne également avec les variables lexicales. De plus, vous pouvez tapez ici les identificateurs sans spécificateur de type, tel que $ ou @, comme ceci :
 
Sélectionnez
V Animal::Chameau SPOT FIDO
  • Au lieu d'un nom de variable dans VARS, vous pouvez utiliser ~MOTIF ou !MOTIF pour imprimer les variables existantes dont le nom correspond ou ne correspond pas au MOTIF spécifié.

X
X VARS

  • Cette commande est identique à V PAQUETAGE_COURANT, où PAQUETAGE_COURANT est le paquetage où la ligne courante a été compilée.

H

H -NUMÉRO

  • Cette commande affiche la dernière commande NUMÉRO. Seules les commandes de plusieurs caractères sont stockées dans l'historique. (Sinon, la plupart d'entre elles seraient des s ou des n.) Si NUMÉRO est omis, toutes les commandes sont listées.
20-2-e. Localisation de code

À l'intérieur du débogueur, vous pouvez extraire et afficher des parties de votre programme avec ces commandes.

l
l LIGNE
l NOM_SOUS_PROGRAMME
l MIN+INCR
l MIN-MAX

  • La commande l liste quelques-unes des prochaines lignes de votre programme ou la LIGNE spécifiée, si elle est fournie ou quelques lignes au début du sous-programme ou de la référence de code NOM_SOUS_PROGRAMME.
  • La forme l MIN+INCR liste INCR+1 lignes, en commençant à MIN. La forme l MIN-MAX liste les lignes MIN jusqu'à MAX.

-

  • Cette commande liste quelques lignes au début de votre programme.

w
w LIGNE

  • Liste une fenêtre (quelques lignes) autour de la LIGNE de code source donnée ou de la ligne courante, si aucune LIGNE n'est fournie.

f NOM_FICHIER

  • Cette commande vous permet de visualiser un programme ou une instruction eval différent. Si le NOM_FICHIER n'est pas un nom de chemin absolu tel qu'on l'aurait trouvé dans les valeurs de %INC, il est interprété en tant qu'expression régulière pour trouver le nom de chemin que vous voulez.

/MOTIF/

  • Cette commande recherche le MOTIF en avant dans votre programme ; le / final est optionnel. Le MOTIF entier est également optionnel et s'il est omis, la recherche précédente est répétée.

?MOTIF?

  • Cette commande recherche le MOTIF en arrière dans votre programme ; le / final est optionnel. La recherche précédente est répétée si le MOTIF est omis.

S
S MOTIF
S!MOTIF

  • La commande S liste les sous-programmes dont le nom correspond (ou avec !, ne correspond pas) au MOTIF. Si aucun MOTIF n'est fourni, tous les sous-programmes sont listés.
20-2-f. Actions et exécution de commandes

Depuis l'intérieur du débogueur, vous pouvez spécifier des actions à accomplir à certains moments. Vous pouvez également lancer des programmes externes.

a
a COMMANDE
a LIGNE
a LIGNE COMMANDE

  • Cette commande positionne une action à accomplir avant d'exécuter la LIGNE ou la ligne courante si LIGNE est omise. Par exemple, ceci affiche $truc chaque fois que la ligne 53 est atteinte :
 
Sélectionnez
a 53 print "DB a trouvé $truc\n"
  • Si aucune COMMANDE n'est spécifiée, l'action à la LIGNE spécifiée est supprimée. Sans LIGNE, ni ACTION, l'action sur la ligne courante est supprimée.

A

  • La commande A du débogueur supprime toutes les actions.

<
<?
< EXPR
<< EXPR

  • La forme < EXPR spécifie une expression Perl à évaluer avant toute invite du débogueur. Vous pouvez ajouter une autre expression avec la forme << EXPR, les lister avec <? et toutes les supprimer avec un simple <.

>
>?
> EXPR
>> EXPR

  • Les commandes > se comportent exactement comme leurs cousines, < mais sont exécutées après l'invite du débogueur plutôt qu'avant.

{
{?
{ COMMANDE
{{ COMMANDE

  • Les commandes { du débogueur se comportent exactement comme, < mais spécifient une commande du débogueur à exécuter plutôt qu'une expression Perl. Un avertissement est émis s'il apparaît que vous avez accidentellement entré un bloc de code à la place. Si c'est vraiment ce que vous voulez faire, écrivez-le avec ;{ ... } ou même do{ ... }.

!
! NUMÉRO
! -NUMÉRO
!MOTIF

  • Un ! isolé répète la commande précédente. Le NUMÉRO spécifie quelle commande exécuter depuis l'historique ; par exemple, !3 exécute la troisième commande tapée dans le débogueur. Si un signe moins précède le NUMÉRO, on compte les commandes à rebours : ! -2 exécute l'avant-dernière commande. Si un MOTIF (sans slash) est fourni à la place d'un NUMÉRO, la dernière commande commençant par ce MOTIF est exécutée. Voir également l'option du débogueur recallCommand.

!! CMD

  • Cette commande du débogueur lance la commande externe CMD dans un sous-processus, qui lira depuis DB::IN et écrira vers DB::OUT. Voir également l'option du débogueur shellBang. Cette commande emploie le shell cité dans $ENV{SHELL}, qui peut parfois interférer avec une interprétation correcte des informations concernant les statuts, les signaux et les core dump. Si vous voulez une valeur de sortie cohérente, positionnez $ENV{SHELL} à /bin/sh.

|
| CMD_DB
|| CMD_PERL

  • La commande |CMD_DB lance la commande CMD_DB du débogueur, en pipant DB::OUT vers $ENV{PAGER}. On emploie souvent ceci avec les commandes qui produiraient sinon une sortie trop longue, comme :
 
Sélectionnez
DB<1> |V main
  • Remarquez que ceci est valable pour les commandes du débogueur et non pour celles que vous auriez tapées depuis votre shell. Si vous vouliez envoyer la commande externe who vers votre pager, vous pourriez faire quelque chose comme ceci :
 
Sélectionnez
DB<1> !!who | more
  • La commande ||CMD_PERL est semblable à, |CMD_DB mais DB::OUT est également temporairement sélectionné (avec select), afin que toutes les commandes appelant print, printf ou write sans handle de fichier soient également envoyées vers le pipe. Par exemple, si vous aviez une fonction générant des tonnes d'affichages en appelant print, vous utiliseriez cette commande à la place de la précédente pour envoyer la sortie vers le pager :
 
Sélectionnez
DB<1> sub qui { print "Utilisateurs : ", `who` } DB<2> ||qui()
20-2-g. Commandes diverses

q et ^D

  • Ces commandes quittent le débogueur. Il s'agit de la manière recommandée de sortir du débogueur, bien que taper deux fois exit fonctionne parfois. Positionnez l'option inhibit_exit à 0 si vous voulez être capable de passer directement à la fin du programme et de tout de même rester dans le débogueur. Vous pouvez également avoir besoin de positionner $DB::finished à 0 si vous voulez suivre la destruction globale.

R

  • Redémarre le débogueur en lançant une nouvelle session avec exec. Le débogueur essaie de maintenir votre historique entre les sessions, mais quelques réglages internes et quelques options de la ligne de commande peuvent être perdus. Les réglages suivant sont actuellement préservés : l'historique, les points d'arrêt, les actions, les options du débogueur et les options -w, -I et -e de la ligne de commande de Perl.

=
= ALIAS
= ALIAS VALEUR

  • Cette commande affiche la valeur actuelle d'ALIAS si aucune VALEUR n'est fournie. Avec une VALEUR, elle définit une nouvelle commande du débogueur avec le nom ALIAS. Si ALIAS et VALEUR sont tous deux omis, tous les alias actuels sont listés. Par exemple :
 
Sélectionnez
= quit q
  • Un ALIAS devrait être un simple identificateur et devait également se convertir en un simple identificateur. Vous pouvez obtenir des mécanismes d'alias plus sophistiqués en ajoutant directement vos propres entrées à %DB::aliases. Voir Personnalisation du débogueur plus loin dans ce chapitre.

man
man PAGE_DE_MANUEL

  • Cette commande appelle la visionneuse de documentation par défaut de votre système pour la page donnée ou la visionneuse elle-même si PAGE_DE_MANUEL est omise. Si cette visionneuse est man, les informations courantes de %Config sont utilisées pour l'invoquer. Le préfixe « perl » sera automatiquement fourni à votre place si nécessaire ; ceci vous permet de taper man debug et man op depuis le débogueur.
  • Sur les systèmes ne disposant pas d'ordinaire de l'utilitaire man, le débogueur invoque perldoc ; si vous voulez changer ce comportement, positionnez $DB::doccmd avec la visionneuse que vous désirez. Ceci peut être initialisé dans un fichier rc ou par l'intermédiaire d'une affectation directe.

O
O OPTION ...
O OPTION? ...
O OPTION=VALEUR ...

  • La commande O vous permet de manipuler les options du débogueur, qui sont énumérées au paragraphe « Options du débogueur » plus loin dans ce chapitre. La form O OPTION positionne chacune des options listées à 1. Si un point d'interrogation suit une OPTION, sa valeur actuelle est affichée.
  • La forme O OPTION=VALEUR positionne la valeur ; si VALEUR contient des espaces, ils doivent être isolés (quoted). Par exemple, vous pouvez positionner O pager="less -MQeicsNfr" pour utiliser less avec ces drapeaux spécifiques. Vous pouvez utiliser des apostrophes ou des guillemets, mais si c'est le cas, vous devez isoler des instances imbriquées avec un type de caractère de protection identique à celui avec lequel vous avez commencé. Vous devez également protéger tout antislash précédant immédiatement l'apostrophe ou le guillemet, mais n'étant pas destiné à protéger l'apostrophe ou le guillemet lui-même. En d'autres termes, vous n'avez qu'à suivre les règles de l'isolement par apostrophes sans tenir compte du type de caractère d'isolement réellement utilisé. Le débogueur répond en vous montrant la valeur de l'option qui vient d'être positionnée, en utilisant toujours la notation avec des apostrophes dans l'affichage :
 
Sélectionnez
DB<1> O OPTION='ceci n\'est pas mal'
             OPTION = 'ceci n\'est pas mal'
DB<2> O OPTION="\"N'est-ce pas ?\", dit-elle"
             OPTION = '"N\'est-ce pas ?", dit-elle'
  • Pour des raisons historiques, le =VALEUR est optionnel, mais vaut 1 par défaut, seulement là où cela peut être fait en toute sécurité — c'est-à-dire surtout pour les options booléennes. Il vaut mieux affecter une VALEUR spécifique en utilisant =. L' OPTION peut être abrégée, mais à moins que vous ne soyez volontairement occulte, il vaudrait certainement mieux vous abstenir. Plusieurs options peuvent être positionnées ensemble. Voir la section Options du débogueur pour en avoir la liste.

20-3. Personnalisation du débogueur

Le débogueur contient probablement assez de points d'entrée pour sa configuration, pour que vous n'ayez jamais à le modifier vous-même. Vous pouvez changer le comportement du débogueur depuis l'intérieur de ce dernier en utilisant sa commande O, depuis la ligne de commande via la variable d'environnement PERLDB_OPTS et en lançant l'une des commandes prédéfinies stockées dans des fichiers rc.

20-3-a. Implémentation de l'éditeur pour le débogage

Le mécanisme d'historique de la ligne de commande du débogueur n'offre pas de facilités d'édition pour la ligne de commande comme le font de nombreux shells : vous ne pouvez pas accéder aux lignes précédentes avec ^p, ni vous déplacer au début de la ligne avec ^a, bien que vous puissiez exécuter les lignes précédentes en utilisant la syntaxe avec un point d'exclamation, familière aux utilisateurs du shell. Toutefois, si vous installez les modules Term::Readkey et Term::ReadLine de CPAN, vous obtiendrez des fonctionnalités d'édition complètes, similaires à ce qu'offre GNU readline(3).

Si emacs est installé sur votre système, il peut interagir avec le débogueur Perl pour fournir un environnement intégré de développement logiciel, avec des réminiscences de ses interactions avec les débogueurs C. Perl est distribué avec un fichier de démarrage pour pousser emacs à agir comme un éditeur sensible à la syntaxe qui comprenne la syntaxe (du moins, une partie) de Perl. Regardez dans le répertoire emacs du code source de la distribution Perl. Les utilisateurs de vi devraient également regarder vim (et gvim, la version fenêtre et souris) pour obtenir une coloration des mots-clefs de Perl.

Une configuration similaire, faite par l'un des auteurs (Tom), pour interagir avec le vi de n'importe quel fabricant et le système de fenêtrage X11, est également disponible. Ceci fonctionne comme l'implémentation intégrée multifenêtre fournie par emacs, où le débogueur dirige l'éditeur. Toutefois, à l'heure où ces lignes sont écrites, son emplacement éventuel dans la distribution de Perl reste incertain. Mais nous avons pensé que vous deviez connaître cette possibilité.

20-3-b. Personnalisation avec des fichiers d'initialisation

Vous pouvez effectuer certaines personnalisations en configurant un fichier .perldb ou perldb.ini (selon votre système d'exploitation), contenant du code d'initialisation. Ce fichier d'initialisation contient du code Perl, et non des commandes du débogueur, et il est traité avant de regarder la variable d'environnement PERLDB_OPTS. Par exemple, vous pourriez construire des alias en ajoutant des entrées dans le hachage %DB::alias, de cette manière :

 
Sélectionnez
$alias{longueur} = 's/^longueur(.*)/p length($1)/';
$alias{stop}     = 's/^stop (at|in)/b/';
$alias{ps}       = 's/^quit(\s*)/exit/';
$alias{aide}     = 's/^ps\b/p scalar /';
$alias{quit}     = 's/^aide\s*$/|h/';

Vous pouvez changer les options depuis votre fichier d'initialisation en utilisant les appels de fonctions dans l'API interne du débogueur :

 
Sélectionnez
parse_options("NonStop=1 LineInfo=db.out AutoTrace=1 frame=2");

Si votre fichier d'initialisation définit la routine afterinit, cette fonction est appelée après la fin de l'initialisation du débogueur. Le fichier d'initialisation peut se trouver dans le répertoire courant ou dans le répertoire maison. Comme ce fichier contient du code Perl arbitraire, il doit appartenir au super-utilisateur ou à l'utilisateur courant et n'être modifiable que par son propriétaire, pour des raisons de sécurité.

Si vous voulez modifier le débogueur, copiez perl5db.pl depuis la bibliothèque Perl vers un autre nom et bidouillez-le selon votre inspiration. Vous voudrez alors peut-être positionner votre variable d'environnement PERL5DB pour écrire quelque chose comme ceci :

 
Sélectionnez
BEGIN { require "mon_perl5db.plx" }

En dernier ressort, vous pourriez également utiliser PERL5DB pour personnaliser le débogueur en positionnant directement les variables internes ou en appelant directement les fonctions internes du débogueur. Toutefois, soyez bien conscient que toutes les variables et les fonctions qui ne sont documentées ni ici, ni dans les pages de manuels en ligne perldebug, perldebguts ou DB, sont considérées comme étant réservées à un usage interne et sont susceptibles d'être modifiées sans préavis.

20-3-c. Options du débogueur

Le débogueur possède de nombreuses options que vous pouvez positionner avec la commande O, soit interactivement, soit depuis l'environnement, soit depuis un fichier d'initialisation.

recallCommand, ShellBang

  • Les caractères utilisés pour rappeler une commande ou pour engendrer un shell. Par défaut, les deux options sont positionnées à !.

pager

  • Le programme utilisé pour afficher les commandes envoyées sur un pager (celles commençant par un caractère |). Par défaut, $ENV{PAGER} sera utilisé. Comme le débogueur utilise les caractéristiques de votre terminal courant pour les caractères gras et le soulignement, si le pager choisi ne transmet pas sans changements les séquences d'échappement, l'affichage de certaines commandes du débogueur ne sera plus lisible après leur passage dans le pager.

tkRunning

  • Exécute sous le module Tk lors de l'invite (avec ReadLine).

signalLevel, warnLevel, dieLevel

  • Positionne le niveau de verbosité. Par défaut, le débogueur laisse tranquilles vos exceptions et vos avertissements, car leur altération peut empêcher le bon fonctionnement des programmes.
  • Pour désactiver ce mode sécurisé par défaut, placez ces valeurs à une valeur supérieure à 0. À un niveau de 1, vous obtenez une trace arrière pour tous les types d'avertissements (c'est souvent gênant) ou toutes les exceptions (c'est souvent précieux). Malheureusement, le débogueur ne peut pas discerner les exceptions fatales et non fatales. Si dieLevel vaut 1, alors vos exceptions non fatales sont aussi tracées et altérées sans cérémonie si elles proviennent de chaînes évaluées (avec eval) ou d'un eval quelconque à l'intérieur des modules que vous essayez de charger. Si dieLevel est à 2, le débogueur ne se soucie pas de leur provenance : il usurpe vos gestionnaires d'exceptions et affiche une trace, puis modifie toutes les exceptions avec ses propres embellissements. Ceci peut être utile dans une optique de traçage, mais tend à désespérément semer la confusion dans tout programme prenant au sérieux sa gestion des exceptions.
  • Le débogueur essayera d'afficher un message lorsque des signaux INT, BUS ou SEGV arriveront sans être traités. Mais si vous vous trouvez dans un appel système lent (comme un wait, un accept ou un read depuis le clavier ou depuis une socket) et si vous n'avez pas positionné votre propre gestionnaire pour $SIG{INT}, vous ne serez alors pas capable de faire un Ctrl-C pour revenir dans le débogueur, car le propre gestionnaire du débogueur pour $SIG{INT} ne comprend pas qu'il doit lever une exception et sortir des appels système lents par un longjmp(3).

AutoTrace

  • Positionne le mode trace (similaire à la commande t, mais peut être mise dans PERLDB_OPTS).

LineInfo

  • Affecte le fichier ou le pipe dans lequel écrire les informations sur les numéros de ligne. S'il s'agit d'un pipe (mettons, |visual_perl_db), alors un message court est utilisé. C'est le mécanisme mis en œuvre pour interagir avec un éditeur esclave ou un débogueur visuel, comme les points d'entrées spéciaux de vi ou d'emacs ou le débogueur graphique ddd.

inhibit_exit

  • Si cette option vaut 0, elle permet le passage direct à la fin du script.

PrintRet

  • Affiche la valeur de retour après la commande r si cette option est activée (ce qui est le cas par défaut).

ornaments

  • Affecte l'apparence à l'écran de la ligne de commande (voir les documentations en ligne de Term::ReadLine). Il n'y a actuellement aucun moyen de désactiver les ornements, ce qui peut rendre certains affichages illisibles sur certains écrans ou avec certains pagers. Ceci est considéré comme un bogue.

frame

  • Affecte l'affichage des messages à l'entrée et à la sortie des sous-programmes. Si frame & 2 est faux, les messages ne sont affichés qu'à l'entrée. (L'affichage de messages à la sortie pourrait s'avérer utile s'ils sont entrecroisés avec d'autres messages).
  • Si frame & 4 est vrai, les arguments des fonctions sont affichés en plus d'informations sur le contexte et sur l'appelant. Si frame & 8 est vrai, les FETCH surchargés, chaînifiés (stringify) et liés (avec tie) sont activés sur les arguments affichés. Si frame & 16 est vrai, la valeur de retour du sous-programme est affichée.
  • La longueur à partir de laquelle la liste d'arguments est tronquée est régie par l'option suivante.

maxTraceLen

  • La longueur à laquelle la liste d'arguments est tronquée lorsque le bit 4 de l'option frame est positionné.

Les options suivantes affectent ce qui se produit avec les commandes V, X, et x :

arrayDepth, hashDepth

  • Affiche seulement les n premiers éléments. Si n est omis, tous les éléments seront affichés.

compactDump, veryCompact

  • Change le style de sortie des tableaux et des hachages. Si compactDump est activé, les tableaux courts peuvent être affichés sur une seule ligne.

globPrint

  • Afficher le contenu des typeglobs.

DumpDBFiles

  • Affiche les tableaux contenant les fichiers débogués.

DumpPackages

  • Affiche les tables de symboles des paquetages.

DumpReused

  • Affiche le contenu des adresses « réutilisées ».

quote, HighBit, undefPrint

  • Change le style d'affichage des chaînes. La valeur par défaut de quote est auto ; vous pouvez activer le format avec des guillemets ou avec des apostrophes en la positionnant à " ou ' , respectivement. Par défaut, les caractères dont le bit de poids fort est positionné sont affichés tels quels.

UsageOnly

  • Au lieu de montrer le contenu des variables de paquetage, vous obtenez lorsque cette option est activée, un affichage rudimentaire de l'usage de la mémoire par paquetage, basé sur la taille totale des chaînes trouvées dans les variables du paquetage. Puisque la table de symboles du paquetage est utilisée, les variables lexicales sont ignorées.
20-3-d. Exécution autonome

Pendant le démarrage, les options sont initialisées à partir de $ENV{PERLDB_OPTS}. Vous pouvez y placer les options d'initialisation TTY, noTTY, ReadLine et NonStop.

Si votre fichier d'initialisation contient :

 
Sélectionnez
parse_options("NonStop=1 LineInfo=tperl.out AutoTrace");

alors votre script s'exécutera sans intervention humaine, plaçant les informations de trace dans le fichier tperl.out. (Si vous l'interrompez, vous avez intérêt à réinitialiser LineInfo à /dev/tty si vous voulez voir quelque chose.)

Les options suivantes peuvent être spécifiées uniquement au démarrage. Pour les positionner dans votre fichier d'initialisation, appelez parse_options("OPT=VAL").

TTY

  • Le terminal à utiliser pour les entrées/sortie de débogage.

noTTY

  • Si cette option est positionnée, le débogueur entre en mode NonStop et ne se connectera pas à un terminal. En cas d'interruption (ou si le contrôle passe au débogueur via un réglage explicite de $DB::signal ou de $DB::single depuis le programme Perl), il se connecte au terminal spécifié par l'option TTY au démarrage ou à un terminal trouvé à l'exécution en utilisant le module Term::Rendezvous de votre choix.
  • Ce module devrait implémenter une méthode appelée new, renvoyant un objet contenant deux méthodes : IN et OUT. Celles-ci devraient renvoyer deux handles de fichiers à utiliser que le débogueur utiliserait comme son entrée et sa sortie, respectivement. La méthode new devrait inspecter un argument contenant la valeur de $ENV{PERLDB_NOTTY} au démarrage ou de "/tmp/perldbtty$$" autrement. On n'inspecte pas l'appartenance appropriée ni l'accès grand ouvert en écriture de ce fichier, des risques de sécurité sont donc théoriquement possibles.

ReadLine

  • Si cette option est fausse, l'implémentation de ReadLine dans le débogueur est désactivée de façon à pouvoir déboguer les applications utilisant elles-mêmes le module ReadLine.

NonStop

  • Si cette option est mise, le débogueur entre en mode non interactif jusqu'à ce qu'il soit interrompu ou que votre programme positionne $DB::signal ou $DB::single.

Les options peuvent parfois être abrégées de manière unique par leur initiale, mais nous vous recommandons de toujours les épeler en entier, pour la lisibilité et la compatibilité future.

Voici un exemple de l'utilisation de la variable d'environnement $ENV{PERLDB_OPTS} pour positionner les options automatiquement.(159) Il lance votre programme de façon non interactive, affichant les infos à chaque entrée dans un sous-programme et pour chaque ligne exécutée. La sortie de la trace du débogueur est placée dans le fichier tperl.out. Ceci permet à votre programme de continuer à utiliser son entrée et sa sortie standards, sans que les informations de trace se mettent sur son chemin.

 
Sélectionnez
$ PERLDB_OPTS="NonStop frame=1 AutoTrace LineInfo=tperl.out" perl -d mon_prog

Si vous interrompez le programme, vous aurez besoin de réinitialiser rapidement O LineInfo=/dev/tty ou à autre chose de sensé sur votre plate-forme. Sinon, vous ne verrez plus l'invite du débogueur.

20-4. Implémentation du débogueur

Perl fournit des points d'entrée spéciaux pour le débogage, à la fois à la compilation et à l'exécution pour créer des environnements de débogage, tels que le débogueur standard. Ces points d'entrées ne doivent pas être confondus avec les options perl -D, qui ne sont utilisables que si votre version de Perl a été compilée avec l'implémentation DDEBUGGING.

Par exemple, à chaque fois que vous appelez la fonction interne de Perl caller, depuis le paquetage DB, les arguments avec lesquels l'enregistrement d'activation correspondant avait été appelé, sont copiés dans le tableau @DB::args. Lorsque vous invoquez Perl avec l'option -d, les fonctionnalités supplémentaires suivantes sont activées :

  • Perl insert le contenu de $ENV{PERL5DB} (ou, si cette variable est absente, BEGIN {require 'perl5db.pl'}) avant la première ligne de votre programme.
  • Le tableau @{"_<$nom_fichier"} contient les lignes de $nom_fichier pour tous les fichiers compilés par Perl. Il en va de même pour les chaînes évaluées (avec eval) qui contiennent des sous-programmes ou sont actuellement en cours d'exécution. Le $nom_fichier pour les chaînes évaluées ressemble à (eval 34). Les affectations de code dans les expressions régulières ressemblent à (re_eval 19).
  • Le hachage %{"_<$nom_fichier"} contient les points d'arrêt et les actions avec le numéro de la ligne comme clef. Vous pouvez positionner des entrées individuelles, par opposition au hachage complet. Perl ne se soucie ici que de la vérité booléenne, bien que les valeurs utilisées par perl5db.pl se présentent sous la forme "$condition_d_arret\0$action". Les valeurs dans ce hachage sont magiques dans un contexte numérique : elles valent zéro si l'on ne peut pas placer de point d'arrêt sur la ligne.
  • Le même hachage renferme les chaînes évaluées qui contiennent des sous-programmes ou sont actuellement en train d'être exécutées. Le $nom_fichier pour les chaînes évaluées ressemble à (eval 34) ou (re_eval 19).
  • Le scalaire ${"_<$nom_fichier} contient "_<$nom_fichier". Ceci est également le cas pour les chaînes évaluées qui contiennent des sous-programmes ou sont actuellement en cours d'exécution. Le $nom_fichier pour les chaînes évaluées ressemble à (eval 34) ou (re_eval 19).
  • Après que chaque fichier chargé par require est compilé, mais avant qu'il soit exécuté, DB::postponed(*{"_<$nom_fichier"}) est appelé si le sous-programme DB::postponed existe. Ici, le $nom_fichier est le nom développé du fichier chargé par require, tel qu'on le trouve dans les valeurs de %INC.
  • Après que chaque sous-programme nom_sous_programme est compilé, l'existence de $DB::postponed{nom_sous_programme} est vérifiée. Si cette clef existe, DB::postponed(nom_sous_programme) est appelé si le sous-programme DB::postponed existe également.
  • Un hachage %DB::sub est maintenu, dont les clefs sont les noms des sous-programmes et dont les valeurs se présentent sous la forme nom_fichier:ligne_debutligne_fin. nom_fichier se présente sous la forme (eval 34) pour les sous-programmes définis à l'intérieur de chaînes évaluées ou (re_eval 19) pour ceux définis à l'intérieur d'affectations de code dans les expressions régulières.
  • Lorsque l'exécution de votre programme atteint un certain point qui pourrait contenir un point d'arrêt, le sous-programme DB::DB() est appelé si l'une des variables $DB::trace, $DB::single ou $DB::signal est vraie. Ces variables ne peuvent être affectées par local. Cette fonctionnalité est désactivée lorsque l'exécution se déroule à l'intérieur de DB::DB(), y compris les fonctions appelées depuis là, sauf si $^D & (1< <30) est vrai.
  • Lorsque l'exécution du programme atteint un appel de sous-programme, un appel à &DB::sub(args) est effectué à la place, avec $DB::sub contenant le nom du sous-programme appelé. Ceci ne se produit pas si le sous-programme avait été compilé dans le paquetage DB.

Remarquez que si &DB::sub a besoin de données externes pour fonctionner, aucun appel de sous-programme n'est possible jusqu'à ce que ceci soit accompli. Pour le débogueur standard, la variable $DB::deep (à combien de niveaux de profondeur de récursion pouvez-vous aller avant d'arrêter obligatoirement) donne un exemple d'une telle dépendance.

20-4-a. Écriture de son propre débogueur

Un débogueur minimal en état de fonctionner consiste en une ligne :

 
Sélectionnez
sub DB::DB {}

qui, puisqu'il ne fait rien de particulier, peut facilement être défini via la variable d'environnement PERL5DB :

 
Sélectionnez
% PERL5DB="sub DB::DB {}" perl -d votre_programme

Un autre débogueur minuscule, légèrement plus utile, pourrait être créé comme ceci :

 
Sélectionnez
sub DB::DB {print ++$i; scalar <STDIN>}

Ce petit débogueur imprimerait le numéro de ligne séquentiel à chaque instruction rencontrée et attendrait que vous entriez un retour à la ligne avant de continuer.

Le débogueur suivant, bien qu'il paraisse petit, est vraiment tout à fait fonctionnel :

 
Sélectionnez
{
    package DB;
    sub DB {}
    sub sub {print ++$i, " $sub\n"; &$sub}
}

Il affiche le numéro séquentiel de l'appel de sous-programme et le nom du sous-programme appelé. Remarquez que &DB::sub doit être compilé dans le paquetage DB, tel que nous l'avons fait ici.

Si vous basez votre nouveau débogueur sur le débogueur courant, il y a quelques points d'entrée qui peuvent vous aider à le personnaliser. Au démarrage, le débogueur lit votre fichier d'initialisation dans le répertoire courant ou dans votre répertoire maison. Après que le fichier est lu, le débogueur lit la variable d'environnement PERLDB_OPTS et parcourt celle-ci comme le reliquat d'une ligne O ..., telle que vous l'auriez entrée à l'invite du débogueur.

Le débogueur maintient également des variables internes magiques, comme @DB::dbline et %DB::dbline, qui sont des alias pour @{"::_< et %{"::_<. Ici, fichier_courant est le fichier actuellement sélectionné, soit choisi explicitement avec la commande f du débogueur, soit implicitement de par le flux d'exécution.

Certaines fonctions peuvent faciliter la personnalisation. DB::parse_options(CHAÎNE) analyse une ligne comme l'option O. DB::dump_trace(SAUT[, saute le nombre spécifié d'enregistrements d'activation dans la pile (stack frames) et renvoie une liste contenant des informations à propos des enregistrements d'activation appelants (tous, si COMPTEUR est omis). Chaque entrée est une référence vers un hachage avec les clefs « context » (., $ ou @), « sub » (nom du sous-programme ou informations sur un eval), « args » (undef ou une référence vers un tableau), « file » (nom du fichier) et « line » (numéro de ligne). DB::print_trace(HF, SAUT[, COMPTEUR[, COURT]]) imprime des informations formatées, concernant les enregistrements d'activation appelants de la pile, dans le handle de fichier donné. Les deux dernières fonctions peuvent s'avérer pratiques en tant qu'arguments des commandes < et << du débogueur.

Vous n'avez pas besoin d'apprendre tout cela — la plupart d'entre nous ne l'ont pas fait. En fait, lorsque nous avons besoin de déboguer un programme, nous ne faisons généralement qu'insérer quelques instructions print ici et là avant de relancer le programme.

Dans des jours meilleurs, nous nous rappellerons qu'il faut activer les avertissements en premier. Cela fait souvent disparaître le problème, en évitant de nous arracher les cheveux (pour ce qu'il en reste). Mais lorsque cela ne fonctionne pas, il est bon de savoir, en vous attendant patiemment derrière cette bonne vieille option -d, qu'il existe un adorable petit débogueur qui peut raccommoder n'importe quoi sauf trouver les bogues à votre place.

Mais si vous devez vous rappeler une seule chose à propos de la personnalisation du débogueur, c'est peut-être celle-ci : ne restreignez pas la notion de bogue aux choses qui rendent Perl malheureux. Il s'agit également d'un bogue si votre programme vous rend malheureux. Plus tôt, nous vous avons montré quelques débogueurs personnalisés très simples. Dans le prochain paragraphe, nous allons vous montrer un exemple d'une différente sorte de débogueur personnalisé, une sorte qui peut (ou peut ne pas) vous aider à déboguer le bogue connu sous le nom de « Est-ce que ce truc va s'arrêter un jour ? ».

20-5. Le profileur Perl

Voulez-vous rendre votre programme plus rapide ? La réponse est oui, bien entendu. Mais vous devriez d'abord vous arrêter et vous demander, « Ai-je vraiment besoin de passer du temps à rendre ce programme plus rapide ? » L'optimisation récréative peut être amusante(160), mais normalement, vous avez mieux à faire de votre temps. Parfois, vous n'avez besoin que de planifier votre travail et de lancer le programme pendant que vous allez boire un café. (Ou vous prenez ceci comme excuse pour en boire un.) Mais si votre programme a absolument besoin de tourner plus rapidement, vous devriez commencer par le profiler. Un profileur peut vous dire quelles parties de votre programme prennent le plus de temps à s'exécuter, afin que vous ne perdiez pas de temps à optimiser un sous-programme dont les effets sont insignifiants par rapport au temps d'exécution total.

Perl est livré avec un profileur, le module Devel::DProf. Vous pouvez l'utiliser pour profiler le programme Perl de mon_code.plx en tapant :

 
Sélectionnez
perl -d:DProf mon_code.plx

Même si nous l'avons appelé un profileur — puisque c'est ce qu'il fait — le mécanisme employé par Dprof est exactement le même que celui dont nous avons parlé auparavant dans ce chapitre. DProf n'est qu'un débogueur qui enregistre le temps entre le moment où Perl entre dans chaque sous-programme et celui où il en sort.

Lorsque votre script profilé se terminera, Dprof déversera les informations mesurant le temps dans un fichier nommé tmon.out. Le programme dprofpp, livré avec Perl, sait comment analyser tmon.out et produit un rapport. Vous pouvez également utiliser dprofpp comme frontal pour le processus complet avec l'option -p(voir la description plus loin).

Étant donné ce programme :

 
Sélectionnez
Exterieur();

sub exterieur {
    for (my $i=0; $i < 100; $i++) { interieur() }
}

sub interieur {
    my $total = 0;
    for (my $i=0; $i < 1000; $i++) { $total += $i }
}

interieur();

la sortie de dprofpp est :

 
Sélectionnez
Total Elapsed Time = 0.158572 Seconds
User+System Time = 0.158572 Seconds
Exclusive Times
%Time ExclSec CumulS #Calls sec/call Csec/c Name
 88.2   0.140 0.140 101   0.0014 0.0014 main::interieur
 0.00   0.000 0.139   1   0.0000 0.1393 main::exterieur

Remarquez que la somme des pourcentages n'est pas égale à 100. En fait, dans ce cas, elle en est assez loin, ce qui devrait être un signe comme quoi vous avez besoin de lancer le programme plus longtemps. En règle générale, plus vous pouvez collecter de données de profilage, meilleur sera votre échantillon statistique. Si nous augmentons la boucle extérieure pour tourner 1000 fois au lieu de 100, nous obtiendrons des résultats plus justes :

 
Sélectionnez
Total Elapsed Time = 1.403988 Seconds
  User+System Time = 1.373988 Seconds
Exclusive Times
%Time ExclSec CumulS #Calls sec/call Csec/c Name
 98.2   1.350  1.348   1001   0.0013 0.0013 main::interieur
 0.58   0.008  1.357      1   0.0085 1.3570 main::exterieur

La première ligne rapporte combien de temps le programme a mis à s'exécuter, du début à la fin. La seconde ligne affiche la somme de deux nombres différents : le temps pris pour exécuter votre code (« user », utilisateur) et le temps pris par le système d'exploitation à exécuter les appels systèmes faits par votre code (« system », système). (Nous devrons admettre un peu de mauvaise précision dans ces nombres — l'horloge de l'ordinateur n'a certainement pas des tics d'un millionième de seconde. Elle peut avoir des tics d'un centième de seconde si vous avez de la chance.)

Les temps « user+system » peuvent être changés avec les options de la ligne de commande de dprofpp. -r affiche le temps écoulé, -s le temps système uniquement et -u le temps utilisateur seulement.

Le reste du rapport est une analyse du temps passé dans chaque sous-programme. La ligne « Exclusive Times » (Temps exclusifs) indique que lorsque le sous-programme exterieur a appelé le sous-programme interieur, le temps passé dans interieur n'a pas compté dans le calcul du temps de exterieur. Pour changer ceci et faire que le temps de interieur soit comptabilisé dans celui d'exterieur, donnez l'option -I à dprofpp.

Pour chaque sous-programme, les informations suivantes sont rapportées : %Time, le pourcentage de temps passé dans l'appel à ce sous-programme ; ExclSec, le temps en secondes passé dans ce sous-programmes, en n'incluant pas les sous-programmes appelés depuis celui-ci ; CumulS, le temps en secondes passé dans ce sous-programme et dans ceux appelés depuis celui-ci ; #Calls, le nombre d'appels du sous-programme ; sec/ call, le temps moyen en secondes de chaque appel au sous-programme, en n'incluant pas les sous-programmes appelés depuis celui-ci ; Csec/c, le temps moyen en secondes de chaque appel au sous-programme et de ceux appelés depuis celui-ci.

De toutes ces valeurs, la plus utile est %Time, qui vous dira où passe votre temps. Dans notre cas, le sous-programme interieur prend le plus de temps, nous devrions donc essayer de l'optimiser ou de trouver un algorithme qui l'appellera moins souvent. :-)

Les options de dprofpp fournissent un accès à d'autres informations ou modifient la manière dont les temps sont calculés. Vous pouvez également pousser dprofpp à lancer en premier lier le script à votre place, afin que vous n'ayez pas à vous souvenir de l'option -d:DProf :

-p SCRIPT

  • Dit à dprofpp qu'il devrait profiler le SCRIPT donné et ensuite interpréter ses données de profilage. Voir également l'option -Q.

-Q

  • Utilisée avec -p pour dire à dprofpp de sortir après avoir profilé le script, sans interpréter les données.

-a

  • Trie la sortie dans l'ordre alphabétique des noms de sous-programme plutôt que dans l'ordre décroissant des pourcentages de temps.

-R

  • Compte séparément les sous-programmes anonymes définis dans le même paquetage. Le comportement par défaut est de compter tous les sous-programmes anonymes en un seul, appelé main::__ANON__.
  • Affiche les temps des sous-programmes en incluant les temps des sous-programmes fils.

-l

  • Trie les sous-programmes par nombre d'appels. Ceci peut faciliter la sélection de candidats à l'inclusion par référence (inlining).

-O COMPTEUR

  • Montre seulement le top COMPTEUR des sous-programmes. La valeur par défaut est 15.

-q

  • N'affiche pas les en-têtes des colonnes.

-T

  • Affiche l'arbre d'appel des sous-programmes sur la sortie standard. Les statistiques des sous-programmes ne sont pas affichées. Une fonction appelée plusieurs fois (consécutivement) au même niveau d'appel, ne sera affichée qu'une seule fois, avec un compteur de répétition.

-S

  • Produit un affichage structuré par la manière dont vos sous-programmes s'appellent l'un, l'autre :
 
Sélectionnez
main::interieur x 1     0.00s
main::exterieur x 1     1.35s = (0.01 + 1.35)s
main::interieur x 1000  1.35s
  • Lisez ceci ainsi : le niveau supérieur de votre programme a appelé interieur une fois et pendant une durée de 0.00 s et le niveau supérieur a appelé exterieur une fois et il a tourné pendant 1.35 s inclusivement (0.01 s dans exterieur lui-même, 1.35 s dans les sous-programmes appelés depuis exterieur) en appelant interieur 1000 fois (ce qui a pris 1.35 s). Waouh, compris ? Les branches du même niveau (par exemple, interieur appelé une fois et exterieur appelé une fois) sont triées dans l'ordre des temps inclusifs.

-U

  • Ne trie pas. Affiche dans l'ordre trouvé dans le fichier de profilage tel quel.

-v

  • Trie dans l'ordre des temps moyens passés dans les sous-programmes à chaque appel. Ceci peut faciliter l'identification de candidats pour une optimisation à la main en incluant par référence (inlining) le corps des sous-programmes.

-g SOUS_PROGRAMME

  • Ignore les sous-programmes sauf SOUS_PROGRAMME et tous ceux qui sont appelés depuis celui-ci.

D'autres options sont décrites dans dprofpp(1), sa page de manuel standard.

DProf n'est pas votre seul choix de profileur. CPAN contient également Devel::Small-Prof, qui rapporte les temps passés dans chaque ligne de votre programme. Cela peut vous aider à vous faire une idée si vous utilisez une construction Perl particulière qui s'avère étonnamment coûteuse. La plupart des fonctions internes sont assez efficaces, mais il est facile d'écrire accidentellement une expression régulière dont le temps système (overhead) croît exponentiellement avec la taille de l'entrée. Voir également le paragraphe Efficacité au chapitre 24, Techniques couramment employées, pour d'autres astuces utiles.

Maintenant, allez boire un café. Vous en aurez besoin pour le prochain chapitre.

21. Mécanismes internes et accès externes

Comme nous l'avons expliqué au chapitre 18, Compilation, perl (le programme) contient à la fois un compilateur et un interpréteur pour les programmes écrits en Perl (le langage). Le compilateur/interpréteur Perl est lui-même écrit en C. Dans ce chapitre, nous allons esquisser la manière dont les programmes en C fonctionnent si l'on se place dans la perspective de quelqu'un voulant soit étendre, soit insérer Perl. Lorsque vous étendez Perl, vous mettez un bout de code C (appelé l'extension) sous le contrôle de Perl et lorsque vous insérez Perl, vous mettez un interpréteur Perl(161) sous le contrôle d'un programme C plus large.

La couverture succinte que nous faisons ici ne remplace en aucun cas les documentations en ligne sur les tripes de Perl : perlguts, perlxs, perlxstut, perlcall, perlapi et h2xs, toutes livrées avec Perl. Encore une fois, à moins que vous n'étendiez ou insériez Perl, vous n'aurez jamais besoin de connaître tout ceci.

En supposant que vous avez besoin de savoir, ce que vous devez savoir en premier est un peu des entrailles de Perl. Vous aurez également besoin de connaître C pour la plupart de ce qui suit. Vous aurez besoin d'un compilateur C pour lancer les exemples. Si votre but final est de créer un module dont les autres gens se serviront, ils auront également besoin d'un compilateur C. Plusieurs de ces exemples ne tourneront que sur les systèmes ressemblant à Unix. Oh, et ces informations sont susceptibles de changer dans les futures versions de Perl.

En d'autres termes, nous pénétrons en terra incognita, au risque de rencontrer des monstres marins.

21-1. Comment fonctionne Perl

Lorsqu'on alimente le compilateur Perl avec un programme Perl, la première tâche qu'il effectue est l'analyse lexicale : décomposer le programme en ses éléments syntaxiques de base (souvent appelés tokens). Si le programme est :

 
Sélectionnez
print "Salut, tout le monde !\n";

l'analyseur lexical le décompose en trois tokens : print, "Salut, tout le monde !\n" et le point-virgule final. La séquence de tokens est alors analysée syntaxiquement, en fixant les relations entre les tokens. En Perl, la frontière entre l'analyse lexicale et syntaxique est encore plus f loue que dans les autres langages. (C'est-à-dire, les autres langages informatiques. Si vous pensez à toutes les significations que new Bestiau peut prendre selon qu'il s'agit d'un paquetage Bestiau ou d'un sous-programme appelé new, vous comprendrez alors pourquoi. D'un autre côté, nous passons notre temps à lever les ambiguïtés de ce genre en français.)

Une fois qu'un programme a été analysé syntaxiquement et compris (vraisemblablement), il est compilé dans un arbre de codes d'opérations (opcodes), représentant les opérations de bas niveau et finalement, cet arbre d'opérations est exécuté — sauf si vous aviez invoqué Perl avec l'option -c (« contrôle de la syntaxe »), qui sort après avoir terminé la phase de compilation. C'est pendant la compilation, et non pendant l'exécution, que les blocs BEGIN, les blocs CHECK et les instructions use sont exécutés.

21-2. Types de données internes

Pendant que l'arbre de codes d'opérations, constituant un programme Perl, est exécuté, des valeurs Perl sont créées, manipulées et détruites. Les types de données avec lesquels vous êtes familier en Perl ont tous leur type de données correspondant dans le C que Perl a sous le capot et vous aurez besoin de connaître ces types lorsque vous passerez des données entre les deux langages.

Trois typedefs (définitions de type) C correspondent aux trois types de données de base de Perl : SV (valeur scalaire), AV (valeur tableau) et HV (valeur hachage). De plus, IV est un type simple entier signé dont on est sûr qu'il est assez grand pour contenir soit un pointeur, soit un entier ; et I32 et I16 sont des types dont on est sûr qu'ils sont assez grands pour contenir respectivement 32 et 16 bits. Pour stocker les versions non signées des ces trois derniers typedefs, il existe également UV, U32 et U16. Tous ces typedefs peuvent être manipulés avec les fonctions C décrites dans la documentation de perlguts. Nous esquissons le comportement de certaines de ces fonctions ci-dessous :

  • Il existe quatre types de valeurs pouvant être copiées dans un SV : une valeur entière (IV), un double (NV), une chaîne (PV) et un autre scalaire (SV). Il existe des douzaines de fonctions pour les SV, pour vous permettre de créer, de modifier, d'agrandir et de vérifier la vérité booléenne ou le fait que les scalaires Perl qu'ils représentent soient définis. Les références Perl sont implémentées en tant que RV, un type spécial de SV.
  • Lorsqu'un AV est créé, il peut être créé vide ou peuplé de SV, ce qui est sensé puisqu'un tableau est une collection de scalaires.
  • Le HV possède des fonctions C associées pour stocker, récupérer, supprimer et vérifier l'existence de paires clef/valeur dans le hachage que le HV représente.
  • Il existe également un GV (valeur glob), pouvant contenir des références vers n'importe laquelle des valeurs associées à un identificateur de variable : une valeur scalaire, un valeur tableau, une valeur hachage, un sous-programme, un handle d'entrée/sortie ou un format.

Lorsque vous étendez Perl, vous aurez parfois besoin de connaître ces valeurs lorsque vous créerez les liaisons vers les fonctions C. Lorsque vous insérez Perl, vous aurez besoin de connaître ces valeurs lorsque vous échangerez des données avec l'interpréteur Perl inclus dans votre programme C.

21-3. Étendre Perl (utiliser du C depuis du Perl)

Si vous voulez utiliser du code source C (ou une bibliothèque C) depuis Perl, vous devez créer une bibliothèque qui puisse être chargée dynamiquement ou avec laquelle vous puissiez faire une édition de liens statique avec l'exécutable perl. (On préfère d'ordinaire le chargement dynamique pour minimiser le nombre des différents exécutables perl qui prennent place en étant différents.) Vous créez cette bibliothèque en créant un fichier XS (avec un suffixe .xs) contenant une série de sous-programmes d'enveloppe (wrapper). Les sous-programmes d'enveloppe ne sont toutefois pas des sous-programmes Perl ; ils sont écrits en langage XS et nous appelons un tel sous-programme, une XSUB (pour « eXternal SUBroutine », routine externe). Une XSUB peut envelopper une fonction C depuis une bibliothèque externe, une fonction C ailleurs dans le fichier XS ou du code C brut dans la XSUB elle-même. Vous pouvez alors utiliser l'utilitaire xsubpp, livré avec Perl pour prendre le fichier XS et le traduire en code C qui puisse être compilé dans une bibliothèque que Perl comprendra.

En supposant que votre système d'exploitation implémente l'édition de liens dynamique, le résultat final sera un module Perl se comportant comme tout autre module écrit à 100 % en Perl pur, mais contenant sous le capot du code C compilé. Il fait ceci en enlevant des arguments depuis la pile d'arguments de Perl, en convertissant les valeurs Perl vers les formats attendus par une fonction C particulière (spécifiés à travers une déclaration de XSUB), en appelant la fonction C et en retransférant finalement les valeurs renvoyées par la fonction C vers Perl. Ces valeurs de retour peuvent être repassées à Perl soit en les mettant dans la pile de Perl, soit en modifiant les arguments fournis depuis Perl. (Si votre système n'implémente pas l'édition de liens dynamique, il existe un autre cerceau dans lequel vous devrez sauter et nous en parlerons dans le prochain paragraphe.)

La description précédente est en quelque sorte une vision simplifiée de ce qui se passe réellement. Puisque Perl autorise des conventions d'appels plus flexibles que C, les XSUB peuvent en faire bien plus en pratique, comme vérifier la validité des paramètres d'entrée, lever des exceptions, renvoyer undef ou une liste vide, appeler différentes fonctions C en se basant sur le nombre et le type des arguments ou fournir une interface orientée objet. Encore une fois, voir les pages de manuel de perlxs et perlxstut.

21-3-a. XS et XSUB

XS est un moyen pratique : il n'y a rien qui vous empêche d'écrire du code de glu directement en C et de le lier dans votre exécutable Perl. Toutefois, ceci serait pénible, particulièrement si vous devez écrire la glu pour de multiples fonctions C ou si vous n'êtes pas familier avec la discipline de la pile de Perl et autres mystères. XS vous permet d'écrire une description concise de ce qui devrait être fait par la glu et le compilateur XS, xsubpp, s'occupe du reste.

Pour les gens qui ne trouvent pas XS assez pratique, le système SWIG génère automatiquement des XSUB simples. Voir http ://www.swig.org pour plus d'informations.

Le langage XS vous permet de décrire la correspondance entre une fonction C et une fonction Perl. Il vous permet également décrire une fonction Perl qui soit une enveloppe autour de code C pur que vous écrivez vous-même. Lorsque XS est utilisé uniquement pour faire la correspondance entre C et Perl, la déclaration d'une XSUB est quasiment identique à la déclaration d'une fonction C. Dans de telles circonstances, un outil appelé h2xs (livré avec Perl) est capable de traduire un fichier d'en-tête C entier vers un fichier XS correspondant, fournissant la glu vers les fonctions C et les macros.

L'outil xsubpp crée les constructions nécessaires pour permettre à une XSUB de manipuler des valeurs Perl et la glu nécessaire pour permettre à Perl d'appeler la XSUB.

Un fichier XS commence par le code C que vous voulez inclure, ce qui n'est souvent rien de plus qu'un ensemble de directives #include. Après un mot-clef MODULE, le reste du fichier devrait être dans le « langage » XS, une combinaison de directives XS et de définitions de XSUB. Nous verrons bientôt l'exemple d'un fichier XS complet, mais en attendant, voici la définition d'une XSUB simple, permettant à un programme Perl d'accéder à une fonction de bibliothèque C, appelée sin(3). La XSUB spécifie le type renvoyé (un nombre en virgule flottante à double précision), le nom de la fonction et une liste d'arguments (avec un argument traduit x), ainsi que le type de l'argument (un autre double) :

 
Sélectionnez
double
sin(x)
    double x

Les XSUB plus compliquées contiendront souvent d'autres morceaux de code XS. Chaque section d'une XSUB commence par un mot-clef suivi d'un deux-points, tel que INIT: ou CLEANUP:. Cependant, les deux premières lignes d'une XSUB contiennent toujours les mêmes données : la description du type renvoyé et le nom de la fonction avec ses paramètres. Tout ce qui suit immédiatement ceci est considéré comme étant une section INPUT: sauf si on le marque explicitement avec un autre mot-clef. Les divers mots-clefs sont tous expliqués dans la page de manuel perlxs, que vous devriez lire pour apprendre tout ce que vous pouvez faire avec les XSUB.

Si votre système n'est pas capable de charger dynamiquement des bibliothèques partagées, vous pouvez toujours utiliser des XSUB, mais vous devez faire une édition de liens statique des XSUB avec le reste de Perl, créant ainsi un nouvel exécutable Perl (qui prend place en étant différent). Le mécanisme de construction d'une XSUB vérifiera le système et construira une bibliothèque partagée si possible, sinon une bibliothèque statique. Optionnellement, il peut construire un nouvel exécutable lié statiquement avec cette bibliothèque statique incluse. Mais vous pourriez vouloir attendre pour lier toutes vos nouvelles extensions dans un seul exécutable (qui prend place en étant le même, puisque c'est ce qu'il est.)

Si votre système est capable de lier dynamiquement les bibliothèques, mais que vous voulez toujours construire un exécutable lié statiquement, vous pouvez lancer make perl au lieu de make dans les exemples suivants. Vous devrez ensuite lancer make test_static au lieu de make test pour tester votre extension.

Le programme xsubpp a également besoin de savoir comment convertir des types de données Perl en types de données C. Il le devine souvent, mais avec des types définis par l'utilisateur, vous pouvez avoir besoin de l'aider en spécifiant la conversion dans un fichier typemap (littéralement : carte des types). Les conversions par défaut sont stockées dans CHEMIN-VERS-LA-BIBLIOTHÈQUE/ExtUtils/typemap.

Le fichier typemap est découpé en trois sections. La première, étiquetée TYPEMAP, dit au compilateur quels sont les fragments de code parmi ceux des deux sections suivantes devraient être utilisées pour faire la correspondance entre les types C et les valeurs Perl. La deuxième section, INPUT, contient du code C spécifiant comment les valeurs Perl devraient être converties en types C. La troisième section, OUTPUT, contient du code C spécifiant comment traduire les types C en valeurs Perl.

21-3-b. Création d'extensions

Une extension convenable consiste en plusieurs fichiers : l'un contenant le code XS, plus des fichiers implémentés qui aident Perl à deviner quoi faire avec le code XS. Vous pouvez créer tous ces fichiers à la main, mais il est plus facile d'utiliser l'outil h2xs, qui crée un squelette d'extension que vous pouvez ensuite compléter :

 
Sélectionnez
h2xs -A -n Montest

Ceci crée un répertoire nommé Montest, peut-être sous ext/ si ce répertoire existe dans le répertoire courant. Six fichiers seront créés dans le répertoire Montest : MANIFEST, Makefile.PL, Montest.pm, Montest.xs, test.pl et Changes. Nous décrivons les quatre premiers ci-dessous.

MANIFEST

  • Le fichier MANIFEST contient le nom de tous les fichiers qui viennent d'être créés dans le répertoire Montest. Si vous ajoutez d'autres fichiers dans votre extension et que vous la distribuez au monde entier, ajouter le nom des fichiers ici. Ceci est testé par certains systèmes pour s'assurer que votre distribution est complète.

Makefile.PL

  • Il s'agit d'un programme Perl générant un Makefile (qui est ensuite passé à make ou à un équivalent). Makefile.PL est décrit plus en détail dans « Créer des modules CPAN » au chapitre 22, CPAN .

Montest.pm

  • Les utilisateurs feront un use sur ce module lorsqu'ils voudront charger votre extension. Vous êtes supposés remplir les blancs du module squelette créé pour vous par h2x2 (162) :
 
Sélectionnez
package Montest;
use strict;
use warnings;

require Exporter;
require DynaLoader;

our @ISA = qw(Exporter DynaLoader);
# Éléments à exporter par défaut dans les espaces de noms des appelants.
# Remarque : N'exportez pas des noms par défaut sans une très bonne
# raison. Utilisez plutôt EXPORT_OK. N'exportez pas bêtement toutes vos
# fonctions/méthodes/constantes.
our @EXPORT = qw(

);
our $VERSION = '0.01';

bootstrap Montest $VERSION;

# Les méthodes préchargées viennent ici.

# Les méthodes autochargées viennent après __END__ et sont traitées par 
# le programme autosplit.

1;
__END__
# En-dessous se trouve une souche de documentation pour votre module.
# Vous feriez mieux de l'éditer !
  • La plupart des modules d'extension feront un require des extensions Exporter et DynaLoader. Après avoir alimenté @ISA (pour l'héritage) et @EXPORT (pour rendre les fonctions disponibles pour le paquetage utilisant le module), le code d'initialisation dit à Perl d'amorcer (bootstrap) le code XS. Perl lie alors dynamiquement la bibliothèque partagée dans le processus perl à l'exécution.

Montest.xs

  • Le fichier Montest.xs contient les XSUB disant à Perl comment passer les données aux routines C compilées. Initialement, Montest.xs ressemblera à quelque chose comme ceci :
 
Sélectionnez
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

MODULE = Montest  PACKAGE = Montest
  • Éditons le fichier XS en ajoutant ceci à la fin :
 
Sélectionnez
void
salut()   
    CODE:
        printf("Salut, tout le monde !\n");

Lorsque vous lancez perl Makefile.PL, le Makefile dont make a besoin est créé :

 
Sélectionnez
% perl Makefile.PL
Checking if your kit is complete...
Looks good
Writing Makefile for Montest

Le lancement de make produit maintenant un affichage qui ressemble à ceci (certaines lignes trop longues ont été raccourcies pour plus de clarté et certaines lignes accessoires ont été supprimées) :

 
Sélectionnez
% make
umask 0 && cp Montest.pm ./blib/Montest.pm
perl xsubpp -typemap typemap Montest.xs >Montest.tc && mv Montest.tc
Montest.c
cc -c Montest.c
Running Mkbootstrap for Montest ()
chmod 644 Montest.bs
LD_RUN_PATH="" ld -o ./blib/PA-RISC1.1/auto/Montest/Montest.sl -b Montest.o
chmod 755 ./blib/PA-RISC1.1/auto/Montest/Montest.sl
cp Montest.bs ./blib/PA-RISC1.1/auto/Montest/Montest.bs
chmod 644 ./blib/PA-RISC1.1/auto/Montest/Montest.bs
Manifying ./blib/man3/Montest.3

Nous supposons que le programme make que Perl utilise pour construire le programme est appelé make. Au lieu de lancer make dans ces exemples, il se peut que vous deviez substituer un autre programme make que Perl a été configuré pour utiliser. Vous pouvez trouver ce programme avec :

 
Sélectionnez
% perl -V:make

Le lancement de make a créé un répertoire nommé blib (pour « build library », bibliothèque de construction) dans le répertoire de travail courant. Ce répertoire contiendra la bibliothèque partagée que nous construirons. Une fois que nous sommes sûrs que nous savons ce que nous faisons, nous pouvons l'installer depuis ce répertoire vers son emplacement définitif. Jusqu'alors, nous devrons ajouter explicitement le répertoire blib au tableau @INC de Perl en utilisant le module ExtUtils::testlib. Si nous créons maintenant un fichier appelé salut, ressemblant à ceci :

 
Sélectionnez
use ExtUtils::testlib; # ajoute les répertoires blib/* à @INC
use Montest;
Montest::salut();

nous pouvons creuser un passage depuis Perl vers C :

 
Sélectionnez
% perl salut
Salut, le monde !

Une fois que notre extension est achevée et qu'elle a passé tous ses tests, nous pouvons l'installer avec make install.

Vous aurez besoin des permissions en écriture pour votre bibliothèque Perl. (Si ce n'est pas le cas, vous pouvez spécifier un autre répertoire comme indiqué dans Installation des modules de CPAN dans la bibliothèque Perl au chapitre 22.)

21-3-c. Entrées et sorties des XSUB

En poursuivant l'exemple précédent, nous ajouterons une seconde XSUB, prenant un seul argument numérique en entrée et renvoyant 1 si le nombre est pair et 0 s'il est impair :

 
Sélectionnez
int
est_pair(x)
         int x
    CODE:
         RETVAL = (x %2 == 0);
    OUTPUT:
         RETVAL

La liste des paramètres de sortie arrive tout à la fin de la fonction, juste après la directive OUTPUT:. L'utilisation de RETVAL indique à Perl que vous souhaitez renvoyer cette valeur comme valeur de retour de la XSUB. Si nous avions voulu que la fonction modifie son paramètre d'entrée, nous aurions utilisé x à la place de RETVAL.

Nous pouvons construire à nouveau notre nouvelle bibliothèque partagée avec les mêmes étapes qu'avant, en générant un Makefile à partir du Makefile.PL et en lançant make.

Pour tester que notre extension fonctionne, nous créerons une suite de tests dans test.pl. Ce fichier est initialisé par h2xs pour imiter le script de test de Perl lui-même. À l'intérieur de ce script, vous pouvez lancer des tests pour confirmer que l'extension se comporte comme il faut, en affichant ok lorsque c'est le cas et not ok dans le cas contraire. Changez l'instruction d'affichage dans le bloc BEGIN de test.pl en print "1..4\n"; et ajoutez le code suivant à la fin de ce fichier :

 
Sélectionnez
print Montest::est_pair(0) == 1 ? "ok 2" : "not ok 2", "\n";
print Montest::est_pair(1) == 0 ? "ok 3" : "not ok 3", "\n";
print Montest::est_pair(2) == 1 ? "ok 4" : "not ok 4", "\n";

Le script de test s'exécutera lorsque vous taperez make test.

21-3-d. Utilisation des fonctions venant d'une bibliothèque C externe

Jusqu'ici, nos deux exemples ne reposaient sur aucun code C se trouvant à l'extérieur du fichier XS. Nous utiliserons maintenant quelques fonctions de la bibliothèque mathématique C :

 
Sélectionnez
void
arrondi(arg)
        double arg
    CODE:
        if (arg > 0.0) {
            arg = floor(arg + 0.5);
        } else if (arg < 0.0) {
           arg = ceil(arg -0.5);
        } else {
            arg = 0.0;
        }
    OUTPUT:
        arg

Remarquez que l'arrondi que nous définissons ci-dessus ne renvoie pas de valeur, mais change plutôt la valeur de son argument sur place.

Les fonctions floor(3) et ceil(3) font partie de la bibliothèque mathématique C. Si vous compiliez un programme C et que vous aviez besoin de faire une édition de liens avec la bibliothèque mathématique, vous ajouteriez -lm à la ligne de commande, c'est donc ce que vous mettez dans la ligne LIBS du fichier Makefile.PL :

 
Sélectionnez
'LIBS' => ['-lm'], # Se lie avec la biblio mathématique 'm'

Générez le Makefile et lancez make. Changez le bloc BEGIN pour lancer neuf tests et ajouter ce qui suit dans test.pl :

 
Sélectionnez
$i = -1.5; Montest::arrondi($i); print $i == -2.0 ? "ok 5" : "not ok 5", "\n";
$i = -1.1; Montest::arrondi($i); print $i == -1.0 ? "ok 6" : "not ok 6", "\n";
$i = 0.0; Montest::arrondi($i); print $i == 0.0 ? "ok 7" : "not ok 7", "\n";
$i = 0.5; Montest::arrondi($i); print $i == 1.0 ? "ok 8" : "not ok 8", "\n";
$i = 1.2; Montest::arrondi($i); print $i == 1.0 ? "ok 9" : "not ok 9", "\n";

Le lancement de make test devrait maintenant afficher que ces neuf tests sont ok.

La documentation, perlxstut, livrée avec Perl, présente plusieurs autres exemples d'extensions Perl, y compris un exemple utilisant h2xs pour rendre automatiquement disponible une bibliothèque C entière à Perl.

21-4. Insérer Perl (utiliser Perl depuis C)

Vous pouvez accéder à un interpréteur Perl depuis du C en insérant Perl à l'intérieur de votre programme C. Puisque Perl est lui-même un programme, l'insertion consiste à prendre les morceaux importants de Perl et à les intégrer dans les vôtres.

Remarquez que l'insertion n'est pas nécessaire si votre seul but est d'utiliser un programme Perl pouvant fonctionner de manière indépendante et que cela ne vous dérange pas de lancer un processus séparé. Vous pouvez utiliser une fonction comme popen(3) de C pour échanger vos données entre votre programme C et tout programme Perl externe, exactement comme vous pouvez utiliser open(PIPE, "| programme") ou IPC::Open2 et IPC::Open3 de Perl pour échanger des données entre votre programme Perl et tout autre programme. Mais si vous voulez éviter le surcoût du lancement d'un processus séparé, vous pouvez insérer un interpréteur dans votre programme C.

Lors du développement d'applications devant tourner longtemps (par exemple pour l'insertion dans un serveur web), c'est une bonne idée de maintenir un seul interpréteur persistant plutôt que de créer et de détruire des interpréteurs encore et encore. La raison majeure est la vitesse, puisque Perl ne sera chargé qu'une seule fois en mémoire. En utilisant un interpréteur Perl persistant, le module mod_perl d'Apache évite de charger Perl en mémoire à nouveau à chaque fois que quelqu'un charge une page web d'Apache. La page de manuel perlembed donne un exemple d'interpréteur persistant, ainsi qu'un exemple de la manière dont un programme Perl peut gérer de multiples interpréteurs simultanés (un autre atout majeur pour les serveurs web).

21-4-a. Compilation des programmes insérés

Lorsque vous insérez Perl dans C, généralement votre programme C allouera, « exécutera » puis désallouera un objet PerlInterpreter, qui est une struct C définie dans la bibliothèque libperl qui a été construite dans le processus de configuration de Perl pour votre système. La bibliothèque libperl (ainsi que EXTERN.h et perl.h, dont vous aurez également besoin) se trouve dans un répertoire qui variera d'un système à l'autre. Vous devriez être capable de trouver le nom de ce répertoire avec :

 
Sélectionnez
% perl -MConfig -e "print $Config{archlib}"

Vous devriez compiler votre programme exactement de la même manière que votre exécutable perl a été compilé. Tout d'abord, vous aurez besoin de savoir quel compilateur C a été utilisé pour construire Perl sur votre machine. Vous pouvez l'apprendre avec :

 
Sélectionnez
% perl -MConfig -e "print $Config{cc}"

Vous pouvez deviner quoi mettre sur la ligne de commande avec le module standard ExtUtils::Embed. Si vous aviez un programme C appelé interp.c et que votre compilateur C était cc, vous pourriez le compiler pour l'insérer comme ceci :

 
Sélectionnez
% cc -o interp interp.c `perl -MExtUtils::Embed -e ccopts -e ldopts`
21-4-b. Ajout d'un interpréteur Perl à votre programme C

Comme perl (le programme C) s'avère être un bon exemple d'insertion de Perl (le langage), une démonstration simple de l'insertion peut être trouvée dans le fichier miniperlmain.c, dans le code source de Perl. Voici une version non portable de miniperlmain.c, contenant les principes essentiels de l'insertion :

 
Sélectionnez
#include <EXTERN.h>               /* depuis la distribution de Perl */
#include <perl.h>                 /* depuis la distribution de Perl */
static PerlInterpreter *mon_perl; /*** L'interpréteur Perl ***/
int main(int argc, char **argv, char **env)
{
    mon_perl = perl_alloc();
    perl_construct(mon_perl);
    perl_parse(mon_perl, NULL, argc, argv, (char **)NULL);
    perl_run(mon_perl);
    perl_destruct(mon_perl);
    perl_free(mon_perl);
}

Lorsque ceci sera compilé avec la ligne de commande donnée plus haut, vous serez capable d'utiliser interp exactement comme un interpréteur perl ordinaire :

 
Sélectionnez
% interp -e "printf('%x', 3405707998)"
cafefade

Vous pouvez également utiliser des instructions Perl stockées dans un fichier en plaçant le nom du fichier dans argv[1] avant d'appeler perl_run.

21-4-c. Appel d'un sous-programme Perl depuis C

Si un programme Perl contient un sous-programme que vous voulez appeler depuis un programme C, vous pouvez créer un interpréteur Perl, puis utiliser l'une des fonctions commençant par call_, documentées dans la page de manuel perlcall, pour invoquer ce sous-programme. Supposons que ceci soit votre programme Perl, appelé affiche_heure.plx :

 
Sélectionnez
print "Je peux pas être affiché.";

sub affiche_heure {
    print time;
}

Dans cet exemple, nous utiliserons call_argv pour invoquer le sous-programme affiche_heure depuis ce programme C appelé affiche_heure.c :

 
Sélectionnez
#include <EXTERN.h>;
#include <perl.h>

static PerlInterpreter *mon_perl;

int main(int argc, char **argv, char **env)
{
    char *args[] = { NULL };
    mon_perl = perl_alloc();
    perl_construct(mon_perl);
    perl_parse(mon_perl, NULL, argc, argv, NULL);

    /*** court-circuite perl_run() ***/

    call_argv("affiche_heure", G_DISCARD | G_NOARGS, args);

    perl_destruct(mon_perl);
    perl_free(mon_perl);
}

Ici, nous supposons que affiche_heure est un sous-programme Perl qui ne prend pas d'argument (c'est le G_NOARGS) et pour lequel nous pouvons ignorer la valeur de retour (c'est le G_DISCARD). Ces drapeaux, et d'autres, sont présentés dans perlcall. Nous compilons et lançons affiche_heure comme ceci :

 
Sélectionnez
% cc -o affiche_heure affiche_heure.c `perl -MExtUtils::Embed -e ccopts -e ldopts`
% affiche_heure affiche_heure.pl
996412873

Dans ce cas particulier, nous n'appelons pas perl_run, mais, en général, on considère que c'est une bonne formule pour que les méthodes DESTROY et les blocs END soient exécutés au bon moment.

Si vous voulez passer des arguments au sous-programme Perl, vous pouvez ajouter des chaînes à la liste args, terminée par un NULL, passée à call_argv. Pour d'autres types de données, ou pour examiner les valeurs de retour, vous aurez besoin de manipuler la pile de Perl. Nous aborderons ceci un peu plus tard ; pour les rouages et le cambouis, lisez la page de manuel perlcall, livrée avec Perl.

21-4-d. Évaluation d'une instruction Perl depuis C

Perl fournit deux fonctions pour évaluer des extraits de code Perl : eval_sv et eval_pv, décrites dans la page de manuel perlapi. De façon discutable, celles-ci sont les seules routines dont vous aurez jamais besoin pour exécuter du code Perl depuis votre programme C. Le code exécuté peut être aussi long que vous le désirez, contenir plusieurs instructions et employer use, require ou do pour inclure d'autres fichiers Perl.

eval_pv vous permet d'évaluer des chaînes Perl individuelles, puis d'extraire des variables pour la coercition en type C. Le programme suivant, chaine.c, exécute trois chaînes Perl, en extrayant un int depuis la première, un float depuis la deuxième et un char * depuis la troisième :

 
Sélectionnez
#include <EXTERN.h>
#include <perl.h>

static PerlInterpreter *mon_perl;

main (int argc, char **argv, char **env)
{
    STRLEN n_a;
    char *insertion[] = { "", "-e", "0" };

    mon_perl = perl_alloc();
    perl_construct( mon_perl );

    perl_parse(mon_perl, NULL, 3, insertion, NULL);
    perl_run(mon_perl);

    /** Traite $a en tant qu'entier **/
    eval_pv("$a = 3; $a **= 2", TRUE);
    printf("a = %d\n", SvIV(get_sv("a", FALSE)));

    /** Traite $a en tant que flottant **/
    eval_pv("$a = 3.14; $a **= 2", TRUE);
    printf("a = %f\n", SvNV(get_sv("a", FALSE)));

    /** Traite $a en tant que chaîne **/
    eval_pv("$a = 'lreP rueugnoM ertua nu erocnE'; $a = reverse($a);", TRUE);
    printf("a = %s\n", SvPV(get_sv("a", FALSE), n_a));

    perl_destruct(mon_perl);
    perl_free(mon_perl);
}

Toutes les fonctions avec Sv dans le nom, convertissent des scalaires Perl en type de C. Elles sont décrites dans les pages de manuel perlguts et perlapi. Si vous compilez et exécutez ce programme, vous verrez ce qu'engendre, comme résultats, l'utilisation de SvIV pour créer un int, SvNV pour créer un float et SvPV pour créer une chaîne C :

 
Sélectionnez
a = 9
a = 9.9596000
a = Encore un autre Mongueur Perl

Dans l'exemple précédent, nous avons créé une variable globale pour stocker temporairement la valeur calculée de notre expression évaluée. Il est également possible (et dans la plupart des cas, c'est une meilleure solution) d'utiliser la valeur de retour d'eval_pv plutôt que de s'en débarrasser :

 
Sélectionnez
SV *val = eval_pv("reverse 'lreP rueugnoM ertua nu erocnE'", TRUE);
printf("%s\n", SvPV(val,n_a));

La page de manuel perlembed, livrée avec Perl, comprend une démonstration de eval_sv vous permettant d'utiliser les fonctionnalités d'expressions régulières de Perl depuis votre programme C.

21-4-e. Bricolage de la pile de Perl depuis C

En essayant d'expliquer les piles, la plupart des manuels informatiques(163) marmonnent quelque chose à propos d'une pile d'assiettes de cafétéria : la dernière chose que vous avez posée sur la pile est la première que vous retirez. Ça ira pour nos objectifs : votre programme C poussera des arguments sur « la pile de Perl » , fermera les yeux pendant que la magie opère, puis retirera les résultats — les valeurs renvoyées par votre sous-programme Perl — de la pile.

Nous allons présenter ici un exemple sans trop d'explications. Pour vraiment comprendre ce qui se passe, vous aurez besoin de savoir comment faire des conversions entre les types C et les types Perl, avec newSVic, sv_setnv, newAV et tous leurs amis décrits dans les pages de manuels perlguts et perlapi. Vous aurez ensuite besoin de lire perlcall pour apprendre comment manipuler la pile de Perl.

Comme C ne possède pas de fonction interne pour l'exponentiation d'entiers, rendons l'opérateur ** de Perl accessible en C. (Ceci est moins utile qu'il n'y paraît, puisque Perl implémente ** avec la fonction pow(3) de C.) Tout d'abord, nous allons créer une fonction d'exponentiation dans un fichier de bibliothèque appelé puissance.pl :

 
Sélectionnez
sub expo {
    my ($a, $b) = @_;
    return $a ** $b;
}

Maintenant, nous allons créer un programme C, puissance.c, avec une fonction appelée PuissancePerl, poussant les deux arguments dans la pile, invoquant expo et retirant la valeur de retour :

 
Sélectionnez
#include <EXTERN.h>
#include <perl.h>

static PerlInterpreter *mon_perl;

/* "Les vrais programmeurs savent écrire du code assembleur dans
   n'importe quel langage." */

static void
PuissancePerl(int a, int b)
{
    dSP;               /* initialise un pointeur sur la pile */
    ENTER;                          /* tout est créé après ceci */
    SAVETMPS;                       /* ...est une variable temporaire. */
    PUSHMARK(SP);                   /* se souvient du pointeur de pile */
    XPUSHs(sv_2mortal(newSViv(a))); /* pousse la base sur la pile */
    XPUSHs(sv_2mortal(newSViv(b))); /* pousse l'exposant sur la pile */
    PUTBACK;           /* rend local le pointeur de pile global */
    call_pv("expo", G_SCALAR);      /* appelle la fonction */
    SPAGAIN;                        /* rafraîchit le pointeur de pile */
                       /* retire la valeur de retour de la pile */
    printf ("%d à la puissance %d vaut %d.\n", a, b, POPi);
    PUTBACK;
    FREETMPS;                       /* désalloue la valeur de retour */
    LEAVE;             /* ...et les arguments « mortels » de XPUSH */
}

int main (int argc, char **argv, char **env)
{
    char *my_argv[] = { "", "puissance.pl" };

    mon_perl = perl_alloc();    perl_construct( mon_perl );
    perl_parse(mon_perl, NULL, 2, my_argv, (char **)NULL);
    perl_run(mon_perl);

    PuissancePerl(3, 4);            /*** Calcule 3 ** 4 ***/

    perl_destruct(my_perl);
    perl_free(my_perl);
}

Vous pouvez compiler puissance.c pour donner l'exécutable puissance ainsi :

 
Sélectionnez
% cc -o puissance puissance.c `perl -MExtUtils::Embed -e ccopts -e ldopts`
% puissance 3 à la puissance 4 vaut 81.

Maintenant votre programme puissance peut prendre place en étant également différent.

21-5. Morale de l'histoire

Vous pouvez parfois écrire du code plus rapide en C, mais vous pouvez toujours écrire plus rapidement du code en Perl. Puisque vous pouvez utiliser l'un depuis l'autre (et inversement), vous n'avez qu'à combiner leurs forces comme il convient. (Et passez le bonjour aux monstres marins des terra incognita.)


précédentsommairesuivant
 Ou dans certains cas, de mécontenter tout le monde de manière insuffisante.
 Sur certains systèmes, il peut y avoir des moyens de basculer toutes vos interfaces en une seule fois. Si l'option de la ligne de commande -C est utilisée (ou la variable globale ${^WIDE_SYSTEM_CALLS} est positionnée à 1), tous les appels système utiliseront les API de « caractères larges » (wide character). (Ceci n'est actuellement implémenté que sur Microsoft Windows.) Le projet actuel de la communauté Linux est que toutes les interfaces basculeront en mode UTF8 si $ENV{LC_TYPE} est positionnée à « UTF8 ». Les autres communautés peuvent adopter d'autres approches. Ce n'est pas une règle absolue.
Enfin, sauf les sockets AF_UNIX.
En fait, il s'agit plus de cinq ou six bits, selon le nombre de signaux définis par votre système d'exploitation et si l'autre processus utilise le fait que vous n'avez pas envoyé un signal différent.
La synchronisation de l'émission de signaux avec les codes d'opérations (opcodes) au niveau de Perl est planifiée pour une prochaine version de Perl, ce qui devrait régler les questions de signaux et de core dumps.
Si, si c'est vraiment le terme technique consacré.
C'est-à-dire, traditionnellement. Les signaux comptabilisables seront peut-être implémentés sur des systèmes temps-réel d'après les dernières spécifications, mais nous n'avons encore jamais vu de tels systèmes.
Si tant est qu'un processus puisse décéder.
 En fait, les verrous ne s'appliquent pas sur des handles de fichiers — ils s'appliquent sur les descripteurs de fichiers associés aux handles puisque le système d'exploitation ne connaît pas les handles. Cela signifie que tous vos messages die portant sur l'échec de l'obtention d'un verrou sur le nom de fichier sont techniquement inappropriés. Mais les messages d'erreur de la forme « Je ne peux pas obtenir de verrou sur le fichier représenté par le descripteur associé au handle ouvert à l'origine avec le chemin nom_fichier, bien que nom_fichier puisse maintenant représenter un fichier totalement différent de celui que notre handle représente » ne feraient que semer la confusion dans l'esprit de l'utilisateur (sans parler du lecteur).
C'est-à-dire, permettre de la consulter un écran par écran et non déclencher des bips aléatoires.
Vous pouvez utiliser la même chose avec des sockets dans le domaine Unix, mais vous ne pouvez pas utiliser open sur ces derniers.
Une autre utilisation consiste à voir si un handle de fichier est connecté à un pipe, nommé ou anonyme, comme dans -p STDIN.
Il y a même un module Mmap sur CPAN.
Il serait plus réaliste de créer une paire de sémaphores pour chaque morceau de mémoire partagée : un pour la lecture et l'autre pour l'écriture. Et, en fait, c'est ce que fait le module de CPAN IPC::Shareable. Mais nous essayons ici de rester simples. Cependant il vaut mieux admettre qu'avec un couple de sémaphores, vous pourriez alors faire usage d'à peu près le seul bénéfice des IPC SysV : vous pourriez exécuter des opérations atomiques sur des ensembles entiers de sémaphores comme s'il s'agissait d'un seul élément, ce qui est parfois bien utile.
Si cela ne fonctionne pas, lancez ifconfig -a pour trouver l'adresse de broadcast locale appropriée.
N.d.T. La traduction française d'un thread est « une tâche légère » ou une unité élémentaire de processus, mais comme le terme est généralement connu de tous les programmeurs et que les programmeurs Perl sont particulièrement impatients, paresseux et orgueilleux, nous emploierons littéralement le mot thread dans cet ouvrage, ce qui nous permet d'aller plus vite, d'en écrire moins et d'en être particulièrement fiers !
Détourner des fonds publics : piquer la propriété publique dans les fonds au milieu de la nuit ; détourner du public quelque chose qui n'est pas forcément de l'argent, cf. adopter, étendre, GPL.
Le modèle de mémoire partagée des IPC System V, dont nous avons parlé dans le chapitre précédent, ne peut pas être qualifié exactement comme étant « facile et efficace ».
N'appelez pas exit ! Cela essayerait de quitter votre processus entier et y arriverait peut-être. Mais en fait, le processus ne se terminera pas avant que tous les threads ne le fassent et certains d'entre eux peuvent refuser de se terminer sur un exit. Nous donnerons plus d'informations là-dessus plus tard.
Certains feux de passage à niveau sont obligatoires (ceux avec une barrière) et certaines personnes pensent que les verrous devraient l'être également. Mais imaginez seulement un monde dans lequel chaque carrefour aurait des armes se levant et se baissant lorsque les feux changeraient.
Ou traduire, ou transformer, ou transfigurer, ou transmuter, ou métamorphoser.
Votre script original est également un fichier exécutable, mais il n'est pas en langage machine, nous ne l'appelons donc pas une image. Un fichier image est appelé ainsi, car il s'agit d'une copie exacte des codes machine que votre CPU sait directement exécuter.
N.d.T. : en anglais, faire tourner se dit run et par conséquent, cette phase s'appelle également run phase.
Non, il n'y a pas de diagramme de syntaxe formelle comme en syntaxe BNF (Backus-Naur Form), mais nous vous invitons à lire attentivement, dans l'arborescence des sources de Perl, le fichier perly.y, qui contient la grammaire yacc(1) que Perl utilise. Nous vous recommandons de vous tenir le plus loin possible de l'analyseur lexical, qui est connu pour avoir provoqué des troubles alimentaires chez des rats de laboratoire.
En fait, nous simplifions ici à l'extrême. L'interpréteur est bien lancé, car c'est de cette manière que le précalcul de constantes est implémenté. Mais il est immédiatement lancé à la compilation, de la même façon que les blocs BEGIN sont exécutés.
N.d.T. PVM est l'acronyme habituel de Parallel Virtual Machine, machine parallèle virtuelle.
Avec une exception jusqu'ici : la version 5.6.0 de Perl peut cloner des interpréteurs dans l'implémentation de l'émulation de fork sur Microsoft Windows. Il pourrait bien y avoir une API Perl pour les « ithreads » , comme on les appelle, au moment où vous lirez ceci.
Mais alors, comme c'est le cas pour tout, une fois que vous êtes devenu aveugle. Est-ce que nous ne vous avions pas prévenu de ne pas jeter de coup d'œil ?
N.d.T. Dès que parsés, analysés, jeu de mots avec la signification habituelle d'ASAP : as soon as possible, dès que possible.
En supposant que vous êtes d'accord avec le fait qu'Unix soit standard et maniéré.
Heu, veuillez excuser ces problèmes techniques...
Du moins, sur les versions antérieures à Mac OS X, qui, de manière assez heureuse, est un système dérivé de BSD.
Techniquement, il ne s'agit pas vraiment d'une édition « sur place ». Il s'agit du même nom de fichier, mais d'un fichier physique différent.
Nous utilisons le terme en connaissance de cause.
N.d.T. En anglais, « bogue » se dit bug, qui signifie également « punaise », « insecte », « coléoptère » ou « microbe », ce qui explique les différentes allusions à des parasites dans les exemples de ce chapitre. Nous aurions pu adapter ces exemples à l'enveloppe de la châtaigne (sens premier de « bogue » en français), mais avouez qu'un bogue informatique est aussi nuisible que certains insectes !
Nous utilisons la syntaxe du shell sh pour montrer la configuration des variables d'environnement. Les utilisateurs d'autres shells feront les adaptations en conséquence.
Enfin, d'après Nathan Torkington, qui a contribué à la version originale de ce paragraphe.
Alors que nous prenons soin de distinguer le compilateur de l'interpréteur lorsque cette distinction est importante, cela devient un peu ennuyeux de persister à dire « compilateur/ interpréteur », nous raccourcissons donc cela souvent en « interpréteur » pour signifier la globalité du code et des données C fonctionnant comme une instance de perl (le programme) ; lorsque vous incluez du Perl, vous pouvez avoir plusieurs instances de l'interpréteur, mais chacune se comporte en tant que son propre petit perl.
N.d.T. Le squelette généré par h2xs contient des commentaires en anglais. Toutefois, nous les avons traduits ici pour une meilleure compréhension.
Plus le livre Perl de circonstance.

Licence Creative Commons
Le contenu de cet article est rédigé par Larry Wall, Tom Christiansen et Jon Orwant et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Pas de Modification 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2018 Developpez.com.