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 |
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 exit : int -> '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.
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(err, fun_name, arg) -> 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).
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 y; raise 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).
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
.
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.