[Atom] [Mail] [Twitter]
Liens : git · hacks · divers · cabale · planète · à propos +
Au menu
\/

Le code et ses raisons : typedef en C

#. Par rz0 dans Le code et ses raisons. Publié le 01.02 2010 à 00:43. 3 commentaires.
<. Le code et ses raisons : goto en C
c débutants idiomes pratique programmation typedef

Voici un vieil article qui traînait au fond de mes dossiers. Comme il semble presque terminé, je me suis dit que j’allais le poster tout de même…

Aujourd’hui donc, au menu, typedef, le bien nommé, qui, très naturellement, permet de définir un type, c’est-à-dire lui associer un nouveau nom. En pratique, toutefois, les choses ne sont pas si simples et il est bon de rappeler quelques règles d’utilisation fondamentales avant d’attaquer la partie plus « philosophique » de notre billet :

  • typedef s’utilise comme une classe de stockage, dans une déclaration classique ;

  • à ce titre, un nom de type ainsi défini ne peut l’être qu’une seule fois dans une unité de compilation (fichier C plus en-têtes inclus) ;

  • enfin, l’identificateur résultant de cette opération est un synonyme du type utilisé : l’ancien et le nouveau type sont utilisables de manière interchangeable.

typedef, pourquoi

Il y a maintes raisons pour lesquelles quelqu’un peut vouloir utiliser typedef, mais il est possible de les ranger en deux familles :

  • celles qui visent à simplifier l’écriture : p.ex. en renommant un pointeur de fonctions, ou en permettant d’omettre le mot-clé struct ou union ;

  • et celles qui introduisent une abstraction (p.ex. un type entier de taille variable selon l’architecture, ou encore un type opaque).

Les deux usages ont leurs défenseurs et, dans une certaine mesure, leurs détracteurs. Si le second emploi ne semble pas contestable en tant que tel, c’est plutôt la nécessité de l’abstraction qui est souvent remise en cause. Et si le premier apparaît comme une puissante manifestation de flemme, l’histoire a montré que de grands noms le soutiennent.

On peut citer à cet effet Bjarne Stroustrup et son C++, dans lequel la déclaration d’une variable de type structure ou union ne requiert pas de mot-clé particulier.

typedef abusif ?

Il y a une école, relativement répandue, notamment dans le milieu scolaire, dont la position est de systématiquement créer un synonyme pour toute structure ou union définie. Et, à l’inverse, il existe certains cercles où cette pratique est violemment condamnée.α Elle est entre autre explicitement déconseillée par le KNF d'OpenBSD.

La raison invoquée est la suivante : utiliser typedef sur une structure engendre une abstraction non toujours voulue. Lorsque celle-ci est conçue comme un simple agrégat de données, et ses membres pensés pour un accès direct, typedef masque la nature du type réel à l’utilisateur.

Dans l’autre camp, la parole est à l’uniformité. En effet, il existe une multitude de niveaux, ou plutôt de formes, d’abstraction, au-delà du simple qualificatif opaque, et son contraire. À partir de là, en quoi est-il légitime de différencier (et à quel niveau cette distinction doit-elle se faire) la structure directement accessible du type abstrait ? Si un objet a une partie de ses membres accessibles, doit-il utiliser le mot-clé struct ?

α : Pour information, je suis également de ceux qui s’y opposent.

Zoom sur l’idée d’abstraction

Bien plus intéressant que les questions de raccourcis d’écriture, la notion d’abstraction est celle qui domine réellement ce débat. S’abstraire, mais de quoi ?

On peut s’abstraire du matériel, tout d’abord, ou plus généralement de la plateforme, du système : la définition de types (le plus souvent entiers) susceptibles de changer d’une architecture à une autre est une pratique commune, connue depuis longtemps sous Unix et en C, en général. Les diverses normes en contiennent elles-mêmes une collection importante : size_t en ANSI-C, off_t dans POSIX, ou encore les fameux intXX_t du C99.

Au niveau au-dessus, on peut vouloir s’abstraire de l’implémentation. Par exemple, il existe de nombreuses façons d’écrire un dictionnaire, mais l’interface basique est la même : on peut y chercher, insérer et supprimer. Nommer son type Dict est une manière courtoise d’indiquer ses intentions : on n’a besoin que de cette interface-ci.

Typiquement, on se retrouve alors avec un type concret, disons struct htable, qui modélise une table de hachage, que l’on souhaite masquer derrière le type Dict. La bonne pratique du C veut que l’on passe en paramètre de nos fonctions un pointeur vers struct htable. La question qui divise est alors la suivante : faut-il utiliser typedef la structure ou le pointeur vers celle-ci ?

Ça a l’air idiot, n’est-ce pas ? Pourtant, il n’y a pas vraiment de bonne réponse à cette question… et pour s’en convaincre, essayons d’entrer dans le raisonnement des deux partis.

La question fondamentale est : faut-il exposer la structure de pointeur ? Si l’on redéfinit le type structure alors on garde la structure de pointeur dans toutes nos interfaces ; si l’on redéfinit le type pointeur, alors les interfaces ne montreront qu’un objet complètement opaque.

Dans un camp

Un premier point de vue est de dire que l’objet se comporte comme un pointeur (car c’est un pointeur !) donc l’analogie est justifiée. On pourrait dire que cela restreint l’implémentation (on pourrait vouloir utiliser une sorte d’ID quelconque). Mais étant donné que la majorité des implémentations raisonnables, en C, utilisent une forme ou une autre d’objet en mémoire dont on ne manipule qu’une référence (le pointeur), la limitation est relativement virtuelle.β

De plus, on peut aussi décréter que les types abstraits se manipulent comme des types scalaires (entiers, pointeurs, etc.) et donc finalement partagent plus ou moins toutes les propriétés de ceux-ci (égalité, affectation, passage en argument, etc.). De ce point de vue, les types obtenus en redéfinissant le type pointeur vers structure se mêlent mieux aux types de base du C, tandis qu’en redéfinissant le type structure lui-même, les opérations disponibles sur celui-ci sont limitées ; en général, il n’est même pas permis de déclarer une variable de ce type (seulement du type pointeur associé).

β : Le seul domaine que je vois où elle pourrait devenir réellement contraignante est la programmation système : le noyau conserve souvent un certain nombre d’informations sous forme de tables indexées, plutôt que de pointeurs vers des objets directement en mémoire, ces objets n’étant pas forcément situés (entièrement) dans l’espace mémoire de l’utilisateur.

Cependant, il peut arriver qu’il soit plus commode de manipuler les indices au lieu des pointeurs eux-mêmes, notamment si l’identificateur entier est réutilisé pour indexer d’autres structures.

Et dans l’autre

Cependant, si l’on opte pour le typedef simulant un type scalaire, quelque part, on dissimule une information capitale : chaque objet de ce type en cache un autre, « plus gros ». En d’autres termes, ce ne sont que des références.

À l’inverse, en conservant explicitement la syntaxe des pointeurs, cette notion est clairement identifiée et passer un pointeur à une fonction souligne le potentiel de celle-ci à modifier une donnée tierce, masquée par le pointeur.

À cela, les défenseurs de l’autre camp pourront rétorquer qu’une structure de données complexe cache en général elle-même de multiples niveaux d’indirections, et qu’ainsi il est tout aussi trompeur de faire croire à l’utilisateur que c’est l’objet directement pointé qui sera sujet à modification : bref, que l’on ne présente ainsi que le haut de l’iceberg.

Bien sûr, il est facile de considérer que la syntaxe n’est qu’un indice conventionnel signalant au programmeur que de la mémoire indirectement manipulée entre en jeu. Tout comme il est facile d’y opposer le fait qu’à l’usage cela est invisible (le typage étant réservé au prototype) et que la documentation a déjà à sa charge ce genre de détails, ou encore que d’autres techniques sont plus appropriées (p.ex. l’usage d’un qualificateur de fonctions pure tel qu’on le trouve dans GCC). On pourrait contre-attaquer en remarquant que le typage devrait alléger la documentation, à quoi on pourrait répondre que celui du C est de toute façon trop faible pour cela… J’aime débattre avec moi-même ; tout cela n’est qu’opinion, au final. :)

Conclusion

Malgré toutes ces années à coder en C, je ne suis pas parvenu à forger un avis prononcé sur la question. J’oscille moi-même entre les diverses opinions, à mesure que de nouveaux argumentaires en faveur d’une pratique ou d’une autre me parviennent…

Ma position actuelle, si elle intéresse quelqu’un, relève d’un point de vue que je qualifierais de « minimaliste » : je me suis fixé, pour mes nouveaux programmes, de n’utiliser typedef que lorsque j’y vois un intérêt immédiat (p.ex. pour la portabilité des types entiers). Le raisonnement derrière cette politique est que l’abstraction est, en C et au niveau où je travaille, relativement pauvre, et qu’un changement d’implémentation ne saurait généralement se passer d’une altération de l’API tout entière. Abstraire seulement le typage ne suffit pas. Abstraire davantage entraîne un risque de complexité accrue et de performances moindres.

Mais je ne suis pas pour autant fondamentalement opposé à l’usage de typedef dans d’autres scénarios. Une bibliothèque de très haut niveau qui reposerait sur la manipulation d’objets (au sens POO) pourrait choisir de nommer ses classes avec typedef. Une telle convention aurait du sens.

Je concluerai donc sur ces mots : il faut ainsi prendre en compte le contexte et établir des conventions. Pour rabâcher le principe de style fondateur et bien connu : après le bon sens, la cohérence doit primer.

[ tag:blog.huoc.org,2009:posts/37 ]
Voir les commentaires · Commenter

/\ \/

Pourquoi attacher tant d'importance au typage ?

#. Par bluestorm. Publié le 30.12 2009 à 13:34. 15 commentaires.
débutants langages méditation typage

La réponse, enfin dévoilée ! Bluestorm Origins: The Typing Mystery, maintenant dans les blogs (well, le nôtre du moins :-°). —rz0

Il est arrivé une bonne chose à mon dernier billet : des gens m’y ont répondu en commentaire. Je les remercie tous chaudement. Ce n’est pas complètement rationnel, mais je trouve ça vraiment appréciable, et ça m’a motivé pour retenter l’expérience de ces billets « en bref ». Mais pas pour cette fois : c’est intéressant quand on a différentes choses à dire, et il vaut mieux que ce ne soit pas trop souvent, sinon ils deviennent de plus en plus vides et répétitifs.

Dans les commentaires, que j’ai lus avec intérêt (j’ai même répondu à certains, c’est fou les blogs), se trouve une question qui m’a donné envie d’en faire un billet à part entière. C’est le commentaire de robocop :

Il y a quelque chose qui ressort quand même pas mal dans nombre de tes articles, c’est l’importance que tu attaches aux types. J’ai la facheuse impression d’être passé à coté de quelque chose de fondamental, pour moi, les types ce n’est qu’une vérification supplémentaire qui est fait à la compilation et ou à l’interpretation, mais à la façon dont tu en parles, le système de type semble le cœur même d’un langage de programmation.

Si comme je le pressens, je loupe vraiment quelque chose, pourrais-tu disserter rapidement là-dessus ?

En une phrase : pourquoi, parmi toutes les caractéristiques d’un langage de programmation, attacher autant d’importance à son système de typage ?

J’ai été surpris de ne pas pouvoir répondre à cette question sur le moment. J’ai pris du temps pour y réfléchir, et je pense avoir maintenant une réponse satisfaisante (mais pas rapide).

J’ai apprécié les commentaires de la carotte qui, avant l’ours, a relu ce texte et m’a permis de l’améliorer.

Rappels

Je ne compte pas expliquer dans ce billet les divers avantages et inconvénients, existants ou fantasmés, des différents systèmes de typage ; ce n’est pas le sujet. Je me contenterai ici d’un bref rappel de deux notions qui reviennent souvent quand on discute les systèmes de typage, et dont je reparlerai par la suite :

typage statique

désigne les systèmes de typage qui vérifient les types dans un programme avant de l’exécuter, typiquement au moment de la compilation. Les erreurs de typage, s’il y en a, sont signalées pendant cette phase et le programme n’est pas exécuté tant qu’il n’est pas correctement type. En général, à l’exécution, on n’a plus à se soucier des types et on peut raisonner comme s’ils n’existaient plus.

typage dynamique

désigne les systèmes de typage qui vérifient les types pendant l’exécution du programme. Avant chaque opération que le programme doit exécuter, il vérifie que les types des données sont correctes ; cela implique que le programme a accès à ces types quelque part : ils sont stockés dans la mémoire du programme pendant son exécution.

Ce sont deux méthodes très différentes qui ont un but commun : empêcher l’ordinateur d’effectuer des opérations mal typées (« mélanger des patates et des carottes »). Chaque façon de faire a ses adeptes, et les deux camps se livrent des guerres terribles, depuis plusieurs générations de langages de programmation — et, bientôt, d’informaticiens. Le point important ici est que les langages mettant en place l’une ou l’autre de ces méthodes ont un système de typage. Il sera plus ou moins bien défini, selon la précision de la définition du langage lui-même, mais dans tous les cas on peut s’y intéresser.

J’ai déjà parlé du typage dans un article publié sur le SdZ : Le typage, présentation thématique et historique. Je n’en suis pas complètement satisfait : je le trouve trop jeune et un peu naïf, et j’essaierai peut-être de le reprendre quand je saurai mieux ce que je veux dire.
Pour les débats autour du choix d’un système, j’aime bien l’article What To Know Before Debating Type Systems. Il est peut-être un peu biaisé en faveur des systèmes statiques, et un peu trop technique, mais il apporte une vue d’ensemble assez intéressante. C’est une bonne base si vous voulez vous lancer dans le débat : statique ou dynamique ?

Cependant, ce que je voudrais discuter ici, ce n’est pas le choix de telle ou telle méthode de typage, mais plutôt la place du typage au sein d’un langage de programmation, relativement aux autres aspects du langage.

Types comme information

Imaginez que vous voulez écrire un petit morceau de programme dans le langage de votre choix, qui soit réutilisable par d’autres programmeurs. Vous devez leur décrire ce que vous avez fait. Vous commencez par leur dire ce que fait ce bout de programme (« c’est une compression selon le codage de Huffman »), et vous allez décrire la manière dont ils doivent interagir avec ce bout de programme pour profiter de ses bienfaits : « on lui donne un texte, et il le renvoie compressé, avec le dictionnaire de compression ». Avec cette simple phrase, vous avez déjà transmis des informations de typage : on lui fournit un texte, donc en général une chaîne de caractère. S’il a besoin de plus d’informations (par exemple sur la nature du dictionnaire de compression), vous donnerez une description plus précise qui contiendra autant d’informations de typage supplémentaires.

Cette façon de procéder est commune à l’ensemble des programmes et langages. Choisissez un langage que vous appréciez et qui a une documentation utilisable : elle contient (plus ou moins explicitement) des informations de typage. Par exemple, j’ai choisi une page au hasard de la documentation du langage PLT Scheme, réputé « non typé », dans laquelle on peut trouver le texte (code ?) suivant :

(directory-exists? path) → boolean?
  path : path-string?

C’est tout à fait du typage : on décrit une fonction qui vérifie qu’un répertoire existe dans un système de fichier. Cette fonction accepte un chemin de type path-string, et renvoie un boolean. Les points d’interrogation soulignent un détail intéressant : les noms choisis pour décrire les types sont aussi des fonctions (prédicats) que l’on peut utiliser pour vérifier qu’une valeur est bien du type donné. Par exemple la documentation de la fonction path-string? contient :

(path-string? v) → boolean?
  v : any/c

Ces prédicats sont utilisés au sein de ces fonctions pour vérifier que l’utilisateur ne donne pas n’importe quoi, et la vérification de validité est donc effectuée à l’exécution : c’est une forme de typage dynamique.

Ce ne sont pas les seules explications concernant ces fonctions, il y a aussi du texte en langue naturelle qui explique leur usage. Certaines documentations ne mettent aucune information de typage aussi claire (je pense à Python par exemple), il faut aller la pêcher dans le texte explicatif, ou encore elle peut être implicitement contenue dans les noms des paramètres, qui respectent des conventions facilitant cette tâche.

L’idée importante c’est qu’au fond, un type c’est essentiellement une information sur une expression du langage de programmation.

Le langage qui parle de lui-même

Cette idée des types comme informations est très large, car elle doit faire cohabiter les notions de typage très différentes selon les langages. Elle est peut-être même un peu trop étendue, puisqu’elle désigne tout et n’importe quoi, et que je ne vais pas avoir de mal à vous convaincre que vous avez besoin d’informations pour coder correctement. On peut affiner un peu plus.

Quand on parle du système de typage d’un langage, on n’a pas en tête les conventions de nommage des paramètres utilisées dans sa documentation. On parle en général d’une manière de décrire ces informations qui fait partie du langage lui-même : le système de typage, c’est la partie du langage qui permet de donner des informations sur les valeurs.α Je désigne cette partie par le terme langage de types : par exemple le langage des types du C est un sous-ensemble du langage C qui contient les types de base (int, float..), des modulateurs (unsigned), enum, struct, …, des pointeurs, tableaux, et même des types fonctionnels.

En d’autres termes, le système de typage, c’est un méta-langage, qui détient le pouvoir d’expression du langage sur lui-même. Plus le système de typage est expressif, plus on peut dire de choses sur un programme au sein de ce programme, plutôt que dans la documentation ou en commentaire.

Vous comprenez sans doute pourquoi, avec ce point de vue, j’attache tellement d’importance, quand je m’intéresse à un langage de programmation, à son système de typage : c’est une partie toute fondamentale puisqu’elle détermine la capacité des programmes dans ce langage, en plus de faire quelque chose d’utile, à transmettre des informations sur ce qu’il fait.

Il faut cependant bien remarquer que, bien que très importante, elle n’est pas non plus indispensable : on peut très bien imaginer des langages qui ne permettent de formuler aucune information sur les programmes, mais qui ont d’autres qualités qui en font des langages intéressants. Je pense par exemple à Forth, un des premiers langages à pile haut niveau.

α : On pourrait croire à une circularité ici : si les types parlent du langage tout en en faisant partie, alors les types parlent des types et le monde s’écroule. En réalité, la plupart des langages de types ne sont pas suffisamment expressifs, et ne parlent que d’une partie restreinte du langage (qu’on appelle souvent les valeurs). Mais il existe effectivement des langages (par exemple Coq) dans lesquels les types ont eux aussi un type, qui a à son tourβ un type… Il peut bien sûr exister un nombre infini de types différents, bien qu’un programme donné ne les utilise pas tous en même temps.

β : « Turtles all the way down ! » ;-)

Système de typage et vérification

La définition actuelle reste assez molle : est-ce que les prédicats de PLT Scheme constituent un système de typage ? Ils font partie du langage, mais leur utilisation est laissée au bon vouloir de l’utilisateur : il peut aussi implémenter sa fonction sans faire aucune vérification. On pense en général plutôt à des systèmes dans lesquels le typage est obligatoire, et vérifié automatiquementγ par le compilateur. Ce qui est sûr c’est que les informations implicites de la documentation Python ne rentrent pas dans ce cadre : le typage en Python, c’est ce que le compilateur sait vérifier de lui-même et afficher dans un message d’erreur : l’appartenance à une classe, l’existence d’une méthode donnée, etc.

Quand on décrit le système de typage d’un langage, il faut donc aussi préciser la manière dont ces informations sont vérifiées, ou plus généralement exploitées par le reste du langage. Il y a d’autres distinctions que la vérification statique ou dynamique que j’ai évoquée. Par exemple, certains langages permettent des conversions implicite d’un type vers un autre, ou le choix dans une fonction d’un comportement différent selon le type des paramètres ; on parle dans ce dernier cas de polymorphisme ad-hoc, car il permet de gérer chaque cas particulier séparément.

Autres points à considérer pour son système de typage

L’expressivité et les conditions de vérification ne sont pas les seuls points à débattre d’un système de typage.

On peut s’intéresser à l’emplacement, au sein des programmes de ce langage, des annotations de typage. Certains langages demandent au programmeur très peu d’indications de typage, car ils peuvent les reconstruire à partir des informations déjà présentes dans le langage : c’est l’inférence de type. Plus le langage des types est expressif, plus cette reconstruction est difficile. Les annotations peuvent encombrer le programme si elles sont trop nombreuses, mais placées judicieusement elles peuvent augmenter la lisibilité. Plus généralement, les informations de typage, qu’elles soient produites par le programmeur ou ses outils, renforcent la documentation et la modularité entre les différentes partie du programme : donner le type des valeurs d’une bibliothèque logicielle facilite beaucoup son utilisation par d’autres programmeurs, surtout quand il est assez expressif pour traduire l’ensemble des contraintes d’utilisation de cette interface.

Un dernier exemple, plus sophistiqué, la propriété de séparation : dans certains langages (dont OCaml), le typage est une phase totalement séparée de l’évaluation, dans le sens où une fois qu’on a un programme bien typé, on peut oublier complètement tout ce qui concerne le typage, et cela ne change absolument rien au déroulement du programme.δ Dans d’autres langages, en particulier ceux qui possèdent le polymorphisme ad-hoc, il n’y a pas cette séparation. L’avantage de cette propriété est qu’elle donne un langage plus simple à comprendre, avec des phases plus indépendantes. L’inconvénient est qu’elle est contradictoire avec certaines fonctionnalités que les concepteurs du langage peuvent avoir envie de fournir : imposer la séparation peut réduire l’expressivité. Il n’y a pas de bon et de mauvais choix dans l’absolu, cela dépend des besoins du langage, mais c’est un choix auquel les systèmes de typage sont confrontés. On peut enfin imaginer des distinctions plus fines, avec une partie du langage qui brise la séparation, mais qui est traduit dans une première phase vers un langage plus simple, bien séparé. C’est comme cela par exemple qu’on manipule les type classes de Haskell quand on veut en donner des définitions très précises (formelles).

γ : En fait, PLT Scheme permet aussi la vérification des types ; je suis impressionné par cette implémentation de Scheme.

δ : Je parle ici de la sémantique du langage, c’est à dire d’une définition un peu abstraite du comportement (ou sens) des programmes. Ce n’est pas une question d’implémentation : si on crée un compilateur pour un langage avec cette séparation, on peut très bien choisir de faire des optimisations en raisonnant sur le type des variables, par exemple en changeant leur représentation mémoire (boxing, etc.), du moment que cela crée aucune différence de comportement du programme observable par le programmeur.

Contraintes et limitations

Si le langage des types est peu expressif, et la vérification très rigoureuse (impossible de définir des termes non typés), le typage peut vite devenir une contrainte. Par exemple, le langage des types du C ne permet pas d’exprimer le polymorphisme paramétrique (une fonction qui marche sur tous les types ayant structure donnée) ; on fait avec, en utilisant une sorte de type-à-tout-faire, void *, qui permet d’abandonner à la fois l’information de typage et la restriction qui y est associée.

Cette limitation que l’on rencontre n’est pas seulement un artefact de systèmes pratiques, mais se trouve déjà dans la théorie des langages. Par exemple, le lambda-calcul simplement typé de base ne permet pas d’écrire des programmes qui ne terminent pas : tous les programmes bien typés terminent en un temps fini. Ça a ses avantages, mais aussi un inconvénient : ce langage n’est pas Turing-complet. En gros, il y a des fonctions qu’on ne peut pas écrire avec. On peut lever cette limitation,ε soit en enrichissant le langage des types (polymorphisme plus types récursifs par exemple), soit en ajoutant des constructions au reste du langage (opérateur de point fixe ou définitions de fonctions récursives).

Les langages au typage dynamique ont en général un langage de type assez faible, mais ont du coup l’avantage de ne poser que très peu de contraintes : comme ils ne vérifient pas grand chose au niveau du typage (par exemple, ils ne vérifient pas le fait que les deux branches d’une condition renvoient le même type), ils ne posent pas non plus de contraintes gênantes. C’est un autre choix qui peut se justifier ; par exemple, certains motifs de conception de la programmation objet sont notoirement difficilesζ à typer correctement (ils demandent un langage très expressif), les langages dynamiques n’ont pas ce problème.

ε : Limitation qui n’en est pas toujours une : on peut faire des choses très intéressantes avec des langages non Turing-complets (par exemple les expressions régulières/rationnelles, quand elles le sont vraiment, ou Coq).

ζ : Jacques Guarrige, Code reuse through polymorphic variants

The idea for this example originally comes from a post by Phil Wadler on the Java-Genericity mailing list W+98, in which he proposed a solution to the Expression Problem, that is the problem of extending a variant type with new constructors without recompiling code for old ones. A similar problem and solution can be found in KFF98, but untyped. It appeared later that Wadler’s solution, which already supposed an extension of Generic Java, itself an extension of Java, could not be typed. Didier Rémy, Jérôme Vouillon and myself finally came up with a typable solution in an object-oriented extension of the 3rd order typed lambda-calculus, which was later checked correct by Haruo Hosoya in F-omega- sub-rec. On the other hand, I could provide a much shorter solution in about 15 lines of Objective Label using polymorphic variants, which were merged later in Objective Caml. The solution used only standard ML-polymorphism, which is a weakened 2nd order typed lambda-calculus, and can be inferred automatically.

Pour bien typer, il faut bien comprendre

Il arrive donc assez souvent que des idées sur de nouvelles façon de programmer soient développées dans un cadre non typé, et qu’elles trouvent seulement ensuite leur place dans un système de typage, comme par exemple le transfert du langage de programmation Oz, langage dynamique qui a popularisé certains paradigmes de concurrence, à AliceML, langage typé qui reprend une partie de ces fonctionnalités.

Cela ne veut pas dire que le cadre non typé est plus fructueux : exprimer précisément une information statique sur ces nouvelles idées permet souvent de les éclairer d’un jour nouveau. Par exemple, la recherche de sémantiques statiques pour les langages à effets de bords a donné lieu à l’idée de monade, qui est utilisée en Haskell pour représenter, entre aures, les effets de bords, et qui a pour conséquence de modifier le type des programmes en y rendant ces effets explicites. On y a gagné des outils intéressant, un point de vue nouveau, et une meilleure compréhensionη de la programmation impérative.

Malheureusement, la programmation est un sujet compliqué dont nous n’avons fait qu’égratigner la surface : il y a beaucoup de choses qu’on ne sait pas encore expliquer de manière simple et précise à la fois, par exemple la concurrence, les manipulations mémoire, la communication avec les programmes extérieurs, etc. L’explication et le typage précis de manières avancées de programmer est un sujet de recherche actif, dont on espère qu’il nous apportera des réponses ; en attendant, il faut se contenter de typages correspondant à l’état des connaissances aujourd’hui, et qui ne sont pas forcément aussi expressifs qu’on le voudrait.

η : Rz0 me signale que cette « meilleure compréhension » n’a pas du tout affecté la pratique des programmeurs qui, sur le terrain, utilisent des effets de bords, et qu’il est donc un peu « osé » de présenter les monades comme une réussite de ce point de vue. C’est en effet un peu plus compliqué que ça : notre compréhension théorique des effets de bords s’est améliorée, mais on a aussi découvert que les monades sont des outils trop généraux, qu’elles s’appliquent à beaucoup de choses qui ne sont pas considérées habituellement comme des effets de bords, et qu’elles ne sont pas toujours assez précises.

Pour la gestion de la mémoire par exemple, une monade ne peut pas raisonner, au niveau du typage, sur l’étendue des zones mémoires affectées par un programme : la monade saura dire de deux programmes qu’ils modifient la mémoire, mais pas si les zones de mémoire qu’ils utilisent sont disjointes ou s’il y a des interactions entre les deux. On a besoin de ces propriétés plus fines, et on est en train de développer des outils plus spécialisés et plus complexes, comme les systèmes d’effets et les logiques de séparation.

Enfin, même si ces idées mettent du temps à sortir de leur cadre théorique pour atterrir dans la boîte à outils des programmeurs, elles restent présentes dans l’industrie, avec par exemple les outils d’analyse statique de programmes, qui suivent d’assez près les progrès de la recherche dans ce domaine. L’arrivée du concept dans un langage de programmation grand public est la dernière étape (après l’entrée dans les langages expérimentaux comme DDC ou ATS), qui se fera plus tard, ou peut-être pas du tout, si l’on constate que les bénéfices de ces méthodes ne compensent pas l’ajout en complexité.

η´ : Et puis d’abord, si l’exemple des monades ne vous plaît pas, en voici un autre : les continuations. Le comportement dynamique de call/cc est bien connu, mais l’étude de son lien avec le typage et la logique classique est au moins aussi intéressante.

Puissance du typage et complexité du langage

Cette difficulté que rencontrent les concepteurs de langage de programmation se retrouve aussi, à plus petite échelle, quand un programmeur essaie de donner un type statique à son programme. Quand on code dans un langage dynamique, on ne se soucie essentiellement que d’une chose : il faut qu’au moment de l’exécution, notre programme marche. Donner un type très expressif à un programme, cela peut demander de rendre explicites plus d’informations : qu’est-ce que notre programme fait, comment le fait-il, pourquoi cette fonction est-elle correcte ? L’ordinateur peut en deviner une partie (c’est le principe de l’inférence des types), le programmeur en connaît une partie (par exemple ce qui est un entier, et ce qui est un booléen dans son programme), mais il y a parfois des programmes compliqués pour lesquels ce n’est pas aussi simple : donner un type très précis impose une bonne compréhension du programme.

Bien entendu, même dans les langages typés, un programmeur a toujours la possibilité d’utiliser des représentations moins expressives, et qui demandent donc moins de réflexion. Par exemple, au lieu d’avoir un type de donnée spécialisé pour décrire les dates (jours, mois, heure, etc.), on peut par exemple utiliser des chaînes de caractères. C’est généralement une mauvaise idée : un effort au départ pour décrire et utiliser un type plus précis permet ensuite des manipulations plus sûres et plus directes.

Il y a cependant un point à ne pas oublier quand on cherche des langages de types expressifs : ils rendent nécessairement les langages plus complexes. Quand des débutants en Ada doivent comprendre les différences entre les paramètres in, out, et in out, ils peuvent penser qu’un langage faisant moins de fioritures aurait été plus simple. On peut se demander s’il vaut mieux concevoir des langages simplifiés pour les débutants et des langages moins simples mais ayant d’autres qualités pour les experts, ou si une taille unique peut convenir à tout le monde. Cette question intéressante est encore ouverte, mais il est clair que la simplicité (réelle ou perçue) d’un langage joue dans son adoption par les programmeurs, et qu’on ne peut pas se permettre de concevoir des systèmes de typage arbitrairement compliqués en espérant qu’ils vont dominer le monde.

Tous les langages sont typés, parfois sans le savoir

Ma définition est suffisamment large pour qu’on puisse trouver un système de typage à tous les langages, même ceux qui se proclament traditionnellement « non typés ». Les cas les plus extrêmes sont les langages dont les types sont tellement pauvres qu’ils ne peuvent essentiellement rien dire d’intéressant. Par exemple, en Brainfuck, le système de typage est vide : on ne peut rien dire sur les programmes, et d’ailleurs il n’y a rien à dire puisqu’ils ne manipulent jamais rien d’autre qu’une bande mémoire dont une case est pointée.

Plus utiles, les assembleurs sont aussi des langages avec des typages très pauvres : il y a en général des registres, des valeurs immédiates et des adresses mémoires (et par exemple des labels à plus haut niveau), et des considérations de taille, alignement, etc., qui sont assez riches mais ne nous apprennent pas beaucoup sur le comportement à plus haut niveau du code, tant qu’on ne le munit pas d’un système de typage plus expressif.θ On peut encore citer le langage de Matlab/Octave, qui ne connaît essentiellement que les matrices de scalaires, ce qui peut parfois donner de très mauvaises surprises.

θ : Voir par exemple les travaux de Greg Morrisset ou George Necula sur les assembleurs fortement typés.

La réaction de rz0 de sujet souligne un autre aspect du problème :

À mon avis, un point intéressant, et pas du tout développé dans la plupart des visions « haut niveau » de l’assembleur, c’est les contraintes sur les adresses ou la mémoire : représentation, décalage, facteur, taille, alignement, granularité, etc. Je considère que c’est du typage aussi, et c’est du typage « utile », dans le sens où si les données fournies aux opérandes sont mal typées, soit ça n’assemble pas (p.ex. parce qu’il n’y a pas assez de bits pour représenter l’adresse entièrement), soit ça balance une exception au runtime (si on passe un registre et que sa valeur ne vérifie pas les conditions de typage). Bref, je trouve que ce n’est pas « rien à dire ».

Mélanger typage statique et typage dynamique ?

Cette idée de décrire les langages peu typés comme possédant un petit nombre de « types poubelles » ne donnant que très peu d’information peut en fait être utilisée pour mélanger au sein d’un langage le typage statique et le typage dynamique : on peut rajouter ces types poubelles à l’éventail des types du langage.

Il s’agit de permettre de « perdre » localement l’information de typage, en transformant toutes les valeurs, quelle que soit leur type, vers un même type dynamic. On peut ensuite utiliser une fonction d’extraction pour récupérer des valeurs bien typées dans les parties du programme qui n’ont pas besoin de typage dynamique. Cette idée est vieille comme le monde, il s’agit de faire un cast (conversion d’un type vers un autre) au détriment de la sûreté du typage, et elle est présente dans un grand nombre de langages de programmation.

Pour que cette méthode ne nuise pas trop à la sûreté de l’application dans son ensemble, il faut réutiliser les méthodes de vérification de typage des langages dynamiques : quand on envoie la valeur vers le type poubelle, on lui associe une autre valeur runtimeι qui décrit son type. Ensuite, quand on convertit à nouveau la valeur dynamique vers un type plus expressif du langage, on peut insérer un test pendant l’exécution qui vérifie que le type d’arrivée est bien compatible avec le type dynamique stocké avec la valeur.

Au-delà du dynamic_cast de C++, les langages fortement typés ont fait des tentatives dans cette direction. La solution la plus simple est de demander à l’utilisateur de ces fonctions d’insertion et extraction dans le type dynamique de manipuler lui-même les types dynamiques, en utilisant une bibliothèque logicielle dédiée. On peut utiliser des fonctionnalités du langage pour simplifier cette manipulation (par exemple des macros syntaxiques, des types fantômes,κ ou des type classes), et mettre en place des bibliothèques logicielles apportant ces facilités aux programmeurs.

Ça n’est cependant pas suffisant : si l’on se contente d’une bibliothèque logicielle à part, le langage ne garantit pas la cohérence entre l’information statique de typage dont il dispose, et les valeurs dynamiques construites par l’utilisateur ou sa bibliothèque spécialisée : même quand elle est fortement typée (les manipulations à l’extérieur de la bibliothèque sont sûres) il faut faire confiance à l’auteur de cette bibliothèque de valeurs dynamique pour n’avoir pas fait d’erreurs de conversion à l’intérieur.

ι : Un anglicisme qui désigne le « moment de l’exécution », à nuancer de compile-time ou parse-time, et dont j’ai beaucoup de mal à me débarrasser.

κ : C’est ce que j’ai utilisé dans Macaque ; j’avais besoin d’une méthode sûre pour traiter la perte de typage des données passant par le serveur SQL (qui n’utilise que des chaînes de caractères), et je me retrouvé, sans m’en rendre compte au départ, avec du typage dynamique fait maison.

Si l’on veut plus de simplicité et de sûreté, il faut intégrer cette fonctionnalité directement au sein du langage de programmation. La communauté CAML a fait partie des pionniers de ce domaine avec Dynamics in ML, une extension du langage proposant des valeurs dynamique même pour les types polymorphes. Les langages fonctionnels Clean et AliceML proposent aussi cette fonctionnalité.

On peut remarquer que cette approche (construction automatique, par le langage directement, de la description runtime de type) brise la propriété de séparation, que j’ai évoquée précédemment, entre la phase de typage et la phase d’exécution.

Ces méthodes permet de concevoir des langages typés statiquement mais qui acceptent de « laisser la main » gracieusement quand on atteint les limites de leur expressivité. Cela permet de résoudre les problèmes des constructions trop difficiles à typer mentionnés auparavant, sans abandonner trop de typage. Malheureusement, en pratique la mise en place de tels systèmes au sein d’un langage peut compliquer sensiblement son implémentation efficace, et elles ne sont donc pas très répandues. C’est encore une fois un sujet de recherche actif. Il y a des langages qui essaient de rendre encore plus transparente la frontière entre les mondes dynamiquement et statiquement typés. Cela demande de nouvelles méthodologies de typages, et tout une zoologie se crée autour du sujet : soft typing, gradual typing, blame typing, hybrid typing, …

En attendant, les programmeurs se contentent en général de méthodes plus indirectes, en utilisant des valeurs dans des types moins expressifs mais plus souples, comme le font sans le savoir les utilisateurs de langages dynamiques.

Les types ne font pas tout

Ce billet a beaucoup parlé des types, et de pourquoi le système de typage est un ingrédient majeur d’un langage de programmation. Il ne faut pas non plus oublier que ce n’est pas le seul, et qu’une grande partie de l’intérêt des langages ne vient pas du tout de leur système de types.λ

L’un des livres sur la programmation qui m’a beaucoup plu est le CTM : Concepts, Techniques and Models of Computer Programming. C’est un livre vraiment formidable, qui a étendu mon horizon en matière de techniques de programmation, et qui pourtant ne parle pas du tout, ou quasiment pas, de typage.

λ : Même si vous seriez sans doute surpris par la quantité de chose qu’on peut présenter sous forme d’un typage, ou en relation avec le typage : sécurité, performances, invariants de structures de données, etc.

[ tag:blog.huoc.org,2009:bluestorm/8 ]
Voir les commentaires · Commenter

/\ \/

Premiers pas vers un environnement de test NetBSD/Xen

#. Par rz0. Publié le 20.12 2009 à 15:50. 4 commentaires.
débutants ensimag netbsd xen

Loin des préoccupations über-théoriques de bluestorm, ces temps-ci, moi, j’ai essayé de bosser un peu, pour la Junior Entreprise locale. Autant dire que ça ne se passe pas tellement bien : on manque de main d’œuvre compétente et sérieuse, et honnêtement, ça m’irrite au plus haut point que le client soit perpétuellement déçu de notre travail ; c’est comme si je faisais du mauvais travail, mais franchement, si je savais que je me taperais autant de glands à gérer et autant de boulot pour lequel je n’ai pas signé, je n’aurais jamais accepté ce job. Bref, paraît qu’il y a des années avec et des années sans, bah là c’est clairement une année sans.

Tout ceci me mène à une petite anecdote : au cours de mon travail, je suis amené à tester le produit que je développe (la nature duquel est un secret…) dans une perspective de déploiement sur serveur. Récemment, je me suis heurté à des bugs que je n’arrive pas à reproduire sur ma machine de développement, pour une raison ou pour une autre (je n’ai pas encore réglé le souci), qui semble être liée au déploiement lui-même, du moins la configuration employée. Du coup, pour bien faire, je me suis dit que j’allais monter un petit serveur de test… un petit serveur virtuel !

Et ça tombe bien, j’ai toujours trouvé que Xen, ça avait l’air sympa ! Et en plus, chez NetBSD, on semble accorder une certaine importance au support Xen, ce qui est plutôt cool. Je me suis donc mis en tête de configurer un joli petit environnement NetBSD+Xen, le but étant d’avoir un serveur virtuel tournant sur une NetBSD vanilla, avec le minimum de paquetages nécessaires au déploiement de mon application. Bref, un truc propre.α

α : Le véritable serveur tourne sous Linux, mais pour du test rapide, il est beaucoup plus simple pour moi de virtualiser du NetBSD ; j’écrirai peut-être un autre billet, si je me fais un test Linux+Xen.

Xen pour les innocents

Pour me la jouer à la blueblue, je vais dire deux mots pour ceux qui ne connaîtraient pas le principe. Ahem, donc pour ceux qui ne connaîtraient pas, Xen est une solution de virtualisation,β comme VirtualBox… mais pas tellement en fait.

β : … principalement destinée aux serveurs, mais hey !

Avec VirtualBox, vous avez votre système normal et par-dessus, un petit logiciel qui fait tourner en son sein un autre système d’exploitation.

Xen est un peu différent. Vous ne pouvez pas simplement lancer Xen depuis votre système préféré, comme vous lanceriez un programme quelconque… c’est en fait plutôt l’inverse : Xen est une espèce de méta-système (un hyperviseur) qui va se charger de lancer vos systèmes d’exploitation un à un ! Si donc avant vous aviez, disons une Ubuntu avec une NetBSD dans votre VirtualBox, vous auriez maintenant l’hyperviseur Xen, et deux domaines (c’est comme ça que ça s’appelle dans Xen) : votre Ubuntu, et votre NetBSD.γ

γ : Pour tout vous dire, ce n’est probablement pas le meilleur choix… Ubuntu ne semble pas intégrer, dans sa ligne de développement centrale le support de Xen. Voyez la documentation communautaire d'Ubuntu sur Xen pour plus de détails.

Tous les domaines ne sont pas égaux pour autant : il y a toujours un système principal, appelé dom0 (domaine 0) ; les autres sont les domaines U (domU). Le dom0, c’est un peu le root de tous vos domaines : il a tous les pouvoirs, tandis que les autres ont des droits restreints, notamment concernant l’accès au réseau et au matériel. Au démarrage, Xen lance le dom0, qui a à sa charge de créer les domU qu’il désire (ou plutôt d’ordonner à Xen de les créer). Dans notre exemple, Ubuntu est le dom0, et NetBSD, le seul domU.

La dernière chose à savoir est que pour pouvoir être géré par Xen, il faut que le noyau soit conçu pour dialoguer avec l’hyperviseur, qui fait office d’abstraction entre la machine et le système. Il y a des noyaux dom0 et des noyaux domU. NetBSD propose dans sa distribution officielle des noyaux dom0 et domU ; sous Gentoo, compiler un noyau Xen est juste une histoire de changer de paquetage sources ; je ne sais pas trop ce qu’il en est des autres distributions Linux…

Bref, si vous ne connaissiez pas Xen, voilà qui est réglé. Il est temps de passer aux choses sérieuses…

Notes d’installation

Peut-être parce que c’est destiné à des mecs un peu underground, des sysadmins et tout, il n’y a pas beaucoup de tutos sur Xen… du moins, il y en a, mais beaucoup ne sont plus à jour et offrent des informations périmées. Notamment, le howto officiel de NetBSD/xen est, en l’état, incorrect.δ C’est fort dommage car mettre en place un environnement Xen avec NetBSD est en réalité très simple… pour peu que l’on sache comment s’y prendre.

J’offre ici mes humbles notes d’installation. Sait-on jamais, peut-être mon expérience servira-t-elle à quelqu’un.

δ : Si je mets la main sur ces permissions de commit un jour, je devrais ptet y faire quelque chose… Normalement, je fais signer ma clef GPG ce lundi !

  1. Si NetBSD est livré avec tout ce qu’il faut pour le faire tourner comme domaine Xen, Xen lui-même (l’hyperviseur et les outils pour le contrôler depuis le dom0) est à installer via pkgsrc. Il y a un jeu de paquetages par version majeure de Xen. Dans mon cas, j’ai pris la 3 :

    # cd /usr/pkgsrc/xenkernel3 && make install
    # cd /usr/pkgsrc/xentools3 && make install
    

    Le noyau Xen s’installe très logiquement dans la hiérarchie /usr/pkg. À l’heure où j’écris ces lignes, le fichier en question est /usr/pkg/xen3-kernel/xen.gz.

    On aura aussi besoin de scripts de démarrage pour les démons Xen xend et xenbackendd. J’ai simplement recopié ceux fournis à l’endroit habituel :

    # ln -s /usr/pkg/share/examples/rc.d/xend /etc/rc.d/
    # ln -s /usr/pkg/share/examples/rc.d/xenbackendd /etc/rc.d/
    

    Pour l’instant, ces démons ne font rien ; ils ne peuvent même être lancés (mais on peut déjà les ajouter dans le rc.conf, histoire de ne pas oublier). Il nous manque un dom0…

  2. Concernant le dom0, la bonne nouvelle, c’est que seul le noyau a besoin d’être modifié : juste en changeant de noyau au démarrage, on peut décider d’utiliser Xen ou pas, tout ça avec le même jeu de programmes, les mêmes disques et systèmes de fichiers, etc.

    Pour se faire un noyau dom0, rien de plus simple : un noyau dom0 est fourni avec NetBSD (XEN3_DOM0) ; en partant de sa configuration, j’ai simplement modifié le fichier à ma convenance (comme j’ai fait pour le noyau GENERIC, pour obtenir ma configuration non Xen), puis config et make ont fait le reste, comme d’habitude :

    # cd /usr/src/sys/arch/conf/
    # cp XEN3_DOM0 ELHAYM_DOM0
    # emacs ELHAYM_DOM0
    # config ELHAYM_DOM0
    # cd ../compile/ELHAYM_DOM0
    # make depend && make
    # cp netbsd /netbsd-dom0
    

    Pour avoir testé, il est sensiblement plus difficile de partir de la configuraiton personnalisée (dérivée de GENERIC) pour la rendre compatible avec Xen, que de partir de la configuration Xen pour l’adapter à la machine…

    Quelques défauts remarquables du noyau compatible Xen par rapport à un noyau NetBSD normal :
    • pas de SMP ;
    • pas de changement de fréquence SpeedStep du processeur (corrigé dans NetBSD-current, il me semble) ;
    • l’ACPI a quelques soucis chez moi (le poweroff ne fonctionne pas et la machine s’arrête simplement, sans s’éteindre)…
  3. Une fois le nouveau noyau fraîchement compilé, il suffit d’ajouter la ligne correspondante dans le boot.cfg. Autrefois, il fallait passer par Grub, à la place du bootloader NetBSD ; ce n’est plus le cas aujourd’hui, et tant mieux ! Les manips sont d’autant plus simple.

    À noter que la ligne à ajouter ne lance pas le nouveau noyau directement ; elle doit lancer l’hyperviseur, en lui indiquant le chemin du dom0. Quelque chose comme ceci (dans boot.cfg, il faut placer les instructions sur une seule ligne en les séparant par des points-virgules, bien sûr) :

    load /netbsd-dom0 console=pc
    multiboot /usr/pkg/xen3-kernel/xen.gz dom0_mem=max:1536M
    

    Limiter la mémoire pouvant être allouée au dom0 permet d’être sûr de pouvoir créer les domU quand on en a besoin. Je n’ai pas trouvé d’autre moyen de réduire la mémoire consommée par un domaine.

  4. Vient ensuite le domU, la configuration duquel est contrôlée par un fichier, que j’ai, pour ma part, simplement emprunté aux exemples fournis avec le paquetage :

    # cp /usr/pkg/share/examples/xen/netbsd1 /usr/pkg/etc/xen/test
    

    Le fichier est commenté, et c’est plutôt explicite. Cependant, quand on débute dans ce vaste monde, comme moi, ce n’est pas forcément évident pour autant…

    1. Tout d’abord, ça paraît stupide, mais domU, au départ, correspond à une machine vierge, sans système d’exploitation. Cela ne sert donc pas à grand chose de charger un noyau domU (par exemple le noyau XEN3_DOMU standard de NetBSD) vu qu’il ne trouvera rien sur le disque… Il faut en fait charger un noyau d’installation (INSTALL_XEN3_DOMU), qui contient un ramdisk avec sysinst.

      #kernel = '/netbsd-domu'
      kernel = '/netbsd-install-domu'
      
    2. La seconde variable d’intérêt s’appelle disk. Elle décrit les disques virtuels disponibles à l’intérieur du domU. Créer un disque virtuel vivant dans un fichier est le plus simple :

      vif = ['file:/var/xen/images/test.img,0x01,w']
      

      Pour l’installation du domU (avec le noyau d’installation), il ne faut pas non plus oublier de configurer le disque virtuel correspondant au CD-ROM contenant les sets d’installation NetBSD (les gros fichiers .tgz contenant l’userland précompilé) :

      vif = ['file:/var/xen/images/test.img,0x01,w',
             'file:/var/xen/images/install.iso,0x02,r']
      

      L’ISO se télécharge simplement sur un miroir NetBSD, dans le dossier iso/ (séparé des autres dossiers de fichiers d’installation).

      Ces disques apparaîtront dans le domU NetBSD sous la forme de périphériques xbd (le premier étant xbd0, le second xbd1, etc.). D’autres systèmes interprètent cela différemment.ε

      ε : Sous Linux, le second paramètre de la description du disque virtuel spécifie le périphérique à créer dans le domU.

      Le seul point délicat sur lequel je suis tombé est que pour utiliser un fichier en guise de disque virtuel (file:), il faut absolument que ce fichier ne soit pas creux (sparse), sans quoi vnd(4), le périphérique virtuel permettant de monter un fichier comme un disque, est incapable de fonctionner correctement.

      En pratique, cela signifie utiliser dd count=N plutôt que dd seek=N pour créer le fichier.

    3. Enfin, la configuration du réseau mérite un point à elle toute seule. Le principe est que Xen expose deux interfaces réseau (type Ethernet) connectées : une du côté domU (qui s’appelle xennet sous NetBSD) et une du côté dom0 (xvif sous NetBSD).

      Ce qui est fait de ces interfaces est laissé libre à l’administrateur, et est typiquement contrôlé par des scripts. Il y a deux scripts fournis avec le paquetage, vif-bridge et vif-ip, permettant respectivement d’obtenir un pont (bridge(4)) ou une simple IP statique privée associée au domU.ζ

      Pour ma part, j’ai choisi la seconde solution, car elle me convient mieux. Je n’ai pas besoin d’accéder à Internet depuis mon domU, et si cela devait se présenter, je pense que j’essaierai de mettre en place de la redirection, et du NAT. Pour utiliser par défaut vif-ip, il suffit de modifier le fichier de configuration de xend (/usr/pkg/etc/xen/xend-config.sxp, syntaxe s-exp, d’où le nom) :

      (vif-script vif-ip)
      

      Avec une telle configuration, la ligne de configuration du vif ressemble à :

      vif = ['ip=192.168.127.2 netmask 255.255.255.0']
      

      La valeur du sous-paramètre ip est simplement passée à ifconfig(8) pour configurer l’interface xvif (côté dom0). Le script ajoute automatiquement le sous-réseau mentionné (ici 192.168.127/24) dans la table de routage afin de diriger le trafic en direction de celui-ci à travers la bonne interface xvif. Cela signifie, en bref, qu’il suffit de donner une adresse IP appartenant au même sous-réseau à l’interface xennet, du côté du domU, et tout marche comme si les deux domaines étaient sur un même réseau !

      ζ : Le script proposé dans le howto NetBSD/xen est périmé. Celui fourni avec le paquetage semble très bien marcher.

    Une fois le fichier de configuration écrit, il suffit de faire appel à xm create, et tak.

  5. L’installation se passe comme une installation classique, par CD-ROM (à part que le périphérique CD est ici xbd1). Une fois terminée, il suffit de changer de noyau (dans le fichier de configuration du domU), et hop ! Une fois les briques de base en place, on peut passer à l’installation des paquetages que l’on veut.

    Pour ça, j’ai utilisé la solution cheap : un chroot(8) dans le vnd(4), en réutilisant les paquetages précompilés par pkgsrc pour ma NetBSD principale (avec un petit mount_union(8)).

    # vnconfig vnd0 /var/xen/images/test.img
    # mount /dev/vnd0a /mnt/
    # mkdir /mnt/usr/pkgsrc/
    # mount -t union -o -b /usr/pkgsrc/ /mnt/usr/pkgsrc/
    # chroot /mnt/ /bin/sh
    # export PKG_PATH=/usr/pkgsrc/packages/All/
    # pkg_add ...
    

    Et voilà ! Un joli environnement de test. :)

Impressions

Avec tout ça, j’ai un truc qui tourne. Je peux même tout simplement copier le disque en l’état, pour produire des environnements de test en masse.

Ceci dit, même si je considère Xen comme une technologie « cool », tout n’est pas rose. Au-delà des défauts que possède la version dom0 de NetBSD (principalement l’absence de SMP), j’ai eu du mal à trouver de la doc pertinente, et c’est un gros problème pour l’adoption de Xen par un public moins gurutique, à mon avis.

Cela mis à part, comme dit plus haut, je trouve, à titre personnel, que Xen avec NetBSD, c’est plutôt facile à mettre en place une fois que l’on a compris le truc ; et même si la doc comporte quelques erreurs, à l’heure actuelle, cela donne une bonne base sur laquelle partir.

Je ne sais pas ce qu’il en est des distributions Linux. J’entends souvent les gens opposer KVM à Xen. Je ne connais pas KVM donc je ne saurais pas trop dire ce qu’il en est. Mais du coup, il semblerait que l’adoption de Xen dans le monde Linux soit moindre… dommage ? Je ne saurais pas dire.

[ tag:blog.huoc.org,2009:posts/35 ]
Voir les commentaires · Commenter

/\ \/

Le code et ses raisons : goto en C

#. Par rz0 dans Le code et ses raisons. Publié le 10.09 2009 à 13:06. 14 commentaires.
<. Conventions : le retour
>. Le code et ses raisons : typedef en C
c débutants goto idiomes pratique programmation

Tak tak, aujourd’hui Ours répond ! Quelqu’un m’a demandé hier pourquoi mon code contenait autant d’instructions goto, la malfamée. Comme c’est une question qui revient souvent, je me suis dit que cela pouvait intéresser notre audience moins expérimentée en C. Here comes!

Ahem, commençons. goto, c’est un truc un peu vite fait obscur du C, un peu comme les trucs Caml obscurs de bluestorm mais quand même pas au même niveau de louchitude. goto, ça déchaîne les jeunes comme les vieux, tout ça au final pour une pauvre instruction qui sautille. Bref, goto ça transporte le flot d’exécution d’un endroit à un autre d’une fonction. Ça, c’est la description objective, notre ptit Get the Facts avant d’embrailler sur la vraie question : pourquoi diable utilise-t-on goto ? Je ne pourrai pas répondre pour « on » mais je peux répondre pour moi !

goto est avant tout l’instrument du pauvre. Voilà qui est dit. La raison principale derrière l’utilisation de goto est que celui-ci permet d’émuler des flots d’exécution atypiques et indisponibles en C. On parle souvent de gestion des erreurs, d’exceptions du pauvre ; ce n’en est qu’un cas particulier. L’idée générale est celle que je viens d’énoncer : goto permet d’écrire des programmes C avec des structures non présentes directement dans le langage.

Lol le C, c’est pour les ptits joueurs, ya pas d’exceptions

Quelques exemples maintenant, avec tout d’abord le grand classique : la sortie de boucles imbriquées et la gestion des erreurs. Pourquoi ai-je donc regroupé ces deux cas qui apparaissent distincts ? Car ils modèlent tous deux un type de flot permis par les exceptions dans les langages qui en possèdent : on dispose de contextes imbriqués desquels on veut se défaire en un saut.α

α : Notez que je n’ai pas dit que les exceptions pouvaient être émulées par des goto, juste qu’il y a des ressemblances dans le flot obtenu.

Jetons un œil à deux bouts de code. La sortie de bloc, tout d’abord :

{
    while (a) {
        while (b) {
            if (c)
                goto end;
            ...
        }
    }
end:
    ...
}

Et la gestion des erreurs, dans sa forme modelant une pile :

{
    if ((a = f(...)) == NULL)
        goto bada;
    if ((b = g(...)) == NULL)
        goto badb;
    ...
    return ...;
badb:
    antif(a);
bada:
    return ...;
}

Le premier exemple ne devrait demander aucun commentaire. Le second n’est pas aussi évident : il n’exhibe pas clairement l’idée de contextes imbriqués. Celle-ci est en vérité masquée par le court-circuitage introduit par les sauts. Nous aurions pu écrire :

{
    if ((a = f(...)) != NULL) {
        if ((b = g(...)) != NULL) {
            ...
        } else {
            antif(a);
            goto bada;
        }
    } else {
    bada:
        return ...;
    }
    return ...;
}

OK, le tout s’est sensiblement enlaidi, et je vous rassure, personne ne l’écrit ainsi (car en plus d’être laid, on effectue beaucoup plus de sauts en cas d’erreur). L’avantage de cette forme, cependant, est qu’elle modélise beaucoup plus précisément le comportement d’une exception dans un langage de plus haut niveau : chaque bloc if correspondrait à un bloc try (ou équivalent) avec une clause de finalisation (contenue dans la partie else). Lorsqu’une condition exceptionnelle se produit, les clauses de finalisation sont déroulées les unes après les autres, dans l’ordre d’empilement des contextes (représentés ici par les blocs).

À part remarquer que la gestion des erreurs, cas normal d’utilisation des exceptions, n’est pas celui qui possède la correspondance la plus naturelle avec l’utilisation de goto, nous n’avons plus grand chose à dire ici. Passons donc à…

Plus tiré par les cheveux (encore) : appels récursifs terminaux

Si vous jetez un œil aux codes sources que j’écris, il arrive que j’utilise des goto à la place de boucles classiques. Et oui, sacrilège § L’explication ? ’Tention, c’est tordu : cela modélise des appels récursifs terminaux simples.β

L’exemple typique est celui d’une fonction :

{
    T x;

re:
    if (...)
        ...
    if (...)
        ...
    if (...) {
        x = f(x, ...);
        goto re;
    }
}

Alors oui, on pourrait bien sûr écrire tout ça sous forme d’une boucle ou carrément utiliser l’appel récursif et prier pour que le compilateur C optimise.γ

C’est en vérité une question d’emphase, et ces questions là sont profondément subjectives. Une boucle mettrait moins l’accent sur l’action et davantage sur sa répétition. Dans le cas où le cas récursif est marginal, la construction s’en retrouve relativement maladroite : on a alors le choix entre une condition de boucle obscure et redondante, des marqueurs booléens dans tous les sens, ou des instructions break et continue judicieusement placées. En choisissant cette dernière solution, qui semble la moins contraignante, on se retrouve avec un code dans ce genre-là :

{
    T x;

    for (;;) {
        if (...)
            ...
        if (...)
            ...
        if (...) {
            x = f(x, ...);
            continue;
        }
        break;
    }
}

Mieux ? Je ne crois pas, mais je n’irai pas non plus cracher sur celui qui écrit cela. :) Tout cela au fond pour une question de…

β : Là aussi, soyons fous mais pas trop, on ne peut pas émuler la récursivité terminale dans le cas général avec goto.

γ : GCC le fait, m’enfin, d’autres compilateurs ne le font pas forcément, ce n’est pas une optimisation très orthodoxe en C comme elle peut l’être dans les langages fonctionnels, l’idiome étant en Cδ de tout convertir en style itératif.

δ : Mais rms a trop fait de Lisp, comme tout le monde sait. Lalala.

Lisibilité !

C’est là que les gens se fâchent, que les regards se clashent, que la violence se lâche, wash wash. Hum, les uns disent de ne pas utiliser goto, pour la clarté du code, et pour les mêmes raisons, d’autres l’utilisent ! C’est la vie. Après tout, il y bien des gens qui codent en style GNU en pensant se rendre lisibles. :)

La question de la lisibilité de goto tient principalement à sa nature la plus primaire : la possibilité de passer librement d’une portion de code à une autre. Les détracteurs de goto pointent du doigt les utilisations de l’instruction qui engendrent des transitions difficiles à suivre dans le flot. Ceux qui en défendent le gain visuel évoquent la possibilité offerte par goto de déporter des blocs de code d’un endroit à un autre. Cette astuce est principalement utilisée pour la gestion des erreurs et permet ainsi de dégager du traitement utile la gestion des cas de défaillance, rares en pratique et souvent triviaux (l’action à entreprendre étant, la plupart du temps, de faire remonter l’information telle qu’on la reçoit).

Pour conclure sur un dernier exemple, j’abuse souvent de cette propriété pour écrire des choses comme celles-ci :

{
    T *p;
    R *q;

    p = malloc(sizeof *p);
    q = malloc(sizeof *q);
    if (p == NULL || q == NULL)
        goto bad;

    ...
    return ...;

bad:
    free(q);
    free(p);
    return ...;
}

Mon style vous répugne ? N’hésitez pas à commenter. :)

Annexe : setjmp et ses copains

Si, de mon expérience, goto est généralement relativement bien reçu dans la communauté des programmeurs C dans son ensemble (excepté dans certains cercles, et dans l’enseignement, sans doute de peur de pervertir les étudiantsε), il en est autrement de setjmp, le « goto non local ».ζ

Il y a probablement plusieurs raisons à cela : la difficulté d’utilisation (cf. restrictions énoncée par la norme, dont la nécessité de déclarer toutes les variables locales manipulables après le saut volatile), mais aussi les divers problèmes lié à la conservation et la restauration des états : il faut pouvoir replacer de manière sûre le contexte au point où il était au moment de l’appel à setjmp. Cela implique la libération de mémoire et l’appel de destructeurs pour les données allouées dans l’intervalle, mais également des notions de plus bas niveau telles que la gestion des signaux (cf. sigsetjmp(3)). En plus de ça, setjmp avec toute cette complication engendre des performances moisies, en général.

setjmp est toutefois utilisé, parfois, pour implémenter des mécanismes d’exceptions, notamment dans la kazlibη (pour le C) et diverses implémentations portables de langages dynamiques (l’interpréteur interactif d’OCaml, si je me rappelle bien, et d’autres dont je ne me rappelle plus…).

ε : Il paraît que ce sont les éléments perturbateurs tels que moi qui tentent leurs petits camarades avec le côté obscur. :)

ζ : Notez que GCC possède également des goto non locaux et indirects grâce aux pointeurs d’étiquettes. Je ne m’étendrai pas à ce sujet (car je connais mal le GNU C). Plus d'informations dans le manuel de GCC.

η : Je ne vois plus dans Google la page Web correspondant. Le projet est peut-être bien décédé…

Annexe : omg l’optimisation qu’elle est bonne

On entend dire parfois que goto c’est pas bon pour les performances. Bon alors, dans l’absolu, hum, en fait il n’y a pas d’absolu. On pourrait dire que remplacer des structures de contrôle classiques par goto enlève de l’information sémantique, et bon, dans l’absolu, c’est vrai. La question est de savoir si cette information sémantique est pertinente d’une part, et si elle est utilisée, d’autre part. Question pertinence, c’est, là encore, un peu subjectif : est-ce que tel ou tel goto remplace simplement une combinaison intelligente de structures de contrôle ou ne reflète-t-il un concept absent du C ? C’est davantage une question philosophique, donc humaine, que d’importance à la machine… car en vérité, la plupart des compilateurs modernes ne tiennent absolument pas compte de ce genre de choses, et le langage intermédiaire employé n’emploie que des sauts. Ce sont ensuite les algorithmes d’analyse de flot (brrr, théorie des graphes) qui ont à leur charge de différencier les blocs et autres constructions utiles au compilateur.

[ tag:blog.huoc.org,2009:posts/29 ]
Voir les commentaires · Commenter

/\ \/

Conventions : le retour

#. Par rz0 dans Le code et ses raisons. Publié le 25.08 2009 à 14:46. Aucun commentaire.
>. Le code et ses raisons : goto en C
c débutants idiomes pratique programmation

Comme il ne coûte pas cher de déterrer des brouillons presques prêts pour en faire des articles, voici le retour de l’article sur les conventions que j’avais commencé à écrire, et duquel avait été tiré mon petit poème.

Ceci est donc un article orienté débutants. « Woah, rz0 il fait des articles de vulgarisation maintenant. » Et ouais les cocos, ce sont des choses qui arrivent, dans la vie. Parfois, en se levant le matin, et qu’il est déjà midi, on n’a pas envie de plancher tout de suite sur la refactorisation qui a échoué la veille. Bref, trève de bavardage.

Les conventions, c’est quoi ? Le principe est simple, c’est celui de la bonne ptite ouvrière : faire la même chose toujours de la même manière. Oubliez vos pulsions d’artiste et embrassez pleinement la voie de la machine (l’autre voie de la machine, pour ceux qui sont déjà en prépa) !

Et je ne parle pas ici de ne pas nommer une variable prgcnt et une autre MyFckngVisitorCount juste à côté, ça c’est la différence entre lEs GenS Ki ÉkrIv KoM sA et le monde civilisé.

Non, l’idée est que si une variable assure un certain rôle plusieurs fois, alors elle devrait avoir le même nom à chaque fois. Un peu comme les païens du C++ appellent toujours (sont toujours forcés d’appeler) le pointeur sur l’objet manipulé this. Si vous écrivez une bibliothèque de tableaux dynamiques, vous pouvez par exemple décider que n correspond toujours au nombre d’éléments du tableau actuellement considéré.

Heureusement, Le Créateur a pensé à vous, et les anciens vous ont légué une pelleté de noms prêts à l’emploi. La bonne nouvelle, c’est que vous pouvez les utiliser. La mauvaise, c’est que vous devez les utiliser. (Et c’est ici qu’intervenaient mes talents poétiques.)

Car pour la descendance, Dieu l’ordre dicta :
Pour les itérations, les compteurs i, j, k ;
n, m : des quantités ;
p, q devront pointer ;
c pour les caractères ;
s et t pour les chaînes.
Mais ni o ni l, les suppôts du binaire,
Pour que le programmeur toujours lise sans peine.

Mais il est souvent tout aussi utile de définir vos propres conventions, pour vos objets, vos types, vos règles du jeu. Ou simplement de préciser les règles traditionnelles (comme nous l’avons vu juste au-dessus avec n).

Toujours utiliser les mêmes noms pour les variables de mêmes rôles vous évitera d’avoir à commenter leur déclaration avec des banalités du genre :

int lol;      /* Nombre de cailloux dans la marre. */

Remarquez aussi, souvent, vous pourrez choisir des noms conventionnels courts. Si après avoir lu mon chant à la gloire du Grand, vous écrivez toujours indiceCaillou au lieu d’i dans votre module de gestion des cailloux dans les marres, ma foi, je ne peux rien pour vous (sinon prier pour votre âme, pour lui éviter le knout).

Un point de débat possible sur la question, cependant, est le suivant : faut-il privilégier la précision ou la convention ? (Et non, indiceCaillou ne relève pas de la précision à mes yeux.)

Imaginez, vous utilisez p pour toutes vos variables de type Meuh, en entrée des fonctions de votre module. Cela tombe bien, elles ne prennent chacun qu’un seul objet à examiner ! Parfois, en interne, vous avez besoin d’un second pointeur, que vous appelez naturellement q ; tout va bien, vous avez spécialisé les conventions existantes, parfait. Maintenant, imaginez que vous vouliez ajouter une fonction copier qui copie la valeur d’un Meuh dans un autre. Elle aura deux paramètres. Doit-on les appeler dest et src, pour la précision ? Ou p et q pour suivre la convention ? Cela nous amène au point suivant.

Tout comme il est important de se fixer des conventions pour les noms de variables, il est souvent très utile de vous définir des ordres de passage d’arguments respectés par toutes les interfaces de votre unité. Ce sont des règles du type « l’objet ayant le rôle X précède toujours cela de rôle Y ».

L’exemple typique ici est la fonction de copie. La destination doit-elle précéder ou suivre la source ? Il n’y a pas de réponse universelle, et chacun a ses préférences, mais il est important de toujours utiliser la même stratégie de classement de vos paramètres.

Pour revenir au problème précédent, s’il semble d’abord aberrant de vouloir appeler ses paramètres p et q, si l’on considère que l’ordre de passage est bien établi, alors l’usage ne fait aucun doute : p est toujours le premier pointeur et q le second, l’ordre des arguments se chargeant de donner son sens à cette position. Les deux alternatives sont donc viables ; à vous de choisir celle qui colle le plus à votre style.

Notez enfin que le point de vue est souvent influencé (un peu ou beaucoup) par le paradigme dominant. Si, disons, vous réfléchissez en orienté objet et avez codifié que le paramètre numéro un était l’objet sur lequel on opérait, alors le fait de toujours nommer cet objet de la même façon prend tout son sens. Si au contraire, vous adoptez une vision plus procédurale, un autre schéma de noms vous sera peut-être plus naturel…

Et voilà pour les conventions ! Notez que j’ai fait exprès d’éluder le sujet des idiomes, sujet fort proche. La raison ? La flemme, bien sûr. Mais pourquoi pas un jour prochain, ou un autre encore. Qui sait, tant qu’il y a du désir, et des gens pour me lire. Lalala.

[ tag:blog.huoc.org,2009:posts/25 ]
Aucun commentaire · Commenter

/\

Écrire du code C avec Emacs

#. Par rz0. Mis à jour le 20.02 2010 à 10:02. Aucun commentaire.
c débutants emacs pratique programmation tutorial

Et de deux ! —rz0

Cet article d’introduction s’intéresse aux aspects basiques liés à l’écriture et à la gestion du code C basique avec Emacs, et, bien sûr, aux fonctionnalités qui vous y aideront.

Édition du code avec le c-mode

Le mode d’édition du code C dans Emacs se nomme très simplement le c-mode et est inclus par défaut avec l’éditeur ; vous pouvez aussi obtenir la dernière version du mode (qui est développé en dehors d’Emacs lui-même) sur le site du cc-mode. Le c-mode offre plusieurs fonctionnalités et commandes intéressantes.

Indentation automatique

Tout d’abord, comme tous les modes de programmation dans Emacs, le c-mode possède l’indentation automatique. Cela signifie que la touche TAB (ou, de manière équivalente, C-i) n’est pas utilisée pour insérer un caractère « tabulation » mais pour indenter la ligne actuelle, c’est-à-dire aligner le début de celle-ci sur la colonne adéquate, comme vous le feriez vous-même si Emacs n’était pas là. Il est important de remarquer que cette opération ne tient absolument pas compte de l’indentation actuelle de la ligne : qu’elle soit trop décalée à gauche ou à droite, Emacs la replacera au bon endroit.

Emacs indentera la ligne selon deux informations : le contexte syntaxique (si vous êtes au début d’une instruction, au milieu d’un appel de fonction, etc.) et le style sélectionné (par exemple, K&R ou BSD ; voyez mon introduction à la configuration d’Emacs pour plus d’informations).

Comme il peut devenir lassant de devoir appuyer (quoique ce ne soit qu’une seule fois) sur la touche TAB à chaque début de ligne, sachez que vous pouvez aller à la ligne et indenter en une seule commande : C-j (newline-and-indent).

Mais l’indentation d’Emacs ne se limite pas à la ligne actuelle ! Vous pouvez également lui demander d’indenter tout un bloc ou une fonction pour vous. Il s’agit respectivement des commandes C-M-\ (indent-region) et C-c C-q (c-indent-defun), qui indentent la région actuelle (la zone de texte située entre la marque, usuellement apposée par C-SPC, et le curseur) et la fonction dans laquelle se trouve le curseur. En théorie, vous pourriez indenter tout le fichier ainsi, mais je vous le déconseille ; lalgorithme est lent et perd en précision si les constructions sont trop vilaines ou complexes. En revanche, si vous effectuez une modification au milieu d'un bloc, et que, par exemple, vous supprimez un niveau d'indentation, ces commandes peuvent s'avérer très utiles. Parfois, en revanche, vous voulez aligner des éléments (comme le ferait la toucheTABen temps normal) sans qu'Emacs n'essaie d'indenter la ligne. Pour cela, vous pouvez utiliser la commandeM-i(tab-to-tab-stop). C'est le cas, par exemple, si vous alignez vos déclarations ou vos instructions dans unswitch. Pour résumer, voici un tableau des commandes d'indentation et d'alignement : |TAB | indente la ligne actuelle | |C-j | retour à la ligne et indentation simultanés | |C-M-‘ | indente la région actuelle

C-c C-q indente la fonction actuelle
M-i aligne le curseur sur la prochaine colonne de tabulation

Gestion des commentaires

De même qu’Emacs sait indenter le code automatiquement, il sait insérer des commentaires au bon endroit ; pour se faire, il vous suffit d’actionner la commande M-; (comment-dwim). Que vous soyez sur une ligne vide ou non, Emacs déterminera la syntaxe à adopter. C’est la commande idéale pour ceux qui trouvent les commentaires de type C (/* ... */) trop longs à taper.

De plus, Emacs dispose des commandes de traitement de régions suivantes : comment-region et uncomment-region. Elles sont accessibles par M-x et qui peuvent être associées à une combinaison de votre choix si vous en sentez le besoin. Je vous laisse deviner ce qu’elles font. Elles peuvent être utiles à ceux qui aiment isoler des morceaux de code avec des commentaires (personnellement, je préfère utiliser #if 0).

M-; insère un commentaire

Déplacements contextuels

Enfin, le c-mode dispose de mécanismes de navigation contextuelle. La plupart des commandes utilisées sont communes à tous les modes d’édition mais leur fonctionnement a été adapté à la structure du code C.

Vous pouvez, comme toujours, vous déplacer de mot en mot avec les habituels M-b (backward-word) et M-f (forward-word).

Juste au niveau supérieur, les commandes C-M-b (backward-sexp) et C-M-f (forward-sexp) vous permettent de sauter d’un bout à l’autre d’expressions équilibrées (un groupe entre parenthèses, crochets ou accolades). En complément, C-M-u (backward-up-list) et C-M-d (down-list) naviguent à travers les niveaux de parenthésage : C-M-u vous amène au début du groupe dans lequel se situe le curseur et C-M-d vous transporte au début du prochain groupe à l’intérieur de l’expression actuelle. Le plus simple est encore de les essayer pour en voir les effets !

Vous pouvez également vous déplacer au début et à la fin des instructions avec M-a (c-beginning-of-statement) et M-e (c-end-of-statement), respectivement.

Enfin, les commandes beginning-of-defun et end-of-defun font, sans surprise, le voyage au début et à la fin d’une définition de fonction.

Récapitulons les commandes directement accessibles :

C-b, C-f déplacement au mot
C-M-b, C-M-f déplacement à l’expression (équilibrée)
C-M-u, C-M-d déplacement à l’expression (équilibrée)
M-a, M-e déplacement à l’instruction

Plusieurs fichiers sources

Avec ce que nous venons juste de voir, nous sommes maintenant capables d’éditer un fichier source C. Toutefois, la plupart des projets ne sont pas constitués d’un seul fichier, aussi, nous allons nous pencher sur la gestion de plusieurs fichiers dans Emacs. Cette section n’est pas directement liée à la programmation en C, mais la question revient fréquemment chez les novices, et c’est pourquoi j’ai décidé de la traiter ici.

Hiérarchies de fichiers

Typiquement, le scénario est le suivant : vous possédez plusieurs fichiers sources regroupés dans un dossier. Quelque chose que beaucoup de gens aiment faire est pouvoir regarder la hiérarchie des fichiers et naviguer de fichier en fichier. Pour cela, Emacs offre deux mécanismes : dired, un explorateur de fichiers, et la speedbar, une barre latérale.

Selon vos goûts, vous préférerez sans doute l’un ou l’autre. Les différences sont multiples : dired possède plus de facilités pour la gestion des hiérarchies de fichiers et de dossiers, avec toutes les fonctions classiques que l’on pourrait attendre d’un explorateur de fichiers, et se présente comme une application Emacs à part entière, utilisant ses propres buffers. À l’opposé, la speedbar s’affiche dans un cadre (frame) à part, et son contenu est constamment recalculé de sorte que le dossier affiché dans la barre soit toujours celui du fichier actuel.

Si vous n’êtes pas habitué à la terminologie emacsienne, sachez qu’une fenêtre correspond à une division de l’affichage d’Emacs (ce que vous obtenez avec C-x 2 par exemple) tandis qu’un cadre est un ensemble de fenêtres indépendant, ce qui, en mode graphique, correspond, pour la plupart des gestionnaires de fenêtres, à ce qu’ils appellent une fenêtre. Sauf mention contraire, nous utiliserons ici le vocabulaire d’Emacs.

À l’usage, l’un ou l’autre est assez naturel. Pour lancer dired, utilisez la commande C-x d (dired) ou C-x C-f (find-file) avec le chemin d’un dossier. Pour lancer la speedbar, tapez M-x speedbar ; un cadre devrait apparaître à côté de votre cadre principal.

Une fois à l’intérieur de l’un ou l’autre, placez-vous sur un fichier et tapez RET (dired-advertised-find-file ou speedbar-edit-line) pour le visiter. Sous dired, vous avez l’option de visiter le fichier dans une autre fenêtre avec o (dired-find-file-other-window).

Si vous voulez parcourir des sous-dossiers, sachez que dired prône une disposition plane tandis que la speedbar affiche les dossiers et les fichiers sous forme arborescente. Lorsque votre curseur est sur une ligne correspondant à un sous-dossier, tapez i sous dired ou + dans la speedbar (respectivement, dired-maybe-insert-subdir et speedbar-expand-line) pour insérer son listage dans le buffer actuel. Pour supprimer l’affichage d’un sous-dossier, placez-vous sur son en-tête (son chemin, juste au-dessus du listage, pour dired, ou son nom dans l’arborescence, pour la speedbar) et exécutez la commande M-x dired-kill-subdir ou - (speedbar-contract-line), pour dired et la speedbar respectivement. Au lieu de l’effacer complètement, dired vous propose aussi d’utiliser la commande $ (dired-hide-subdir), qui, malgré son nom, permet de cacher et de découvrir le contenu du dossier.

Si votre gestionnaire de fenêtres ne vous permet pas de déplacer facilement le focus au clavier, sachez qu’Emacs vous permet d’aller de cadre en cadre avec C-x 5 o (other-frame).

Et comme précédemment, un petit récapitulatif :

  dired speedbar
parcourir un dossier C-x d automatique
visiter un fichier RET RET
insérer un sous-dossier i +
cacher un sous-dossier $ -

Plusieurs fichiers dans Emacs

Maintenant que vous savez accéder facilement à plusieurs fichiers de votre projet, vous risquez fort d’être vite submergé par le grand nombre de buffers ouverts. Emacs ne dispose pas de tabs (ce n’est pas comme si vous alliez vous servir de votre souris pour y accéder, de toute manière). En revanche, plusieurs modes sont disponibles pour gérer vos buffers. Je ne vais pas tous les passer en revue ici, je ne donnerai qu’une seule solution.

La première commande à connaître pour bien gérer vos multiples buffers est sans doute C-x C-b (list-buffers). Elle démarre le gestionnaire de buffers. Ce dernier se présente comme une simple liste des buffers ouverts. Vous pouvez y effectuer les opérations de navigation habituelles, dont la recherche (avec C-s (isearch-forward) pour les étourdis). RET (Buffer-menu-this-window) vous permet de visiter le buffer à la ligne actuelle. Une autre utilisation très pratique est de supprimer plusieurs buffers à la fois. Pour cela, marquez chaque buffer à détruire avec la commande d (Buffer-menu-delete). La lettre D apparaît alors à gauche du nom. La commande x (Buffer-menu-execute) effectue l’opération de nettoyage en elle-même. Ainsi, si vous vous êtes trompé dans votre marquage, vous pouvez revenir en arrière en utilisant la commande u (Buffer-menu-unmark) sur la même ligne.

Tout cela est bien utile pour faire le ménage mais ne vous aide guère pour votre besoin le plus simple : naviguer de buffer en buffer. Pour cela, il y a bien sûr la commande C-x b (switch-to-buffer) par défaut d’Emacs pour changer de buffer. Mais elle n’est pas très pratique ; en effet, elle demande d’entrer le début du nom pour accéder au buffer. Fort heureusement, il existe (au moins) un mode qui vous permet de changer de buffer en tapant quelques lettres n’importe où dans le nom. Je vous présente là l’iswitchb-mode. Pour l’essayer, il vous suffit d’effectuer la commande M-x iswitchb-mode ; de même pour le désactiver. Pour l’adopter définitivement, ajoutez à votre .emacs la ligne :

(iswitchb-mode t)

La combinaison C-x b invoquera à présent iswitchb-buffer. Si vous l’essayez, vous remarquerez que la liste des buffers qui sont encore candidats à la sélection est maintenant affichée à côté de votre saisie. Dans ce mode, les commandes C-r (iswitchb-prev-match) et C-s (iswitchb-next-match) vous permettent de faire défiler les éléments de la liste de manière cyclique, et RET sélectionne le premier buffer de la liste. Ainsi, vous n’aurez plus à taper le début exact du nom que vous voulez, il vous suffira simplement d’entrer quelques lettres remarquables et, moyennant quelques rotations de la liste, vous arriverez rapidement à destination.

Vous pouvez aussi demander à utiliser des expressions régulières. La commande C-t (iswitchb-toggle-regexp), dans l’iswitchb-mode, vous permet cela. Par exemple, pour obtenir une liste des fichiers C ouverts, vous pourriez utiliser le motif \.c$. Veuillez cependant prendre note du fait que les expressions régulières d’Emacs diffèrent d’autres syntaxes, soyez donc averti et pensez à lire le manuel (avec C-h i (info), par exemple) si vous désirez en faire bon usage.

Avec l’habitude, ce système très flexible remplace avantageusement les tabs. Vous n’êtes plus lié à une liste prédéfinie ; vous avez la possibilité de fabriquer rapidement la vôtre, adaptée à votre requête. Du reste, il vous permettra de même d’aller de buffer en buffer. Et tout cela pour un encombrement nul dans vos autres actions !

Et pour les petits étourdis :

C-x C-b gestionnaire de buffers
C-x b change de buffer
C-r et C-s défilement de la liste (dans iswitchb)
C-t activation des regexps (dans iswitchb)

Édition simultanée de plusieurs fichiers

Bien sûr, vous n’êtes pas encore capable de vous dédoubler pour éditer plusieurs fichiers à la fois, mais parfois, vous pourriez vouloir afficher deux fichiers (voire plus) côte à côte.

Pour se faire, rien de plus simple. Il vous suffit de partager votre affichage en deux : verticalement avec C-x 2 (split-window-vertically) ou horizontalement avec C-x 3 (split-window-horizontally). Pour passer d’une fenêtre à l’autre, utilisez C-x o (other-window). Enfin, C-x 0 (delete-window) et C-x 1 (delete-other-windows) vous permettent respectivement de supprimer la fenêtre sur laquelle se trouve le focus et de ne laisser qu’elle.

Ainsi, vous pourrez plus facilement comparer ou transférer du code d’un fichier à un autre.

Sachez également qu’il existe des commandes qui marient une action donnée à un changement de fenêtre, telles queC-x 4 f (find-file-other-window) et C-x 4 b (switch-to-buffer-other-window). La première ouvre un fichier, à l’instar de C-x C-f (find-file) mais l’affiche dans une fenêtre séparée. La seconde agit comme C-x b (switch-to-buffer) mais, de même, place le contenu dans une autre fenêtre. Si le cadre n’est constitué que d’une fenêtre, celui-ci est automatiquement scindé en deux.

C-x 2, C-x 3 création de fenêtres
C-x o déplacement de fenêtre à fenêtre
C-x 0, C-x 1 suppression de fenêtres
C-x 4 f visite un fichier dans une autre fenêtre
C-x 4 b affiche un buffer dans une autre fenêtre

Gestion de l’historique des modifications avec VC

Le dernier outil essentiel à tout projet sérieux est un gestionnaire de versions. Si vous n’êtes pas familier avec ce concept, voici une brève introduction. Vos fichiers seront amenés à grandement évoluer avec les phases d’essai, de correction, d’essais publics, d’amélioration, etc. Il est donc primordial de contrôler ces changements sous forme de versions. Des logiciels spécialisés appelés « systèmes de contrôle de versions » (VCS, Version Control Systems en anglais) permettent de faire cela. Nous n’entrerons pas ici dans le détail du fonctionnement de ceux-ci, mais le principe général est le suivant : chaque fichier est associé à une copie persistante, qui représente tous ses états depuis le début, toutes ses versions, sous forme de différences. Cela signifie qu’une version n’est pas stockée en entier mais uniquement sous une forme relative à une version voisine. Dans sa réalisation la plus classique, par exemple, si vous ajoutez une ligne à un fichier, le fichier de contrôle, contenant la copie persistante, enregistrera une différence d’une ligne par rapport à la version précédente.

Emacs intègre un support de certains de ces systèmes, dont RCS, CVS, SVN et Git. Pour illustrer cette intégration, qui porte le petit nom de VC pour, vous l’aurez deviné, Version Control, nous allons nous intéresser au plus simple d’entre eux : RCS. Mais les commandes demeurent approximativement les mêmes quel que soit le programme de gestion de versions.

Traditionnellement, les fichiers de contrôle de RCS sont stockés localement dans un sous-dossier de celui où se situent vos fichiers sources, ce sous-dossier portant le nom RCS. Si vous ne le créez pas par vous-même, Emacs vous le proposera en temps voulu.

L’utilisation de VC repose sur la commande C-x v v (vc-next-action). Comme son nom l’indique, elle fait… des choses, selon l’état actuel du fichier. Avec RCS, un fichier peut avoir plusieurs états. Dans son utilisation classique, ce sont les suivants.

  1. Au départ, le fichier est inconnu de RCS. Lorsque vous l’enregistrez auprès de ce dernier (avec C-x v v), celui-ci s’approprie le contenu. Le fichier et la version la plus récente connue de RCS sont alors les mêmes.

  2. À ce stade, vous ne pouvez pas modifier le fichier à nouveau. Il vous faut prévenir RCS de votre intention, ceci afin d’empêcher que plusieurs personnes ne modifient le même fichier en même temps. Cette action s’appelle le verrouillage et s’effectue aussi par C-x v v. Une fois le fichier verrouillé, vous êtes sûr qu’il est la seule copie de travail existante.

  3. Une fois vos modifications effectuées, C-x v v synchronise votre brouillon avec la copie de contrôle. VC vous demande alors de fournir un commentaire décrivant les changements, commentaire qui sera inscrit dans une espèce de journal de bord du fichier. La fenêtre d’édition du message est tout à fait banale et la validation est obtenue par le traditionnel C-c C-c, valable pour la plupart des formulaires sous Emacs. Après cette étape, le fichier et la version la plus récente connue de RCS sont à nouveau les mêmes.

Tout ceci est bien joli, mais à quoi serviraient un historique et un journal que l’on ne peut consulter ?

La commande C-x v ~ (vc-version-other-window) vous permet d’accéder au contenu d’une version antérieure de votre fichier. Le numéro de la version vous sera demandé. Mais que faire si vous ne vous rappelez plus de la bonne version ? C’est à cela que servent les commentaires que vous avez pris soin d’entrer tout au long de votre travail ! Pour voir la liste des versions ainsi que les messages associés, tapez C-x v l (vc-print-log).

Une fois la version voulue récupérée dans un buffer secondaire, vous pouvez afficher les deux buffers en parallèle comme nous avons vu précédemment. Mais VC propose une commande intéressante pour vous aider à voir les différences entre vos versions : C-x v = (vc-diff). Elle vous propose une comparaison rapide de deux versions, avec la différence placée dans un buffer à part.

Cela peut sembler beaucoup de choses à enregistrer si vous n’avez jamais utilisé de VCS auparavant, mais j’ai moi-même débuté dans ce domaine avec VC qui est, à mon avis, un moyen simple de se familiariser avec le concept tout en restant bien au chaud dans son éditeur préféré. Et pour vous y aider, voici un… récapitulatif ! Avouez que vous ne vous y attendiez pas.

C-x v v change d’état
C-x v ~ ressuscite une version
C-x v l invoque la liste des versions
C-x v = compare deux versions

Cette présentation de VC et de RCS achève ce premier article. Dans le prochain chapitre (peut-être…α), nous parlerons de la compilation de fichiers et de projets écrits en C, à l’aide de makefiles, le tout depuis Emacs. Contrairement à ce premier tutoriel, il sera bien moins porté sur Emacs lui-même que sur les outils à mettre en œuvre pour compiler depuis Emacs, c’est-à-dire GCC et make.

α : L’histoire nous a montré que ce « peut-être » a su se transformer en « non ». :)

Je remercie Metzgermeister et nicofrand pour la relecture de cet article.

[ tag:blog.huoc.org,2009:posts/40 ]
Aucun commentaire · Commenter

>> Page : 0 1