debut.gif (172 octets) boutprec.gif (153 octets) boutsuiv.gif (154 octets)

10. Construire des programmes modulaires

 

10.1 Position de la question

À ce point de l'exposé et de ses exercices personnels le lecteur sera sans doute arrivé à la conclusion que la programmation est un art dont la difficulté réside moins dans l'impénétrabilité des concepts que dans l'accumulation des objets, le grouillement des processus et la complexité du texte qui les décrit. Dès lors qu'il est possible de diviser ce texte en petits chapitres, beaucoup d'algorithmes se révèlent finalement simples, ou s'ils restent abstrus, du moins leur difficulté se cantonne-t-elle à un passage concis.

Jusqu'à présent nous pouvons diviser les difficultés de nos programmes en les découpant en procédures. Abstraire une partie d'un traitement pour en faire une procédure indépendante (ou emboîtée) réduit la taille du texte que nous avons à garder en mémoire à un instant. Restreindre à cette procédure la portée de ses variables locales diminue le nombre d'objets dont nous avons à nous préoccuper simultanément.

Certaines procédures utilisées dans le programme peuvent être chargées par load. Ceci permet de ne pas avoir tout le texte dans le même fichier. Mais, que le programme soit interprété ou compilé, il est compris dans un environnement global unique, c'est à dire un espace de noms unique, même s'il est hiérarchisé par l'emboîtement des environnements. Toute modification ne serait-ce que d'une ligne du programme nécessite sa ré-interprétation ou sa re-compilation intégrale. On imagine que pour le développement de très gros programmes écrits par une équipe et non par une seule personne cela va poser un problème.

Scheme tel que défini par [CKR98] n'offre pas de solution à ce problème. Les méthodes de programmation que nous allons introduire maintenant sont donc des extensions à la norme, celles du compilateur Bigloo en l'occurrence.

 


10.2 Notion de module

Pour faire face au foisonnement de la complexité engendré par l'écriture de grands programmes, l'idée est de pouvoir découper le programme en modules qui seront compilés séparément.

Dans la plupart des systèmes de programmation, le compilateur traite les programmes source fichier par fichier. Les modules seront chacun dans un ou plusieurs fichiers et nous devrons avoir des moyens d'indiquer au compilateur comment assembler ces fichiers.

Les modules seront eux-mêmes constitués de procédures. Une procédure d'un module devra pouvoir invoquer une procédure d'un autre module.

Les modules ne mettront pas en commun toutes leurs informations, procédures ou données, mais uniquement celles qui seront explicitement définies comme publiques, ou exportées, rendues visibles aux modules extérieurs. Les autres procédures seront dites privées.

Inversement, un module ne tiendra pas compte de toutes les informations exportées par les autres modules, mais il n'importera que les procédures et les données qui sont nécessaires à son exécution.

Pour chaque module, la liste des informations qu'il exporte et de celles qu'il importe définit son interface avec le « monde extérieur ». Lors de la réunion des différents modules pour construire le grand programme (cette opération s'appelle l'édition de liens), les informations définies par les interfaces servent à établir la communication entre les modules.

Historiquement, trois philosophies de compilation séparée se sont développées parmi les langages informatiques :

 

 


10.3 Les modules de Bigloo

Nous allons reprendre le problème de lecture de banque de séquences de l'alinéa 9.3 page [*] pour en imaginer une conception en modules (ou modulaire) du programme destiné à le résoudre.

Nous pouvons avoir envie de découper ce problème en trois parties :

 

 

10.3.1 Premier module : point d'entrée du programme

Voici le premier module choix-fichier, contenu dans le fichier choix-fichier.scm, par où démarre le programme :

Le module choix-fichier

(module choix-fichier
        (main choisir-le-fichier)
        (import lire-swiss-seq))       ; \label{lisp:import-swiss}

(define (choisir-le-fichier argv)       ; \label{lisp:choix}
  (call-with-input-file (cadr argv) lire-des-sequences)  ; \label{lisp:call}
  )

74

Le mot-clé main indique que la procédure choisir-le-fichier sera celle par où commencera l'exécution du programme lors de son lancement.

La procédure lire-des-sequences (ligne [*]) ne figure pas dans ce module. Elle sera importée depuis le module lire-swiss-seq, ce qu'indique la clause import.

À la ligne [*] argv représente la liste d'arguments que le programme reçoit du shell Unix lors de son lancement. Par convention, Unix transmet sous forme de liste à tout programme lancé depuis la ligne de commande du shell le contenu de la ligne tapée par l'utilisateur pour le lancer, chaque mot tapé étant représenté par une chaîne de caractères qui sera un élément de la liste. Le premier élément est donc le nom du programme (plus précisément le nom du fichier qui contient l'exécutable), les éléments suivants sont les arguments éventuellement fournis par l'utilisateur. Dans le cas présent l'utilisateur devra fournir le nom du fichier contenant la banque de séquences qu'il souhaite examiner, nom qui sera donc récupéré comme (cadr argv), ainsi qu'indiqué en ligne [*]. Le lancement du programme, nommé lire-swissprot, se fera par exemple ainsi :

 

./lire-swissprot ../Banques/swissprot.extrait

 


10.3.2 Module de lecture des séquences

Le module lire-swiss-seq va importer des choses qui sont dans lire-outils et exporter la procédure lire-des-sequences, invoquée depuis le module précédent choix-fichier.

Module lire-swiss-seq

(module lire-swiss-seq
        (import lire-outils)
        (export (lire-des-sequences flux)))

(define (lire-des-sequences flux)
   (let boucle ((sequence (lire-une-sequence flux))) ; \label{lisp:lireune}
      (if (string=? (car sequence) "*EOF*")
          #f
          (begin
             (imprime sequence)
             (boucle (lire-une-sequence flux))))))

(define (lire-une-sequence flux)
   (let boucle ((L '())
                (ligne (lire-une-ligne flux)))
      (if (string=? ligne "*EOF*")
          (list ligne)
          (let ((type-ligne (substring ligne 0 2)))
             (if (string=? type-ligne "//")
                 (reverse (cons ligne L))    ; \label{lisp:reverse}
                 (boucle (cons ligne L)
                         (lire-une-ligne flux)))))))

74

Les deux procédures de ce module ont été commentées en 9.3.1 page [*]. On relèvera que lire-une-sequence est une procédure privée du module.

 

10.3.3 Module utilitaire

Le module lire-outils

(module lire-outils
        (export (imprime seq)
                (lire-une-ligne flux)))

(define (imprime seq)
  (if (null? seq)
      (print "sequence vide")
      (if (null? (cdr seq))
          (print (car seq))
          (begin
            (print (car seq))
            (imprime (cdr seq))))))

(define (lire-une-ligne flux)
   (if (eof-object? (peek-char flux))
       "*EOF*"
       (let boucle ((ligne "")
                    (c (read-char flux)))   ; \label{lisp:readchar}
          (if (char=? c #\newline)
              ligne
              (boucle (string-append ligne (string c))
                      (read-char flux))))))  ; \label{lisp:readchar}

74

Les procédures ont été commentées et la clause de la déclaration de module n'appelle guère de commentaire.

 

10.3.4 Structure du programme

Un programme Bigloo est constitué de plusieurs modules. Un module peut s'étendre sur plusieurs fichiers (voir [Ser98]). Il convient de noter que chaque module constitue un espace de noms séparé, et qu'entre modules ne sont partagées que les informations explicitement exportées et importées et les données passées en arguments. Le découpage en modules introduit une divergence avec le modèle d'environnement global de Scheme tel que défini dans [CKR98].

 


10.4 Construction d'un programme modulaire

Nos trois modules sont rédigés, il reste à les compiler et à construire le programme exécutable. Au chapitre 3 page [*] nous avons vu comment construire un programme mono-module :

 

# bigloo -o <nom d'exécutable > <nom de fichier source>  # 92 $[\mathcal{E}]$97

Ici c'est un peu plus compliqué, nous devons compiler chacun des trois modules, puis les relier ensemble pour construire l'exécutable. L'invocation de la commande bigloo montrée ligne $[\mathcal{E}]$ enchaînait automatiquement compilation et construction. Ici il va falloir décomposer la construction en deux temps, compilation et édition de liens.

Cette séparation en deux étapes peut sembler compliquée, mais elle est de toute façon inévitable si l'on veut relier ensemble des modules écrits dans des langages différents, donc forcément compilés par des compilateurs différents et réunis in fine par l'édition de liens.

 

10.4.1 Compilation de modules source vers fichiers objets

 

 


10.4.2 Édition de liens pour construire l'exécutable

 

 

 

Figure: Construction d'un exécutable
\includegraphics[scale=0.8]{../Images/Sep-compile/compil-sep.epsi}67

 

Ce processus est d'une complexité non négligeable, il serait plus confortable de pouvoir le programmer. Ce sera l'objet de la section suivante.

 

10.5 make et Makefile

Le système Unix possède un système de construction de programmes (on dit aussi un configurateur) nommé make. Au moyen de ce système on peut décrire les fichiers source et autres nécessaires à la construction d'un programme, les dépendances entre fichiers, les commandes nécessaires à la compilation des sources et à la construction des exécutables, ainsi que beaucoup d'autres choses.

L'usage habituel de make consiste à déterminer automatiquement quelles parties d'un grand programme doivent être recompilées après que certains fichiers source aient été modifiés, et à lancer les commandes appropriées pour ce faire.

Make est en fait un système plus général que seulement un configurateur de programmes. C'est un langage de programmation adapté à la résolution de systèmes de contraintes. Un programme make comporte deux types d'instructions : des règles et des commandes.

Une règle énonce le nom d'une cible à atteindre (par exemple le nom d'un fichier exécutable) suivi des noms des cibles préliminaires dont elle dépend et qu'il faudra avoir atteintes avant d'espérer atteindre la cible courante.

Une instruction de type règle est suivie d'instructions de type commandes qui formulent les actions à effectuer pour atteindre la cible de la règle.

Make peut être utilisé pour configurer d'autres objets que des programmes : toute situation où certains fichiers-cibles doivent être mis à jour automatiquement à partir de fichiers-origines à chaque modification de ces derniers et selon certaines règles est susceptible d'être automatisée au moyen de make. Une cible n'est d'ailleurs pas nécessairement un fichier, il peut s'agir plus généralement d'un ensemble de tâches à réaliser désigné par un nom symbolique.

Le langage de make est loin d'être simple et sa description complète excéderait les limites de ce cours10.1. Nous nous contenterons d'indiquer quelques moyens propres à rédiger un programme make simple.

 

10.5.1 Un Makefile simple pour construire un programme

Un programme make est contenu dans un fichier nommé par convention Makefile. En général on place dans un même répertoire les fichiers qui servent à la construction du programme et le Makefile qui indique comment le construire. Les exemples ci-dessous supposent cette condition remplie, et que ce répertoire est le répertoire courant quand on tape la commande make.

Nous prendrons comme exemple la construction du programme donné ci-dessus (section 10.4 page [*]). Notre Makefile va comporter des lignes de texte essentiellement de deux sortes :

 

10.5.1.1 Lignes de règles

Des lignes de règles, ou lignes de dépendance, énumèrent, pour une cible à produire, les cibles préalables (souvent des fichiers) dont elle dépend. Si un des fichiers dont dépend la cible est modifié depuis sa dernière construction10.2, il faudra la reconstruire. Ainsi, la règle de dépendance pour lire-outils.o est très simple :

 

lire-outils.o: lire-outils.scm

Le fichier-objet lire-outils.o dépend du fichier-source lire-outils.scm. Si ce dernier est modifié (la création est un cas particulier de modification) il faut reconstruire l'objet.

Une ligne de règle se reconnaît au fait qu'elle comporte le signe « : », qui sépare la cible à sa gauche des pré-requis à sa droite.

La règle pour le programme complet est plus longue, pour aller à la ligne il faut placer un caractère « \ » à la fin, immédiatement suivi d'un retour-chariot :

 

lire-swissprot: lire-outils.o lire-swiss-seq.o \
                choix-fichier.o

La première règle du Makefile joue un rôle particulier, elle mentionne une cible toujours visée. Il convient de mettre en tête soit la règle qui déclenche la construction du programme résultant, soit une règle qui se contente d'énumérer les exécutables souhaités :

 

all: lire-swissprot

 

10.5.1.2 Lignes de commandes

Des lignes de commandes énumèrent les actions à effectuer pour produire la cible. Dans le cas simple que nous allons décrire ce seront des compilations et des éditions de liens, mais toute commande Unix serait acceptable.

Attention, chaque ligne de commande s'exécute dans un environnement propre au sens Unix du terme ; si par exemple nous voulons visiter des sous-répertoires où des commandes make déclencheront d'autres actions, et ainsi de suite récursivement, placer la commande cd appropriée sur une ligne et passer à la ligne suivante pour décrire les actions ne produira pas le résultat escompté. Il faut utiliser la syntaxe du shell pour la composition séquentielle des processus :

 

(cd <mon répertoire> ; ${MAKE}  [<ma cible>])

${MAKE} est une variable standard de make qui désigne make. À cet endroit nous voulons exécuter make, mais si nous écrivons simplement make nous n'aurons la garantie ni que nous invoquons bien la même version de make que celle qui a lancé le Makefile, ni surtout qu'elle va s'exécuter avec les mêmes paramètres. ${MAKE} nous donne ces garanties.10.3

Une ligne de commande commence par un caractère de tabulation. Comme make reconnaît les lignes de commande à ce caractère, il faut prendre garde à ne pas l'oublier pour les lignes de commande et à ne pas en mettre ailleurs. Et avoir remarqué que le « copier-coller » à la souris de votre interface graphique préférée transforme parfois les tabulations en espaces blancs.

La ligne de commande pour la cible lire-outils.o sera :

 

    bigloo -afile access-file.scm -c lire-outils.scm

pour l'édition de liens qui construit le programme :

                bigloo lire-outils.o lire-swiss-seq.o \
                choix-fichier.o -o lire-swissprot

sans oublier la tabulation.

Notre Makefile complet peut donc s'écrire :

Makefile, première version

all: lire-swissprot

choix-fichier.o : choix-fichier.scm
	bigloo -afile access-file.scm -c choix-fichier.scm

lire-swiss-seq.o : lire-swiss-seq.scm
	bigloo -afile access-file.scm -c lire-swiss-seq.scm

lire-outils.o : lire-outils.scm
	bigloo -afile access-file.scm -c lire-outils.scm

lire-swissprot: lire-outils.o lire-swiss-seq.o choix-fichier.o
	bigloo lire-outils.o lire-swiss-seq.o choix-fichier.o \
	-o lire-swissprot

74

 

10.5.1.3 Règles génériques

Les trois règles de construction des objets sont extrêmement similaires. L'esprit de la programmation rechigne devant l'inélégance de cette répétition. Make nous offre la possibilité d'écrire des règles génériques :

 

%.o: %.scm
        bigloo -afile access-file.scm -c $< -o $@

ce qui signifie : pour tout fichier-cible avec un nom de la forme %.o, % étant un préfixe quelconque, le fichier %.scm s'il existe est un bon prérequis, et alors l'action décrite par la ligne de commande suivante est exécutée (si quelque-chose a été modifié dans le prérequis). $< est une variable dont la valeur est la liste des prérequis qui ont déclenché la règle (ici le fichier %.scm), $@ est une variable qui désigne la cible courante. Nous obtenons donc le Makefile suivant :

Makefile

all: lire-swissprot

%.o:	%.scm
	bigloo -afile access-file.scm 	-c $< -o $@

lire-swissprot: lire-outils.o lire-swiss-seq.o choix-fichier.o
	bigloo lire-outils.o lire-swiss-seq.o choix-fichier.o \
	-o lire-swissprot

74

Ceci rédigé il nous suffit de nous placer dans un répertoire contenant les fichiers suivants :

 

Makefile choix-fichier.scm lire-swiss-seq.scm
access-file.scm lire-outils.scm

et taper la commande make suffira à construire notre programme lire-swissprot.

 

10.5.1.4 Règles de suffixes

Makefile avec des règles de suffixes

.SUFFIXES : .o .c .cc .scm
BGL_SOURCES = peuple-iter.scm call-os.scm
BGL_OBJECTS = peuple-iter.o call-os.o
SOURCES = $(BGL_SOURCES) create_db.cc read_db.cc \
	write_db.cc copy_seq.cc \
	xalloc.c liste_db.cc query_db.cc stub.cc
CXX_OBJECTS = read_db.o create_db.o write_db.o \
	copy_seq.o liste_db.o query_db.o stub.o
C_OBJECTS = xalloc.o
OBJECTS = $(CXX_OBJECTS) $(C_OBJECTS) $(BGL_OBJECTS)
EXECUTABLE = iter-os
CCC=/usr/ucb/cxx
BIGLOO = bigloo
CFLAGS= -O3
CPPFLAGS= 
BGL_FLAGS= -Obench -farithmetic -rm
BGL_LDFLAGS= -lstatic-bigloo
CC=gcc

all: $(EXECUTABLE) Makefile

.cc.o: 
	$(CCC) $(CPPFLAGS) -c $< -o $@

.c.o: 
	$(CC) $(CFLAGS) -c $< -o $@

.scm.o:
	$(BIGLOO)  -afile access-file.scm $(BGL_FLAGS) -c $< -o $@

$(EXECUTABLE): $(OBJECTS)
	$(BIGLOO)  $(BGL_LDFLAGS)  -o $@ $(OBJECTS)

clean:
	rm -f $(EXECUTABLE) $(OBJECTS)

74

Une autre façon d'écrire pour make des règles plus générales (et donc moins nombreuses) est d'utiliser les suffixes des noms de fichiers pour déterminer les règles à leur appliquer. En général sous Unix les fichiers source sont suffixés par .scm pour les programmes Bigloo, .c pour les programmes C, .cc pour les programmes C++, .f pour les programmes Fortran, etc. Les compilateurs produisent des fichiers objet suffixés par .o. Ceci nous permet d'écrire des règles selon une syntaxe que nous allons illustrer par un exemple.

Supposons que nous voulions construire un programme nommé iter-os à partir de fichiers source Bigloo, C et C++ :

Ce Makefile commence à ressembler à du travail professionnel. La première ligne définit les suffixes que nous allons utiliser (en fait cette ligne n'est indispensable que pour définir le suffixe des fichiers Bigloo, les autres sont définis par défaut). Les listes de fichiers objet sont définis par des variables pour éviter les définitions multiples qui aboutissent à des incohérences. La variable définie par BGL_OBJECTS = ... est invoquée ultérieurement sous le nom ${BGL_OBJECTS}. Les accolades { et } peuvent être remplacées (sauf cas particulier) par des parenthèses ( et ).

Les drapeaux des compilateurs sont définis par des variables vides par méfiance à l'égard de make, qui souvent prévoit des valeurs par défaut. Les noms des compilateurs sont définis par des variables pour les mêmes raisons que nous utilisions ${MAKE} ci-dessus pour désigner make.

La règle .cc.o décrit comment faire un fichier objet .o à partir d'un fichier source .cc. La variable $< désigne le fichier prérequis à partir duquel on construit la cible. $@ est comme ci-dessus une variable qui désigne la cible courante.

La règle clean permet de détruire les fichiers intermédiaires et finals pour repartir des sources quand on a fait des bêtises. make clean sera la façon de l'invoquer.

Ces quelques paragraphes sont loin d'épuiser le sujet make. Ils devraient vous permettre d'écrire des Makefiles simples et de lire ceux que vous trouverez dans les distributions de logiciels récupérées sur le réseau. Pour de plus amples détails, voir [OT91].

 

10.6 Conserver plusieurs versions d'un programme

Lors de l'écriture d'un programme un peu complexe les étapes du développement et de la mise au point sont multiples. Il arrive que l'on explore une voie de développement qui se révèle peu fructueuse et que l'on souhaite abandonner, ou que ce qui était prévu pour être un logiciel se scinde en deux programmes. Bref, on souhaiterait revenir en arrière et retrouver son programme (constitué de ses nombreux modules) sous une de ses formes antérieures.

Déjà si on a découpé son programme en modules et rédigé des Makefiles on a de meilleures chances de parvenir à faire cette régression. Mais si on s'est contenté de modifier les fichiers avec son éditeur de texte il va falloir re-modifier les programmes source, ce qui est toujours une opération périlleuse. Il serait bien de disposer d'un outil d'archivage informatisé qui pourrait nous restituer des versions clairement identifiées de l'état ancien de nos programmes. Unix nous procure un tel outil, il s'appelle RCS (pour Revision Control System).

Nous allons introduire l'utilisation de RCS en prenant l'exemple du programme des paragraphes précédents.

 

10.6.1 RCS : usage pour un cas simple

RCS peut archiver les versions successives de fichiers dont le contenu est du texte, nous nous en servirons donc pour les fichiers-source, à l'exclusion des fichiers objet et exécutables. De toute façon, la forme à archiver d'un programme est la forme source, parce qu'à partir d'un source on peut reconstituer un exécutable, alors qu'on ne sait rien faire d'un exécutable.

RCS utilise pour gérer les fichiers qu'il archive le système de fichiers de Unix et note dans chacun les lignes modifiées par rapport au précédent archivage du même fichier, il ne s'agit pas de l'accumulation des versions successives.

 

10.6.1.1 Créer une base RCS

Nous allons utiliser RCS pour gérer les programmes sources et les autres fichiers de texte nécessaires à la construction du programme lire-swissprot développé au cours de ce chapitre. Pour commencer il faut se placer dans le répertoire qui contient ces fichiers et y créer un sous-répertoire nommé RCS :

 

# mkdir RCS

 

10.6.1.2 Archiver un programme

Nous allons archiver dans la base fraîchement créée un programme qui nous semble dans un état suffisamment stable :

 

# ci -u choix-fichier.scm                         # 92 $[\mathcal{G}]$97
RCS/choix-fichier.scm,v <- choix-fichier.scm
enter description, terminated with single '.' or end of file:
NOTE: This is NOT the log message!
>> Le module principal de lire-swissprot
>> .
initial revision: 1.1
done

La commande ci (pour check-in) de la ligne $[\mathcal{G}]$ demande l'archivage du fichier choix-fichier.scm dans la base RCS. L'option -u précise que le fichier d'origine ne doit pas être détruit. Il ne sera pas détruit mais ses droits d'accès seront modifiés de telle sorte qu'il ne soit désormais accessible qu'en lecture, ce qui permet de le compiler mais pas de le modifier « à l'insu » de RCS.

On exécutera la commande ci de la même façon pour les autres fichiers, y compris Makefile et access-file.scm.

 

10.6.1.3 Extraire un programme de l'archive RCS

Si maintenant on désire introduire de nouvelles modifications dans le programme choix-fichier.scm, qui en constitueront une nouvelle version, la commande est la suivante :

 

# co -l choix-fichier.scm
RCS/choix-fichier.scm,v -> choix-fichier.scm
revision 1.1 (locked)
done

co (pour check-out) restaure les droits d'écriture sur le fichier. L'option -l précise que l'on souhaite verrouiller (lock) le fichier à son usage exclusif pour éviter que quelqu'un d'autre le modifie en même temps (indispensable pour le travail en équipe).

Une fois les modifications faites, une nouvelle application de la commande ci créera la version 1.2 du programme :

 

# ci -u choix-fichier.scm
RCS/choix-fichier.scm,v <- choix-fichier.scm
new revision: 1.2; previous revision: 1.1
enter log message, terminated with single '.' or end of file:
>> une modification
>> .
done

 

10.6.1.4 Revenir à une version précédente

Si nous voulons travailler à nouveau sur cette version modifiée nous utiliserons la commande co -l comme ci-dessus, mais si notre précédente modification nous inspire des remords nous pouvons interroger RCS au moyen de la commande rlog pour savoir quel est l'historique des versions :


# rlog choix-fichier.scm
RCS file: RCS/choix-fichier.scm,v
Working file: choix-fichier.scm
head: 1.2
branch:
locks: strict
access list:
symbolic names:
comment leader: "\# "
keyword substitution: kv
total revisions: 2; selected revisions: 2
description:
Le module principal de lire-swissprot
----------------------------
revision 1.2
date: 1997/09/03 10:57:28; author: bloch; state: Exp; lines: +2 -0
une modification
----------------------------
revision 1.1
date: 1997/09/03 10:27:35; author: bloch; state: Exp;
Initial revision

et décider par exemple de revenir à la version 1.1 :

 

# co -l1.1 choix-fichier.scm
RCS/choix-fichier.scm,v -> choix-fichier.scm
revision 1.1 (locked)
done

C'est la disponibilité sous Unix d'outils tels que RCS et make qui fait de ce système le préféré des développeurs de logiciels.

 

10.7 CGI en Scheme

Pour utiliser les outils décrits ci-dessus et apprendre à cette occasion de nouvelles choses utiles, nous allons écrire en Scheme un « script CGI ».

Le langage HTML (Hypertext Markup Language) permet de confectionner des pages pour le World-Wide Web. Si on veut que le visiteur de la page puisse dialoguer avec un programme installé sur le serveur, il convient de construire une page d'un type particulier appelé formulaire, et que le programme obéisse à des conventions spéciales pour acquérir les données entrées par l'utilisateur dans le formulaire et pour afficher le résultat sur la page WWW. Un tel programme s'appelle une « CGI » (pour « Common Gateway Interface »). Une explication plus complète sur l'organisation des données et des programmes pour constituer un serveur WWW peut se trouver, par exemple, dans [LPJ$^$95] ou en http://www.univ-rennes1.fr/CRI/documentations/cgi-ismap/JRES.html.

Nous allons réutiliser comme exemple le programme 7.3.4 du chapitre 7 page [*] de construction de triangles de Pascal, et construire un formulaire qui permettra au visiteur de demander la construction du triangle de Pascal pour une puissance n de son choix.

 

10.7.1 Le formulaire

Il n'est pas question de donner ici un cours HTML . Ce langage a l'avantage considérable de s'apprendre par plagiat, puisque toute page HTML exhibe son code source sur simple demande d'un coup de souris. Le nôtre est inspiré d'un article de Pierre Ficheux pour « Linux Magazine » :

Un formulaire HTML

<HTML>
<HEAD>
<TITLE>Test Pascal CGI</TITLE>
</HEAD>

<BODY>
<HR>
<FORM ACTION="/cgi-bin/pascal.cgi" METHOD="POST">
Nombre :<INPUT NAME="Nombre" ><BR>
<P>
<INPUT NAME="SUBMIT" TYPE=SUBMIT VALUE="Calculer">
<INPUT NAME="RESET" TYPE=RESET VALUE="Effacer !">

</P>
</FORM>
</BODY>
</HTML>

74

La ligne :

 

<FORM ACTION="/cgi-bin/pascal.cgi" METHOD="POST">

donne le nom du programme (pascal.cgi) associé au formulaire dont l'exécution sera déclenchée par le bouton déclaré par :

 

<INPUT NAME="SUBMIT" TYPE=SUBMIT VALUE="Calculer">

La fenêtre de dialogue et le nom de la variable entrée par le visiteur sont déclarés par la ligne :

 

Nombre :<INPUT NAME="Nombre" ><BR>

 

 

Figure: La page HTML affichée
\includegraphics[scale=0.8]{../Images/Sep-compile/pascal.epsi}67

 

 

10.7.2 Le programme CGI

 

10.7.2.1 Analyse du message renvoyé par le formulaire

Un formulaire tel que le programme 10.7.1 renvoie les données saisies par le visiteur sous une forme un peu particulière et il va nous falloir écrire quelques lignes de programme pour les analyser (les « parser »), en d'autres termes découper la chaîne renvoyée en champs et retourner au programme de calcul le champ désiré, en l'occurrence celui dénommé Nombre.

Un formulaire HTML tel que celui de la section précédente renvoie la chaîne suivante si on a entré 12 dans le champ Nombre :

 

Nombre=12&SUBMIT=Calculer

Les différents champs sont séparés par des &, chaque champ comporte son nom et sa valeur séparés par le signe =. Nous devons extraire la valeur saisie (ici 12) pour la donner au programme de calcul, ce qui est l'objet du module 10.7.2.1.

Construction du triangle de Pascal

(module pascal
   (main init)
   (import html-parser))

(define (init argv)
   (print "MIME-Version: 1.0")
   (print "Content-type: text/html")
   (newline)
   (pascal
    (string->number
     (le-champ "Nombre"
               (les-champs (read-line (current-input-port)))))))

(define (pascal n)
   (do ((i 0 (+ i 1))
        (L '(1) (ligne-suivante L)))
         ((> i n))
      (print L "<BR>")))
      
(define (ligne-suivante Ligne)
   (let construct ((L Ligne)
                   (L+1 '(1)))
      (if (null? (cdr L))
          (reverse (cons 1 L+1))
          (let ((L+1 (cons (+ (car L) (cadr L))
                           L+1)))
             (construct (cdr L) L+1)))))

74

Décoder les champs du formulaire HTML

(module html-parser
   (export (les-champs html-query)
           (le-champ nom a-liste-champs)))

(define (index chaine caractere)
   (let ((longueur (string-length chaine)))
      (do ((i 0 (+ i 1)))
            ((or (= i longueur)
                 (char=? caractere
                         (string-ref chaine i)))
             (if (= i longueur) #f i)))))

(define (get-champ html-query)
   (let ((longueur (string-length html-query))
         (iperlu (index html-query #\&)))
      (cond ((number? iperlu)
             (cons (substring html-query 0 iperlu)
                   (substring html-query (+ iperlu 1) longueur)))
            (else (cons html-query "")))))

(define (split champ separateur)
   (let ((longueur (string-length champ))
         (ieq (index champ separateur)))
      (cons (substring champ 0 ieq)
            (substring champ (+ ieq 1) longueur))))

(define (unPlus chaine)
;
; Dans le message renvoyé au programme par
; un formulaire HTML, les espaces blancs sont
; remplacés par des signes +. La procédure qui
; suit remet des espaces blancs.
;
   (let ((longueur (string-length chaine))
         (iplus (index chaine #\+)))
      (if (or (zero? longueur)
              (not iplus))
          chaine
          (string-append (substring chaine 0 iplus)
                         " "
                         (unPlus (substring chaine
                                            (+ iplus 1)
                                            longueur))))))

(define (unQP chaine)
;
; Dans le message renvoyé au programme par
; un formulaire HTML, les caractères composés
; de code ASCII supérieur à 127 peuvent être
; représentés par le signe \% suivi de la valeur
; du code ASCII représentés sur deux chiffres
; hexadécimaux ("quoted printable"). La procédure
; suivante décode cette représentation.
;
   (let ((longueur (string-length chaine))
         (ipercent (index chaine #\%)))
      (if (or (zero? longueur)
              (not ipercent))
          chaine
          (string-append (substring chaine 0 ipercent)
                         (string
                          (integer->char
                           (string->integer
                            (substring chaine
                                       (+ ipercent 1)
                                       (+ ipercent 3))
                            16)))   ; Bigloo : base 16
                         (unQP (substring chaine
                                          (+ ipercent 3)
                                          longueur))))))
                         

(define (les-champs html-query)
   (define (champs-aux html-query aliste-champs)
      (let* ((longueur (string-length html-query))
             (champ-reste (get-champ html-query))
             (champ-1 (car champ-reste))
             (reste-query (cdr champ-reste)))
         (if (= longueur 0)
             (reverse aliste-champs)
             (champs-aux reste-query
                         (cons (split champ-1 #\=) aliste-champs)))))
   (champs-aux (unQP (unPlus html-query)) '()))

(define (le-champ son-nom a-liste-champs)
   (cdr (assoc son-nom a-liste-champs)))

74

 

10.7.2.2 Module de calcul

Il est très similaire au programme 7.3.4 donné en page [*], agrémenté des quelques formules magiques nécessaires au bon fonctionnement de HTML. C'est le programme 10.7.2.1.

 

10.7.3 Le Makefile

Makefile pour un programme CGI.

BIGLOO    = bigloo
AFILE     = access-file.scm
BGL_FLAGS = -afile $(AFILE) -Obench -farithmetic -static-bigloo
CIBLE     = pascal.cgi
REPERT_CIBLE = ./bin
REPERT_CGI   = /usr/local/http/cgi-bin/

%.o: %.scm
	@ $(BIGLOO) $(BGL_FLAGS) -c $*.scm -o $*.o

OBJECTS = Src/pascal.o Src/html-parser.o 
SOURCES = Src/pascal.scm Src/html-parser.scm 

all: $(REPERT_CIBLE)/$(CIBLE)

$(REPERT_CIBLE)/$(CIBLE): $(OBJECTS)
	@ echo "Edition de liens..."
	@ $(BIGLOO) $(BGL_FLAGS) $(OBJECTS) \
	  -o $(REPERT_CIBLE)/$(CIBLE)
	@ echo "$(REPERT_CIBLE)/$(CIBLE) construit."
	@ echo "-------------------------------"

$(REPERT_CIBLE):
	@ mkdir -p $(REPERT_CIBLE)

install:
	cp $(REPERT_CIBLE)/$(CIBLE) $(REPERT_CGI)

clean: 
	-rm -f $(OBJECTS) $(SOURCES_C)
	-rm -f *~ Src/*~ Src/*.o Src/*.mco
	@ echo "nettoyage fait..."
	@ echo "-------------------------------"

# destruction aussi des binaires :
cleanall: clean
	-rm -f $(REPERT_CIBLE)/$(CIBLE)

74

Le caractère « @ » au début de certaines lignes de commande indique à make de ne pas les afficher lors de leur exécution. L'option de compilation -farithmetic demande à Bigloo d'utiliser les procédures de l'arithmétique entière plutôt que les procédures génériques, plus lentes (notre programme ne traite que des nombres entiers). L'option -static-bigloo stipule que les programmes de bibliothèque10.4seront incorporés physiquement au fichier exécutable, plutôt que liés dynamiquement, ce qui rendrait le programme exécutable dépendant de la présence de la bibliothèque pour son exécution.

 

10.7.4 Ranger les fichiers

Le secret du fonctionnement d'un serveur WWW est que les bons fichiers soient rangés dans les bons répertoires. En effet, pour des raisons de sécurité essentiellemment, les programmes CGI et les pages HTML sont placés en des endroits convenus. Ces endroits sont précisés, pour un serveur Apache par exemple, par le fichier /etc/apache/srm.conf.

Sur le serveur de l'auteur, la configuration, assez classique, est la suivante : l'arborescence des fichiers HTML a sa racine en /usr/local/http. Les programmes CGI seront en /usr/local/http/cgi-bin/, alias /cgi-bin/. Les pages HTML seront dans /usr/local/http/Public_html/ ou dans ses sous-répertoires.

Respecter les emplacements précisés par le fichier de configuration et ne pas oublier de conférer aux fichiers les permissions adéquates est la première condition pour un fonctionnement correct.

Le serveur de triangles de Pascal sera accessible en : http://hatchepsout.sis.pasteur.fr/Public_html/Scripts/pascal-post.html, service réservé à l'intérieur de l'Institut Pasteur, trop stratégique.

Notez que, par convention, /usr/local/http/Public_html/ se transforme en

http://hatchepsout.sis.pasteur.fr/Public_html/

/usr/local/http/cgi-bin/ en

http://hatchepsout.sis.pasteur.fr/cgi-bin/.

 

10.8 Annuaire associatif en CGI

Juste pour illustrer ce qui précède avec un exemple un peu plus réel, nous allons mettre sous forme de programme CGI l'annuaire associatif du chapitre 8 page [*]. Nous allons conserver deux fonctions : interroger l'annuaire par un nom de personne, ajouter à l'annuaire le doublet constitué du nom d'une personne et de son numéro de téléphone.

Entre deux utilisations, l'annuaire réside dans un fichier sur disque qui comporte une ligne par abonné. Chaque ligne contient, sous forme de chaînes de caractères, le nom et le numéro de téléphone de l'abonné, séparés par un caractère de tabulation (#\tab). Avant chaque usage il faut donc lire ce fichier et construire la table associative qui constitue l'annuaire « actif ». Après chaque ajout il faut reconstituer le fichier à partir de l'annuaire et le réécrire sur disque10.5.

Voici le formulaire HTML :


<HTML>
<HEAD>
<TITLE>CGI Annuaire</TITLE>
</HEAD>

<BODY>
<HR>
<P>
<FORM ACTION="/cgi-bin/annuaire.cgi" METHOD="POST">
<PRE>
Nom :<INPUT NAME="Nom" >
<INPUT NAME="SUBMIT" TYPE=SUBMIT VALUE="Interroger"> 
<INPUT NAME="RESET" TYPE=RESET VALUE="Effacer !">
</PRE>
</FORM>
<FORM ACTION="/cgi-bin/annuaire.cgi" METHOD="POST">
<PRE>
Nom :<INPUT NAME="Nom" > Numéro :<INPUT NAME="Numero" ><BR>
<INPUT NAME="SUBMIT" TYPE=SUBMIT VALUE="Ajouter"> 
<INPUT NAME="RESET" TYPE=RESET VALUE="Effacer !">
</PRE>

</P>
</FORM>
<FORM ACTION="/cgi-bin/annuaire.cgi" METHOD="POST">
<PRE>
<INPUT NAME="SUBMIT" TYPE=SUBMIT VALUE="Afficher">
</PRE>

</P>
</FORM>
<FORM ACTION="/cgi-bin/annuaire.cgi" METHOD="POST">
<PRE>
<INPUT NAME="SUBMIT" TYPE=SUBMIT VALUE="Vider">
</PRE>

</P>
</FORM>
</BODY>
</HTML>

qui devra afficher ceci :

 

 

Figure: La page HTML affichée
\includegraphics[scale=0.6]{../Images/Sep-compile/annuaire-html.epsi}67

 

Le serveur d'annuaire sera accessible en :

http://hatchepsout.sis.pasteur.fr/Public_html/Scripts/annuaire.html

Voici les différents programmes mis en jeu (les procédures pour décoder HTML sont les mêmes qu'à la section précédente) :

Les modules

((annuaire       "Src/annuaire.scm")
 (mod-hash       "Src/hash.scm")
 (manip-annuaire "Src/manip-annuaire.scm")
 (outils         "Src/outils.scm")
 (html-parser
  "../../Sep-compile/Pascal/Src/html-parser.scm"))

74

Le Makefile

BIGLOO   = bigloo
OBJETS_U = Src/outils.o Src/hash.o Src/manip-annuaire.o
OBJET	 = Src/annuaire.o
OBJET_H  = ../../Sep-compile/Pascal/Src/html-parser.o
AFILE     = access-file.scm

%.o:    %.scm
	bigloo -afile $(AFILE)   -c $< -o $@

annuaire: $(OBJETS_U) $(OBJET) $(OBJET_H)
	$(BIGLOO) $(OBJETS_U) $(OBJET) $(OBJET_H) -o ./bin/annuaire

clean:
	rm -f Src/*.o Src/*~ *~ Html/*~

install:
	cp ./Html/annuaire-filtre.html 	\
		/local/http/Public_html/Scripts/annuaire.html
	cp ./bin/annuaire /local/http/cgi-bin/annuaire.cgi
	chmod go+rx /local/http/Public_html/Scripts/annuaire.html
	chmod go+rx /local/http/cgi-bin/annuaire.cgi
	chown www:wwwmaint \
		/local/http/Public_html/Scripts/annuaire.html
	chown www:wwwmaint /local/http/cgi-bin/annuaire.cgi

74

Module et point d'entrée

(module annuaire
   (main init)
   (import outils manip-annuaire html-parser))

(define *N-ELEM* 32)

(define fichier (string-append "/home/bloch/Cours/Algo/Programmes/"
                           "Memoire/Annuaire-cgi/Data/annuaire.dat"))

(define (init argv)
   (print "MIME-Version: 1.0")
   (print "Content-type: text/html")
   (newline)
   (let ((query-list (les-champs (read-line (current-input-port))))
         (cet-annuaire (make-annuaire fichier *N-ELEM*)))
      (cond ((not (file-exists? fichier))
             (display
              (string-append "L'annuaire "
                             (basename fichier)
                             " n'est pas présent")))
            (else (cet-annuaire 'charger query-list)))))
      
(define (make-annuaire fichier n)
  (let ((un-annuaire (make-vector n '())))

     (letrec
        ((self
          (lambda message
             (case (car message)
                ((charger)
                 (charger fichier un-annuaire)
                 (apply self (cons 'dispatch (cdr message))))
                ((dispatch)
                 (let* ((query-list (cadr message))
                        (action (le-champ "SUBMIT" query-list)))
                    (cond ((string-ci=? action "Interroger")
                           (let ((nom (le-champ "Nom" query-list)))
                                     (self 'interroger nom)))
                          ((string-ci=? action "Ajouter")
                           (let ((nom (le-champ "Nom" query-list))
                                 (numero (le-champ "Numero"
                                                   query-list)))
                              (self 'ajouter nom numero)))
                          ((string-ci=? action "Afficher")
                           (self 'afficher))
                          ((string-ci=? action "Vider")
                           (self 'dump))
                          ((string-ci=? action "Fin")
                           (self 'exit))
                          (else (self 'ZZZ)))))
                ((interroger)
                 (let ((nom (cadr message)))
                    (interroger un-annuaire nom)))
                ((ajouter)
                 (let ((nom (cadr message))
                       (numero (caddr message)))
                    (ajouter un-annuaire fichier nom numero)))
                ((donner)
                 un-annuaire)
                ((afficher)
                 (afficher un-annuaire))
                ((dump)
                 (for-each-vector print un-annuaire))
                ((exit) (print "Fin"))
                (else (print "Message inconnu"))))))
         self)))

74

Les programmes utilitaires

(module outils
   (import mod-hash)
   (export (index chaine caractere)
           (split champ separateur)
           (ecrire a-liste flux)
           (afficher . argv)
           (vec!:ajouter-objet doublet vecteur)
           (for-each-vector proc V)))

(define (index chaine caractere)
   (let ((longueur (string-length chaine)))
      (do ((i 0 (+ i 1)))
            ((or (= i longueur)
                 (char=? caractere
                         (string-ref chaine i)))
             (if (= i longueur) #f i)))))

(define (split champ separateur)
   (let ((longueur (string-length champ))
         (isep (index champ separateur)))
      (cons (substring champ 0 isep)
            (substring champ (+ isep 1) longueur))))

(define (ecrire a-liste flux)
   (if (not (null? a-liste))
       (begin
          (display (string-append (caar a-liste)
                                  (string #\tab)
                                  (cdar a-liste))
                   flux)
          (newline flux)
          (ecrire (cdr a-liste) flux))))

(define (afficher . argv)
   (let ((annuaire (car argv))
         (flux (if (null? (cdr argv))
                   (current-output-port)
                   (open-output-file (cadr argv)))))
      (for-each-vector (lambda (L) (ecrire L flux)) annuaire)
      (if (not (null? (cdr argv))) (close-output-port flux))))

(define (vec!:ajouter-objet doublet vecteur)
  (let* ((n (vector-length vecteur))
         (une-case (hash (car doublet) n))
         (elem (vector-ref vecteur une-case)))
    (vector-set! vecteur une-case (cons doublet elem))))

(define (for-each-vector proc V)
  (let ((longueur (vector-length V)))
    (let boucle ((index 0))
       (if (not (= index longueur))
           (begin
              (proc (vector-ref V index))
              (boucle (+ index 1)))))))

74

D'autres procédures...

(module manip-annuaire
   (import outils mod-hash)
   (export (ajouter un-annuaire fichier nom numero)
           (interroger un-annuaire le-nom)
           (charger fichier un-annuaire)
           (stocker annuaire fichier)))

(define (ajouter un-annuaire fichier nom numero)
   (vec!:ajouter-objet (cons nom numero) un-annuaire)
   (stocker un-annuaire fichier))

(define (interroger un-annuaire le-nom)
   (let*  ((n (vector-length un-annuaire))
           (la-case (hash le-nom n))
           (reponse 
            (assoc le-nom (vector-ref un-annuaire la-case))))
      (print (if reponse
                 (cdr reponse)
                 (string-append "Pas d'abonné nommé " le-nom)))))

(define (charger fichier un-annuaire)
   (let ((flux (open-input-file fichier)))
      (let boucle ((ligne (read-line flux)))
         (if (not (eof-object? ligne))
             (begin
                (vec!:ajouter-objet (split ligne #\tab)
                             un-annuaire)
                (boucle (read-line flux)))
             (close-input-port flux)))))

(define (stocker annuaire fichier)
   (let ((flux (open-output-file fichier)))
      (for-each-vector (lambda (L) (ecrire L flux)) annuaire)
      (close-output-port flux)))

74

La fonction de dispersion

(module mod-hash
   (export (hash nom taille-table)))

(define (hash nom taille-table) 
    (remainder 
      (apply + (map char->integer (string->list nom)))
      taille-table))

74

debut.gif (172 octets) boutprec.gif (153 octets) boutsuiv.gif (154 octets)