Previous Up Next

Chapter 1  Généralités

1.1  Les modules Sys et Unix

Les fonctions qui donnent accès au système depuis OCaml sont regroupées dans deux modules. Le premier module, Sys, contient les quelques fonctions communes à Unix et aux autres systèmes d’exploitation sous lesquels tourne OCaml. Le second module, Unix, contient tout ce qui est spécifique à Unix. On trouvera dans l’annexe C l’interface du module Unix.

Par la suite, on fait référence aux identificateurs des modules Sys et Unix sans préciser de quel module ils proviennent. Autrement dit, on suppose qu’on est dans la portée des directives open Sys et open Unix. Dans les exemples complets (ceux dont les lignes sont numérotées), on met explicitement les open, afin d’être vraiment complet.

Les modules Sys et Unix peuvent redéfinir certains identificateurs du module Pervasives et cacher leur anciennes définitions. Par exemple, Pervasives.stdin est différent de Unix.stdin. Les anciennes définitions peuvent toujours être obtenues en les préfixant.

Pour compiler un programme OCaml qui utilise la bibliothèque Unix, il faut faire:

     
   ocamlc -o prog unix.cma mod1.ml mod2.ml mod3.ml

en supposant que le programme prog est composé des trois modules mod1, mod2 et mod3. On peut aussi compiler séparément les modules:

     
   ocamlc -c mod1.ml
   ocamlc -c mod2.ml
   ocamlc -c mod3.ml

puis faire pour l’édition de lien:

     
   ocamlc -o prog unix.cma mod1.cmo mod2.cmo mod3.cmo

Dans les deux cas, l’argument unix.cma représente la bibliothèque Unix écrite en OCaml.

Pour utiliser le compilateur natif plutôt que le bytecode, on remplace ocamlc par ocamlopt et unix.cma par unix.cmxa.

On peut aussi accéder au système Unix depuis le système interactif (le “toplevel”). Si le lien dynamique des bibliothèques C est possible sur votre plate-forme, il suffit de lancer le toplevel ocaml et de taper la directive

     
   #load "unix.cma";;

Sinon, il faut d’abord créer un système interactif contenant les fonctions systèmes pré-chargées:

     
   ocamlmktop -o ocamlunix unix.cma

Ce système se lance ensuite par:

     
   ./camlunix

1.2  Interface avec le programme appelant

Lorsqu’on lance un programme depuis un shell (interpréteur de commandes), le shell transmet au programme des arguments et un environnement. Les arguments sont les mots de la ligne de commande qui suivent le nom de la commande. L’environnement est un ensemble de chaînes de la forme variable=valeur, représentant les liaisons globales de variables d’environnements: les liaisons faites avec setenv var=val dans le cas du shell csh, ou bien avec var=val; export var dans le cas du shell sh.

Les arguments passés au programme sont placés dans le vecteur de chaînes argv:

     
   Sys.argv : string array

L’environnement du programme tout entier s’obtient par la fonction environment:

     
   Unix.environment : unit -> string array

Une manière plus commode de consulter l’environnement est par la fonction getenv:

     
   Unix.getenv : string -> string

getenv v renvoie la valeur associée à la variable de nom v dans l’environnement, et déclenche l’exception Not_found si cette variable n’est pas liée. Exemple:

Comme premier exemple, voici le programme echo qui affiche la liste de ses arguments, comme le fait la commande Unix de même nom.

     
   let echo() =
     let len = Array.length Sys.argv in
     if len > 1 then
       begin
         print_string Sys.argv.(1);
         for i = 2 to len - 1 do
           print_char ' ';
           print_string Sys.argv.(i);
         done;
         print_newline();
       end;;
   echo();;


Un programme peut terminer prématurément par l’appel exit:

     
   val exitint -> 'a

L’argument est le code de retour à renvoyer au programme appelant. La convention est de renvoyer zéro comme code de retour quand tout s’est bien passé, et un code de retour non nul pour signaler une erreur. Le shell sh, dans les constructions conditionnelles, interprète le code de retour 0 comme le booléen “vrai” et tout code de retour non nul comme le booléen “faux”. Lorsqu’un programme termine normalement après avoir exécuté toutes les phrases qui le composent, il effectue un appel implicite à exit 0. Lorsqu’un programme termine prématurément parce qu’une exception levée n’a pas été rattrapée, il effectue un appel implicite à exit 2. La fonction exit vide toujours les tampons des canaux ouverts en écriture. La fonction at_exit permet d’enregistrer d’autres actions à effectuer au moment de la terminaison du programme.

     
   val at_exit : (unit -> unit) -> unit

La fonction enregistrée la dernière est appelée en premier. L’enregistrement d’une fonction avec at_exit ne peut pas être ultérieurement annulé. Cependant, ceci n’est pas une véritable restriction, car on peut facilement obtenir cet effet en enregistrant une fonction dont l’exécution dépend d’une variable globale.

1.3  Traitement des erreurs

Sauf mention du contraire, toutes les fonctions du module Unix déclenchent l’exception Unix_error en cas d’erreur.

     
   exception Unix_error of error * string * string

Le deuxième argument de l’exception Unix_error est le nom de l’appel système qui a déclenché l’erreur. Le troisième argument identifie, si possible, l’objet sur lequel l’erreur s’est produite; par exemple, pour un appel système qui prend en argument un nom de fichier, c’est ce nom qui se retrouve en troisième position dans Unix_error. Enfin, le premier argument de l’exception est un code d’erreur, indiquant la nature de l’erreur. Il appartient au type concret énuméré error (voir page ?? pour une description complète):

     
   type error = E2BIG | EACCES | EAGAIN | ...  | EUNKNOWNERR of int

Les constructeurs de ce type reprennent les mêmes noms et les mêmes significations que ceux employés dans la norme POSIX plus certaines erreurs de UNIX98 et BSD. Toutes les autres erreurs sont rapportées avec le constructeur EUNKNOWNERR.

Étant donné la sémantique des exceptions, une erreur qui n’est pas spécialement prévue et interceptée par un try se propage jusqu’au sommet du programme, et le termine prématurément. Qu’une erreur imprévue soit fatale, c’est généralement la bonne sémantique pour des petites applications. Il convient néanmoins de l’afficher de manière claire. Pour ce faire, le module Unix fournit la fonctionnelle handle_unix_error.

     
   val handle_unix_error : ('a -> 'b) -> 'a -> 'b

L’appel handle_unix_error f x applique la fonction f à l’argument x. Si cette application déclenche l’exception Unix_error, un message décrivant l’erreur est affiché, et on sort par exit 2. L’utilisation typique est

     
   handle_unix_error prog ();;

où la fonction prog : unit -> unit exécute le corps du programme prog.

Pour référence, voici comment est implémentée handle_unix_error.

     
   open Unix;;
   let handle_unix_error f arg =
     try
       f arg
     with Unix_error(errfun_namearg) ->
       prerr_string Sys.argv.(0);
       prerr_string ": \"";
       prerr_string fun_name;
       prerr_string "\" failed";
       if String.length arg > 0 then begin
         prerr_string " on \"";
         prerr_string arg;
         prerr_string "\""
       end;
       prerr_string ": ";
       prerr_endline (error_message err);
       exit 2;;

Les fonctions de la forme prerr_xxx sont comporte comme les fonction print_xxx mais à la différence qu’elles écrivent dans le flux d’erreur stderr au lieu d’écrire dans le flux standard stdout. De plus prerr_endline vide le tampon stderr (alors que print_endline ne le fait pas).

La primitive error_message, de type error -> string, renvoie un message décrivant l’erreur donnée en argument (ligne 16). L’argument numéro zéro de la commande, Sys.argv.(0), contient le nom de commande utilisé pour invoquer le programme (ligne 6).

La fonction handle_unix_error traite des erreurs fatales, i.e. des erreurs qui arrêtent le programme. C’est un avantage de OCaml d’obliger les erreurs à être prises en compte, ne serait-ce qu’au niveau le plus haut provoquant l’arrêt du programme. En effet, toute erreur dans un appel système lève une exception, et le fil d’exécution en cours est interrompu jusqu’à un niveau où elle est explicitement rattrapée et donc traitée. Cela évite de continuer le programme dans une situation incohérente.

Les erreurs de type Unix_error peuvent aussi, bien sûr, être filtrée sélectivement. Par exemple, on retrouvera souvent plus loin la fonction suivante

     
   let rec restart_on_EINTR f x =
     try f x with Unix_error (EINTR__) -> restart_on_EINTR f x

qui est utilisé pour exécuter une fonction et la relancer automatiquement lorsque elle est interrompue par un appel système (voir 4.5).

1.4  Fonctions de bibliothèque

Nous verrons au travers d’exemples que la programmation système reproduit souvent les mêmes motifs. Nous seront donc souvent tentés de définir des fonctions de bibliothèque permettant de factoriser les parties communes et ainsi de réduire le code de chaque application à sa partie essentielle.

Alors que dans un programme complet on connaît précisément les erreurs qui peuvent être levées et celles-ci sont souvent fatales (on arrête le programme), on ne connaît pas en général le contexte d’exécution d’une fonction de bibliothèque. On ne peut pas supposer que les erreurs sont fatales. Il faut donc laisser l’erreur retourner à l’appelant qui pourra décider d’une action appropriée (arrêter le programme, traiter ou ignorer l’erreur). Cependant, la fonction de libraire ne va pas en général pas se contenter de regarder l’erreur passer, elle doit maintenir le système dans un état cohérent. Par exemple, une fonction de bibliothèque qui ouvre un fichier puis applique une opération sur le descripteur associé à ce fichier devra prendre soin de refermer le descripteur dans tous les cas de figure, y compris lorsque le traitement du fichier provoque une erreur. Ceci afin d’éviter une fuite mémoire conduisant à l’épuisement des descripteurs de fichiers.

De plus le traitement appliqué au fichier peut être donné par une fonction reçu en argument et on ne sait donc pas précisément quand ni comment le traitement peut échouer (mais l’appelant en général le sait). On sera donc souvent amené à protéger le corps du traitement par un code dit de «finalisation» qui devra être exécuté juste avant le retour de la fonction que celui-ci soit normal ou exceptionnel.

Il n’y a pas de construction primitive de finalisation try ... finalize dans le langage OCaml mais on peut facilement la définir1:

     
   let try_finalize f x finally y =
     let res = try f x with exn -> finally yraise exn in
     finally y;
     res

Cette fonction reçoit le corps principal f et le traitement de finalisation finally, chacun sous la forme d’une fonction, et deux paramètres x et y à passer respectivement à chacune des deux fonctions pour les lancer. Le corps du programme f x est exécuté en premier et son résultat est gardé de coté pour être retourné après l’exécution du code de finalisation finally y. Lorsque le corps du programme échoue, i.e. lève une exception exn, alors le code de finalisation est exécuté puis l’exception exn est relancée. Si à la fois le code principal et le code de finalisation échouent, l’exception lancée est celle du code de finalisation (on pourrait faire le choix inverse).

Note

Dans le reste du cours, nous utiliserons une bibliothèque auxiliaire Misc qui regroupe quelques fonctions d’usage général, telle que try_finalize, souvent utilisées dans les exemples et que nous introduirons au besoin. L’interface du module Misc est donnée en appendice ??. Pour compiler les exemples du cours, il faut donc dans un premier temps rassembler les définitions du module Misc et le compiler.

Le module Misc contient également certaines fonctions ajoutées à titre d’illustration qui ne sont pas utilisées directement dans le cours. Elles enrichissent simplement la bibliothèque Unix ou en redéfinissent le comportement de certaines fonctions. Le module Misc doit prendre priorité sur le module Unix.

Exemples

Le cours comporte de nombreux exemples. Ceux-ci ont été compilés avec OCaml, version . Certains programmes doivent être légèrement modifiés pour être adaptés à une version plus ancienne.

Les exemples sont essentiellement de deux types: soit ce sont des fonctions réutilisables d’usage assez général, dites «fonctions de bibliothèque», soit ce sont de petites applications. Il est important de faire la différence entre ces deux types d’exemples. Dans le premier cas, on voudra laisser le contexte d’utilisation de la fonction le plus large possible, et on prendra donc soin de bien spécifier son interface et de bien traiter tous les cas particuliers. Dans le second cas, une erreur est souvent fatale et entraîne l’arrêt du programme en cours. Il suffit alors de rapporter correctement la cause de l’erreur, sans qu’il soit besoin de revenir à un état cohérent, puisque le programme sera arrêté immédiatement après le report de l’erreur.


1
Une construction primitive n’en serait pas moins avantageuse.

Previous Up Next