Page suivante Page précédente Table des matières

3. Perlpp: cpp sous stéroïdes

Par Warren MacEvoy wmacevoy@mesastate.edu

L'objet de cet article est de vous présenter un outil que j'ai appelé perlpp, le préprocesseur Perl. Depuis que je l'ai écrit, perlpp n'est disponible dans aucune distribution de Linux. Voyez les ressources pour savoir comment l'obtenir ainsi que les exemples ici décrits.

Perlpp est une version étoffée de cpp, le préprocesseur C. Il peut faire tout ce que fait cpp et plus encore. Il est, par exemple, facile d'introduire la notion de patrons de code dans quelque langage de programmation que ce soit avec perlpp.

L'utilisation de perlpp, le préprocesseur Perl, demande au moins une connaissance rudimentaire de la programmation en Perl, dont une version égale ou supérieure à la 5 doit être installée sur votre système.

C'est un langage tellement utile que presque chaque programmeur devrait en avoir quelques rudiments. Je commencerai par traiter des quelques notions de Perl employés dans les exemples. Si vous êtes déjà à votre aise dans ce domaine, vous pouvez passer directement à la section suivante.

Les variables. Les variables scalaires, qui peuvent recevoir comme valeur des chaînes de caractères, des entiers ou des nombres en double précision sont toujours préfixées par un $, les listes, de simples listes de scalaires en fait, par un @. Toutes les variables sont globales, à moins qu'elles ne soient précédées d'un my lors de leur première utilisation à l'intérieur d'un bloc.

Désignation des chaînes de caractères. En Perl, elles peuvent apparaître sous trois formes : presque littéralement, entre apostrophes ('), avec de l'interpolation, entre guillemets (") et de niveau système, entre apostrophes inversées (`). Nous reviendrons plus en détails sur ce point plus tard, mais en gros :

Les boucles. Perl gère les boucles de style csh sous la forme :

 
foreach $index (@LIST) { 
   expression1;
   expression2; 
   .... 
}

ainsi que celles du C :

 
for (do-once; check-first-each-time; do-last-each-time ) { 
   expression1;
   expression2; 
   .... 
}
Ces deux variantes seront utilisées dans les exemples.

En fait, la syntaxe de base de Perl imite celle du C à bien des égards, ce qui permet à ceux qui programment en C de lire des scripts Perl assez facilement. Non, j'exagère : un programmeur C peut écrire du Perl ressemblant à ce langage, et en obtiendra à peu près ce qu'il en attend. Un programmeur Perl, lui, résoudrait le même problème d'une manière totalement différente. Ce faisant, il peut arriver à réaliser quelque chose de difficile à imaginer : du code plus touffu que celui que l'on écrirait volontiers en C. Si vous ne me croyez pas, regardez donc un peu les sources de perlpp, qui se trouvent être un script Perl.

Perl est beaucoup plus que ce maigre aperçu, mais ces quelques notions devraient vous suffire pour comprendre les exemples. Voyez les ressources pour en savoir davantage à son sujet.

3.1 Introduction

Commençons par discuter de cpp. Les programmeurs C apprennent vite que leur code, au moins logiquement, passe par deux étapes de traduction. La première, le pré-traitement, a recours à des commandes telles que :

 
#include <stdio.h>
et

#define FOO(x) bar(x)
pour traduire le fichier d'entrée hybride C/cpp en un pur fichier C, lequel sera transmis au pur compilateur C. Illustrons :

 
fichier_d_entree  -> cpp -> cc1 ->  fichier_objet 
Alors que le but initial de cpp est de pré-traiter des fichiers source à destination d'un compilateur C (ou C++), on peut l'utiliser pour d'autres types de fichiers. Par exemple, xrdb se sert de cpp pour préparer des fichiers de ressources X11 avant de les charger. cpp est un outil très pratique, mais un programmeur peut être rapidement confronté à des limitations, essentiellement en raison de ses capacités réduites en tant que pré-processeur pour les calculs et le traitement des chaînes de caractères.

La raison pour laquelle j'ai conçu perlpp était précisément de remédier à ces carences pour un problème de calcul scientifique aux Pacific Northwest National Laboratories, où j'ai rédigé la partie sur l'équilibre chimique d'un modèle d'aqueduc. Dans le but de rester compatible avec le reste du modèle, elle devait être écrite en FORTRAN. Et pour le rester avec les environnements de développement Linux, Sun et SGI, il fallait que ce soit FORTRAN 77. L'énoncé du problème était le suivant : étant données les équations d'équilibre chimique pour un ensemble donné d'espèces, générer automatiquement un programme efficace et fiable pour résoudre ces équations.

Il fallait donc, à partir d'équations d'équilibre chimique sous une forme symbolique, passer à un fichier de traitement par lot Maple V (une application de mathématiques symboliques) en respectant un patron et en intégrant les résultats de ce dernier dans une bibliothèque de sous-programmes FORTRAN issue de patrons satisfaisant aux exigences du projet.

Cet environnement demandait la création automatique de plusieurs sortes de programmes à partir de patrons et a tout naturellement fourni matière à réflexion pour des pré-processeurs efficaces. Bien qu'il m'ait fallu presque une semaine pour produire la version alpha de perlpp, cela a permis de gagner le même laps de temps sur ce seul projet. Sans l'avoir, venir à bout de ce problème aurait pu prendre quatre ou cinq semaines de plus. En outre, sans perlpp, le projet serait beaucoup plus difficile à maintenir.

3.2 Ce que fait Perlpp

perlpp prend des fichiers en entrée et engendre des scripts perl lesquels, lors de leur exécution, créeront en mieux des fichiers de sortie semblables.

Exemple 1: Hello World!

Crée un fichier hello.c.ppp qui contient les lignes

 
#include <stdio.h>
int main()
{
printf("Hello World!\n");
return 0;
}
A présent, lancez la commande perlpp de la façon suivante :

 
perlpp -pl hello.c.ppp
Nous reviendrons sur l'option -pl plus loin. Si vous allez jeter un coup d'oeil, vous verrez que perlpp a créé le fichier hello.c.pl, qui contient le script Perl suivant :

 
#!/usr/bin/perl
print '#include <stdio.h>
';
print 'int main()
';
print '{
';
print '  printf("Hello World!\\n");
';
print '  return 0;
';
print '}
';
La teneur de la première ligne pourra varier selon votre configuration. Voyez la section Dépannage si vous rencontrez des problèmes avec ce script.

Le lancement de hello.c.pl produit le même texte que le fichier d'entrée originel hello.c.ppp. De ce fait, on peut considérer perlpp comme un moyen abscons et dispendieux du point de vue des traitements pour copier des fichiers de texte.

L'option -pl signifie "créer un programme perl". Si vous ne la spécifiez pas, le programme est simplement exécuté et la sortie écrite dans hello.c.

En clair

 
perlpp hello.c.ppp
est équivalent à

 
perlpp -pl hello.c.ppp
  ./hello.c.pl > hello.c
  rm hello.c.pl
à part que le fichier hello.c.pl n'est jamais explicitement créé.

Donc, notre premier exemple, hello.c.ppp, lorsqu'il est normalement traité par perlpp, crée une copie de lui-même, hello.c. Ce qui ne devrait ni vous enthousiasmer, ni vous surprendre. Après tout, si vous soumettez à cpp un fichier de texte ne contenant aucune directive qui lui est propre, vous devriez récupérer exactement ce que vous avez fourni au départ.

cpp n'a d'intérêt qu'avec des fichiers d'entrée incluant des directives. Perlpp, lui, n'est que moyennement intéressant avec un fichier d'entrée dépourvu de directives perlpp, car il engendre un script Perl qui régénère le fichier d'entrée grâce à des expressions print. Pour pouvoir en faire plus, il faut utiliser les directives perlpp.

3.3 Directives

Il n'y en a que quatre pour perlpp, en plus d'une directive par défaut. Chacune décrit comment une ligne d'entrée donnée doit être transcrite dans le script Perl.

Si aucun des caractères !, ', " ou ` ne débute une ligne, c'est une traduction par défaut qui intervient :

Exemple 2 : Salutations

Créons un fichier salutations.c.ppp contenant les lignes :

 
  #include <stdio.h>
  int main()
  {
  !foreach $s ('Hello World!','Hola Mundo!', 'Ciao!') {
  "  printf("$s\\n");
  !}
    return 0;
  }
Regardons d'abord le script Perl résultant en tapant :

 
perlpp -pl salutations.c.ppp
Vous trouverez dans salutations.c.pl

 
  print '#include <stdio.h>
  ';
  print 'int main()
  ';
  print '{';foreach $s ('Hello World!','Hola Mundo!', 'Ciao!') { 
  print "  printf(\"$s\\n\");
  ";}
  print '  return 0;
  ';
  print '}
  ';
Regardez attentivement l'expression print créée par l'expression printf de salutations.c.ppp :

 
print "  printf(\"$s\\n\"); ";
Perlpp se préoccupe d'ajouter des barres de fraction inverses là où c'est nécessaire afin que des apostrophes ne terminent pas prématurément la chaîne. De même pour les autres formes d'expressions encadrées de guillemets ou d'apostrophes engendrées par perlpp.

Exécutons ce script avec perlpp

 
perlpp salutations.c.ppp
Ce qui produit le fichier salutations.c,

 
#include <stdio.h>
int main()
{
printf("Hello World!\n");
printf("Hola Mundo!\n");
printf("Ciao!\n");
return 0;
}

Exemple 3 : Les patrons en deux mots

Ce dernier exemple utilise perlpp pour générer un patron pour des classes de vecteurs de longueur fixe en C++, dans lesquelles les boucles sont développées. Développer une boucle signifie, par exemple, remplacer le code

  
for (int i=0; i<3; ++i) a[i]=i;
par

  
a[0]=0; a[1]=1; a[2]=2;
Cela ne change rien quant à l'action du code, il devient simplement plus rapide. Ceci, car la variable d'index n'a pas besoin d'être incrémentée puis comparée entre chaque affectation.

Une telle classe de patron de longueur fixe serait utile, par exemple, dans une bibliothèque graphique où des vecteurs bi- et tri-dimensionnels de types fixés (float, int, double) seraient employés. Tous seraient semblables (d'où une occasion d'utiliser une classe de patron) mais le coût en performance dû au bouclage serait inacceptable.

Ici, perlpp peut aider. Il sera d'abord utilisé pour créer un programme Perl (grâce à l'option -pl) depuis un fichier patron, Point.Template.ppp. Le script Point.Template.pl est conçu pour engendrer différentes classes de vecteurs de longueur fixe, en fonction des arguments qui lui sont passés. Grâce à la directive d'impression système par apostrophe inversée, ce script est ensuite utilisé dans le fichier source originel, testPoint.cpp.ppp, pour générer les classes spécifiques demandées.

Le fichier Point.Template.ppp est assez long et est disponible en FTP anonyme comme indiqué dans les ressources. Par conséquent, je ne m'attacherai qu'aux parties de ce fichier qui illustrent de façon intéressante la manière d'utiliser perlpp.

La première ligne de Point.Template.ppp digne d'intérêt est

  
! eval join(";",@ARGV);
ce qui, bien sûr, se traduira en expression Perl par
  
eval join(";",@ARGV);
Seul le ! de tête est supprimé. L'exécution de cette ligne réunit tous les arguments de ligne de commande du script, séparés par des points-virgule, et évalue le tout comme une suite d'expressions Perl. C'est une façon très fruste de traiter des arguments de ligne de commande, mais cela convient à nos besoins.

Les quelques lignes qui suivent vérifient que la précédente évaluation de ligne de commande a bien défini trois variables essentielles :

Si elles n'ont pas été définies, le script vous le fera savoir via STDERR et se terminera avec 1 comme code de sortie.

Après ceci, le patron s'occupera de générer la classe désirée. Ce qui commence par

  
"class $name {
"public:
!#
!# Declare internal array of desired type and size
!#
"  $type a[$dim];
"  static const int dim=$dim;
Ici, $name, $type et $dim sont utilisées pour créer du texte spécifique dans la définition de la classe. En Perl, # désigne un commentaire, par conséquent, !# en est également un en perlpp.

On peut trouver la première occurence de développement de boucle dans le constructeur de classe par défaut. Les lignes

  
!  for ($i=0; $i<$dim; ++$i) {
"    a[$i]=0;
!  }
se traduisent en segment Perl

  
for ($i=0; $i<$dim; ++$i) {
     print("    a[$i]=0;
");
}
Cette boucle est exécutée dans le script Perl en tant que pré-processeur, au sein duquel elle sera développée en une suite d'affectations dans la source de la classe C++. Les boucles sont développées d'une manière identique dans les autres parties de la définition de classe.

Efficacité mise à part, le bloc de code source >perlpp suivant fournit un constructeur de classe qui serait impossible à déclarer en utilisant les facilités couramment offertes par les patrons : un constructeur qui a autant d'arguments que la dimension de la classe du vecteur devant être construite.

  
  !  @arg=(); for ($i=0; $i<$dim; ++$i) { $arg[$i]="$type a$i"; } 
  !  $args=join(',',@arg);
  !
  "  $name($args)
Si Perl est nouveau pour vous, vous pouvez avoir du mal à comprendre la première ligne. Elle commence par l'affectation de la liste @arg à une liste vide, puis elle crée via une boucle les entrées de $dim dans @arg: "$type a0", "$type a1", etc. La raison pour laquelle les éléments de @arg sont désignés par $arg[$i] dans la boucle for est que @arg, une fois affectée, fait référence à la variable scalaire en tant qu'ième entrée de @arg. Rappelez-vous, les variables scalaires sont toujours préfixées par un $ --même celles qui sont enfouies dans une liste.

Après cette déclaration, le constructeur est défini de façon à initialiser le vecteur avec ses arguments :

  
"  {
!    for ($i=0; $i<$dim; ++$i) {
"      a[$i]=a$i;
!    }
"  }
Ensuite vient la définition des opérateurs affectés, lesquels sont parfaitement standard. Puis, une autre caractéristique de perlpp est mise en évidence : le code qui permet de définir toutes les assignations des opérateurs est créé via une structure de boucle :

  
!  foreach $op ("=","+=","-=","*=","/=") {
    .
    . # définition de l'assignation d'opérateur $op
    .
!  }
Dans la mesure où les assignations d'opérateurs sont en gros définies de la même façon, cette boucle rend l'écriture du patron plus compacte qu'avec les fonctionnalités habituelles. D'où, un développement, une maintenance et un débogage du patron plus rapides.

On trouve ensuite une boucle semblable destinée à définir les divers opérateurs binaires de la classe : addition, soustraction, etc. Toutes ces boucles, réduisent le côté redondant dans l'effort à fournir pour la définition du patron, ce qui est déjà, c'est amusant, un outil de réduction de redondance des tâches. Bon, d'accord, j'avoue, un rien m'amuse.

Le restant du patron déclare et définit trois opérateurs, des fonctions d'E/S et une multiplication scalaire. Ils font ce pour quoi ils sont conçus et on n'apprendra rien de plus sur perlpp en s'y attardant.

Poursuivons en utilisant Point.Template.ppp. Il faut tout d'abord le convertir en script Perl grâce à la commande :

  
perlpp -pl Point.Template.ppp
A présent, regardez le fichier source du programme de test, testPoint.cpp.ppp. La seule ligne qui nous intéresse est

  
` ./Point.Template.pl '\$name="FixVect"' '\$dim=2' '\$type="float"'
Elle lance le script Point.Template.pl fraîchement créé avec les arguments :

  
$name="FixVect"  $dim=2 $type="float"
Grâce à eux, le script patron affiche une classe FixVect, représentant des tableaux bi-dimensionnels de flottants, lesquels seront inclus dans le fichier source testPoint.cpp via la directive perlpp apostrophe inversée.

Engendrer des classes de patrons de cette façon n'est pas totalement satisfaisant, car les notions de déclaration et de définition de classe doivent être en principe distinctes. Cependant, on peut le rectifier en modifiant le fichier patron. En gros, on pourrait affecter une quatrième variable, $use, lors de l'appel du script, laquelle aurait la valeur soit de "declare" soit de "define". Ainsi, en utilisant des clauses if, le script fournirait la partie définition ou déclaration de la classe. C'est encore une autre manière de réduire la redondance d'un patron avec perlpp.

3.4 Conclusions

Je ne veux pas vous laisser avec le sentiment que perlpp n'est qu'une sorte "d'algorithme de compression". Rassembler les idées dans un projet simplifie leur maintenance. Le but de perlpp est d'éviter les "fuites de concept", dans lesquelles plusieurs parties de fichiers source représentent d'une façon redondante une idée, ceux-ci devant être maintenus séparément.

Perlpp remplace essentiellement le plutôt rigide (mais simple !) langage de "traitement" de texte cpp par l'équivalent souple (mais complexe) Perl. Beaucoup de programmeurs l'utilisent déjà pour un peu tout et n'importe quoi, de ce fait, en connaître la syntaxe est doublement gratifiant : en tant que tel, et comme puissant macro-langage pour tout langage de programmation.

Si vous ne connaissez pas Perl, alors perlpp n'est qu'une bonne raison de plus pour l'apprendre.

3.5 Ressources

perlpp est disponible sous forme d'archive tarée en FTP anonyme sur perlpp-0.5.tar.gz. La distribution inclut la documentation propre à l'installation du paquetage.

Les exemples de cet article se trouvent sur lj-article.tar.gz.

Vous devez disposer d'une version de Perl égale ou supérieure à la 5, pour pouvoir utiliser perlpp. On trouve un paquetage de Perl sous une forme ou sous une autre dans toutes les distributions de Linux. La page Web www.perl.org est un point de départ incontournable si vous voulez en savoir plus sur ce langage.

3.6 Dépannage

perlpp est un script Perl qui en génère d'autres. Pour pouvoir vous en servir, Perl doit être installé, et perlpp capable de le trouver. Si ce dernier ne fonctionne pas, vérifiez que ses deux premières lignes sont en accord avec l'emplacement des exécutables de Perl.

Si elles sont justes, assurez-vous que les droits en exécution sont bien positionnés pour le script (chmod 755 perlpp), et que perlpp est accessible depuis votre PATH.

Si vous venez d'installer perlpp, il se peut que vous deviez rafraîchir le cache des répertoires du PATH de votre shell avec hash -r (si vous utilisez bash) ou rehash (pour csh).

3.7 Remerciements

Merci à la communauté des Linuxiens pour avoir fourni un si fantastique environnement permettant des calculs scientifiques fiables. J'ai beaucoup de mal à retenir mes railleries à chaque fois qu'un de mes collègues essaie de faire quelque chose d'utile sur une machine qui plante tellement souvent qu'il en est arrivé à s'y attendre.

Je remercie également Mike Littlejohn pour avoir testé perlpp et relu cet article, de même que Karl Castleton, Steve Yabusaki et Ashok Chilakapati pour m'avoir embauché sur le projet de modélisation d'aqueduc.

Et enfin, merci aux Pacific Northwest National Laboratories, au programme de regroupement des Associated Western Universities, et à la faculté Mesa State College pour m'avoir accordé le temps, les moyens et l'opportunité de développer perlpp.

3.8 Une petite histoire

Alors que j'avais laissé tourner ma machine Red Hat 5.0 pour l'été dans mon bureau de la faculté Mesa State College de Grand Junction, Colorado, je suis parti pour les Pacific Northwest National Laboratories de Richland, Washington, où j'ai rêvé perlpp.

Je me suis servi à distance de ma Linuxette pendant tout l'été : furetage sur le web, e-mail, récupération de vieux fichiers source, utilisation de Emacs, Maple, TeX, Perl ou du compilateur FORTRAN. C'est vrai que j'ai eu recours à ces outils aussi sur les machines du PNNL, mais quelquefois, il manquait une licence ou l'équivalent Linux était meilleur pour ce que je voulais en faire au labo.

Pendant six semaines, j'ai utilisé cette machine distante au moins une fois par jour. Je n'ai eu des difficultés à m'y connecter qu'une seule fois. Passé l'été, j'ai appris que mon bureau du Colorado, qui se trouve dans un bâtiment réhabilité, avait connu plusieurs coupures de courant. Apparemment, à chaque fois, ma machine était repartie sans l'ombre d'un problème, et je ne m'en étais aperçu qu'à l'occasion d'une interrogation pendant une de ces coupures.

En matière de fiabilité et de disponibilité c'est beaucoup mieux que ce à quoi sont confrontés la plupart de mes collègues qui utilisent d'autres systèmes d'exploitation.

Copyright © 1999, Dr. Warren MacEvoy Publié dans le n° 44 de Linux Gazette, Août 1999.

Traduction française de Joël Sagnes.


Page suivante Page précédente Table des matières