Intéressé par des cours d'informatique en ligne ?
Visitez mon nouveau site https://www.yesik.it !

Ceux qui me connaissent savent que je suis un partisan des solutions libres. Mais il faut savoir ne pas être sectaire et reconnaître quand d'autres modèles économiques donnent naissance à des choses intéressantes. C'est ce dont on peut gratifier Microsoft, qui, en développant C#, a conçu un langage clair et élégant.

Une option pour faire du C# sous Linux est d'utiliser Mono. Ce projet reprend non seulement le langage C#, mais aussi les bibliothèques et l'environnement pour développer et utiliser des applications .NET sous Linux. Seulement, pour intéressante que soit cette solution, la FSF émet des réserves quand à l'utilisation de Mono pour le développement de logiciels libres.

Une alternative pour ceux qui trouvent la syntaxe de C# intéressante est de se tourner vers le langage Vala, un sous-projet de Gnome. Si celui-ci reprend quelques idées de C# et s'inspire de sa syntaxe, il ne nécessite aucune run-time spécifique et ne dépend d'aucune bibliothèque estampillée Microsoft. Je ne suis pas légiste, et je ne vous dirai donc pas qu'il est certain qu'aucun élément de syntaxe repris par Vala ne soit couvert par un brevet Microsoft. Malgré tout, le risque est tout de même bien moindre qu'avec une vraie mise en oeuvre de .NET.

Mais se contenter de comparer Vala à C# est sans doute une vision réductrice. En effet, il ne s'agit pas juste d'un clone ou d'une imitation: À l'usage, il se révèle un vrai langage, sympa à utiliser, et bien intégré avec le modèle de développement de Gnome/GObject/Gtk/GLib/Gio/...

Qui est Vala?

De façon un peu surprenante, il n'est pas facile de savoir pourquoi le nom de Vala a été choisi.

Plusieurs explications sont suggérées sur la mailing-list pour expliquer cette décision [1][2]. Tout d'abord, son assonance avec Java. Ou éventuellement d'autres termes de langues européennes: bala (espagnol), voilà (français).

Mais, on peut aussi y chercher une référence à des personnages de fiction. Ainsi, une explication qui revient fréquemment dans la discussion – et qui reste sans doute la plus amusante – est que ce soit un clin d'œil caché à Vala Mal Doran. Ceci dit, Jürg Billeter, un des auteurs originaux, dément régulièrement les conjectures faites à ce sujet. Par contre, lors d'une conférence, il aurait mentionné l'astéroïde 131 Vala comme origine possible...

Principe de fonctionnement

Valac.png

Le compilateur valac produit un code intermédiaire en C à partir du code source en Vala. Ensuite, c'est le compilateur C du système qui est chargé de générer l'exécutable.

Peut-être parmi vous certains ont connu les temps héroïques de la naissance du C++. Cette époque n'est pas si loin de nous que ça: et, même si je ne suis pas encore un vétéran, je me souviens avoir utilisé Cfront pour mes premiers programmes C++ – ou plutôt « C avec classes » comme on disait alors. En fait, Cfront était un convertisseur qui transformait le source C++ en C. La compilation à proprement parler étant dévolue au compilateur C conventionnel.

Si je rappelle ici ces souvenirs, c'est parce que Vala reprend le même modèle: le programme Vala est compilé en C, puis c'est le compilateur C qui se charge de produire l'exécutable. Cela a plusieurs avantages:

Compatibilité avec les bibliothèques et les programmes C
Tout d'abord l'interopérabilité avec le code C est garantie puisqu'un programme Vala est un programme C. Dit de manière plus formelle, Vala utilise la même ABI que C: un programme Vala peut directement utiliser des fonctions de bibliothèques C. Inversement, des méthodes Vala peuvent être invoquées à partir d'un programme C.
Pas de run-time ni de machine virtuelle
L'exécution n'est pas pénalisée par une run-time ou une machine virtuelle quelconque et permet donc d'envisager des performances similaires à celle d'un programme écrit directement en C.
Génération de code et optimisation par le compilateur de la plate-forme
Enfin, les développeurs de Vala n'ont pas à se préoccuper de la génération du code machine ou des optimisations possibles à ce niveau. Non seulement, c'est le compilateur C qui aura la charge de ce travail (ce dont gcc se charge très bien), mais aussi cela assure la portabilité du code Vala puisque le code intermédiaire peut être compilé sur une majorité de plate-formes.

Installation

Sous Debian, Vala est disponible dans les paquets officiels. Malheureusement, sous Lenny, la version fournie est assez ancienne, puisqu'il s'agit de Vala 0.8.1 – alors qu'au moment d'écrire ce texte, la version 0.10.3 vient de sortir sur le site officiel (voir Vala Releases sur Gnome.org).

Debian Squeeze possède une version raisonnablement plus récente de Vala (0.8.1) – c'est donc cette version que j'ai préféré installer. À ce jour, Squeeze étant toujours unstable, cela impose de créer un système Debian mixte. Si vous craignez pour la stabilité de votre système, je vous suggère d'installer Vala dans un environnement virtuel. D'autant plus que cette installation impliquera aussi la mise à jour de composants essentiels de votre système comme libc6...

vala-dev:~# apt-get install -t=squeeze valac
The following packages will be upgraded:
  libc6 libgcc1 libncurses5 libncursesw5 libpopt0 libssl0.9.8 libstdc++6
  perl-base zlib1g
9 upgraded, 41 newly installed, 0 to remove and 121 not upgraded.
Need to get 50.2MB of archives.
After this operation, 133MB of additional disk space will be used.
Do you want to continue [Y/n]? y

Une fois l'opération terminée, le compilateur valac sera disponible sur votre système. Vous pouvez vous assurer de la version installée ainsi:

sylvain@vala-dev:~$ valac --version
Vala 0.8.1

Hello World

Comme il se doit, le premier programme que nous écrirons sera le fameux Hello World:

sylvain@vala-dev:~$ cat > hello.vala << EOF
/*
 Mon premier programme Vala
 */
void main(string[] args) {
     stdout.printf("Hello, world!\n");
}
EOF

Éléments de syntaxe

Habitués du C ou d'un de ses dérivés, ce code source devrait vous sembler familier. Ainsi, comme ses aînés, Vala utilise /* ... */ pour les commentaires multilignes (et // pour les commentaires sur une ligne). Les blocs sont délimités par des accolades ({ ... }). Les instructions se terminent par un point virgule (;).

On trouve aussi dans cet exemple certains mot-clé typiques du C: main, stdout, printf. Par contre leur utilisation est un petit peu différente:

void main(string[] args) {
void
Le type de retour du programme principal est void. Cela sous entend que le programme se terminera implicitement avec le code de retour 0. Comme en C, on aurait pu préférer le type de retour int et terminer le programme par un return explicite.
string[] args
Le tableau récupéré dans l'argument (optionnel) args contient les arguments de la ligne de commande. De façon similaire à ce qui se passe en Java, on peut accéder au nombre d'éléments dans le tableau en écrivant args.length, et aux arguments individuels avec la notation args[n]
stdout.printf("Hello, world!\n");
stdout.printf
Ici, on retrouve stdout qui correspond au flux de sortie standard d'un programme. Si le nom est similaire à celui utilisé en C, il s'agit dans le cas d'un programme Vala d'un objet sur lequel on peut invoquer des méthodes. Parmi celles-ci, printf pour afficher du texte formaté.

Compilation

Un programme source Vala se termine avec l'extension .vala. Une fois le code source précédent enregistré dans le fichier hello.vala, la compilation se réduit à la ligne suivante:

sylvain@vala-dev:~$ valac hello.vala

Cette commande accepte en argument le nom du code source Vala à compiler, et prend en charge toutes les étapes de la conversion du code en C jusqu'à sa compilation en code machine exécutable. Au final, un exécutable avec le même nom que la code source, mais sans extension, est généré dans le répertoire courant:

sylvain@vala-dev:~$ ls -l
total 12
-rwxr-xr-x 1 sylvain sylvain 5115 Feb  1 14:18 hello
-rw-r--r-- 1 sylvain sylvain   67 Feb  1 14:15 hello.vala

Ce programme peut ensuite être exécuté et produit le résultat tant attendu:

sylvain@vala-dev:~$ ./hello
Hello, world!

Examiner le code source généré

Si vous le souhaitez, vous avez la possibilité de demander au compilateur valac de ne faire que la phase de traduction en C. Considérons le code du programme Vala suivant:

// ccompatible.vala
public void MaMethodeVala() {
    stdout.printf("Hello from Vala\n");
}

L'option -C (C majuscule) permet d'obtenir le code C équivalent – sans générer l'exécutable. Le code C est enregistré dans un fichier de même nom que le code source Vala, mais avec l'extension .c (c minuscule):

sh$ valac -C ccompatible.vala
sh$ cat ccompatible.c
/* ccompatible.c generated by valac 0.11.5, the Vala compiler
 * generated from ccompatible.vala, do not modify */
#include <glib.h>
#include <glib-object.h>
#include <stdio.h>

void MaMethodeVala (void);

void MaMethodeVala (void) {
	fprintf (stdout, "Hello from Vala\n");
}

Appeler Vala à partir de C

De façon un peu complémentaire, vous avez aussi la possibilité de demander la compilation sans édition de lien (option -c minuscule) et la génération d'un fichier d'en-tête .h (option -H nom_de_fichier.h) pour interfacer votre code Vala avec du code C:

sh$ valac -c -H ccompatible.h ccompatible.vala
sh$ ls -l ccompatible.*
-rw-r--r-- 1 sylvain sylvain 220 Feb  8 17:31 ccompatible.h
-rw-r--r-- 1 sylvain sylvain  72 Feb  8 17:23 ccompatible.vala
-rw-r--r-- 1 sylvain sylvain 920 Feb  8 17:31 ccompatible.vala.o

Le .h généré peut ensuite être inclus dans un programme C pour utiliser les méthodes définies dans Vala. Et celles-ci seront liées grâce au fichier objet .vala.o produit par le compilateur:

# Exemple de programme C appelant du code Vala
sh$ cat main.c
#include <stdio.h>

#include "ccompatible.h"

int main() {
    printf("Calling from C\n"); 

    MaMethodeVala();

    return 0;
}

# Compilation et édition des liens avec le code objet généré par valac
sh$ gcc main.c ccompatible.vala.o \
           -I . \
           $(pkg-config --cflags --libs glib-2.0)

# Exécution du programme
sh$ ./a.out
Calling from C
Hello from Vala

pkg-config

L'utilisation de pkg-config permet de passer en argument au compilateur C les options nécessaires pour qu'il trouve les includes et les bibliothèques glib-2.0 indispensables à la compilation et à l'édition des liens.

Programmation orientée objets

Encapsulation, héritage

Vala supporte un modèle de programmation orientée objets basé sur des classes et similaire à celui utilisé en C++, Java ou C#. En plus de la notion de classe, Vala utilise aussi des interfaces (comme en Java), et les mixins à la manière de Scala. Mais nous laisserons cela de côté pour cette introduction [3].

Pour en revenir à la définition de classes utilisateurs et à l'instanciation d'objets, voici une autre version du Hello World mettant en avant quelques possibilités de Vala en terme de programmation orientée objets:

class Greetings {
    // Data member
    private string _name;
 
    // Contructeur
    public Greetings(string name) {
	this._name = name;
    }
 
    public string get_message() {
	return "Hello %s\n".printf(this._name);
    }
}
 
class AnonymousGreetings : Greetings {
    // Constructeur
    public AnonymousGreetings() {
	base("you");
    }
}
 
void main(string[] args) {
    Greetings greetings = (args.length > 1) ? new Greetings(args[1])
					    : new AnonymousGreetings();
 
    stdout.printf("%s", greetings.get_message());
}

Comme vous le voyez, les déclarations de classes ou de classes dérivées ressemblent fortement à celles de leurs équivalents C++ ou Java:

class Greetings {
    // ...
}
 
class AnonymousGreetings : Greetings {
    // ...
}

On retrouve aussi les spécificateurs de visibilité classiques public et private qui ont la même signification que dans les autres langages de la famille.

Petite divergence, pour invoquer le constructeur de la classe de base dans le constructeur d'une classe dérivée, on utilise une syntaxe proche de celle de Java, mais avec le mot-clé base au lieu de super:

class AnonymousGreetings : Greetings {
    // Constructeur
    public AnonymousGreetings() {
	base("you");
    }
}

Enfin, et nous y reviendrons un peu plus loin, comme on peut le voir, il y a allocation d'un nouvel objet avec l'opérateur new. Mais pas de libération explicite. En effet, Vala possède un mécanisme de gestion automatique de la mémoire. Par contre, à l'inverse de ses aînés Java et C#, ce mécanisme n'est pas basé sur un ramasse miette capable de determiner les objets innaccessibles. En effet, la gestion automatique de la mémoire en Vala est basée sur un compteur de référence.

Polymorphisme

Piège:

Si vous êtes programmeur Java ou C#, lisez cette section avec intérêt! En effet, la résolution des appels de méthodes en Vala est calquée sur celle du C++, et diffère donc de celle de votre langage préféré...

Comme beaucoup de langages orientés objets, Vala supporte le polymorphisme. C'est à dire qu'un même appel de méthode peut donner lieu à l'exécution d'un code différent selon l'objet sur lequel il est effectué. On parle parfois aussi de résolution dynamique des appels, puisque le code exact à exécuter est déterminé à l'exécution et non pas à la compilation. Sauf que ... comme C++, pour des raisons d'efficacité, Vala laisse au programmeur le choix d'utiliser soit la résolution dynamique des appels de méthode (à l'exécution). Soit la résolution statique (à la compilation). À l'inverse, en Java la résolution est toujours dynamique. Concrètement, quelle est la différence? Un exemple sera plus clair pour tout le monde. Tout d'abord, définissons deux classes: Animal qui sera la classe de base et Dog une classe dérivée. Comme vous le verrez dans le code qui suit, chacune de ces classes introduit deux définitions de méthodes:

class Animal {
    // *No* virtual keyword:
    // The (s)leep method is (s)tatic.
    public void sleep() {
	stdout.printf("%s\n", "Animal::sleep");
    }
 
    // *With* virtual keyword:
    // The (d)rink method is (d)ynamic.
    public virtual void drink() {
	stdout.printf("%s\n", "Animal::drink");
    }
}
 
class Dog : Animal {
    // You *should* use the 'new' keyword
    // to redefine static methods.
    public new void sleep() {
	stdout.printf("%s\n", "Dog::sleep");
    }
 
    // You *must* use the 'override' keyword
    // to override dynamic methods.
    public override void drink() {
	stdout.printf("%s\n", "Dog::drink");
    }
}

Si les deux définitions de classes se ressemblent beaucoup, vous avez vu aussi, comme c'est souligné en commentaires dans le code, que plusieurs mots-clés ont été utilisés. Ceux ci indiquent au compilateur la manière de mettre en œuvre ces méthodes et donc les caractéristiques dynamiques ou statiques de la résolution d'un appel.

Mot cléExempleSignification
public void sleep()Première définition dans la hiérarchie de classe d'une méthode dont les appels seront résolus à la compilation
virtual public virtual void drink()Première définition dans la hiérarchie de classe d'une méthode dont les appels seront résolus à l'exécution
new public new void sleep()Re-définition d'une méthode dont les appels seront résolus à l'exécution
Le mot-clé new est optionnel. Mais son absence produira cependant un avertissement (warning) à la compilation.
override public override void drink()Surcharge d'une méthode dont les appels seront résolus à l'exécution

Les choses sont peut-être encore mystérieuses pour vous. Regardons un peu le programme principal (main). Au niveau des sites d'appel rien ne distingue résolution dynamique et résolution statique:

void main() {
    Dog dog = new Dog();
    Animal    animal    = dog;
 
    dog.sleep();
    dog.drink();
 
    animal.sleep();
    animal.drink();
}

Comme vous le voyez, ces quatre appels de méthodes portent sur un seul et même objet. La seule différence est que pour deux d'entre eux, on laisse croire au compilateur que c'est un Dog. Et pour les deux autres, que c'est un Animal. Or l'exécution produit un résultat qui peut sembler surprenant:

Dog::sleep
Dog::drink
Animal::sleep
Dog::drink
drink – résolution dynamique
Appeler la méthode drink – résolue dynamiquement – entraîne bien l'appel à la méthode surchargée. Le code exact à exécuter a été déterminé au moment de l'exécution, que le compilateur ait vu le type réel de l'objet, ou un de ses types de base.
sleep – résolution statique
Appeler la méthode sleep – résolue statiquement – entraîne l'appel à la méthode déterminée en fonction du type visible à la compilation. Quand le compilateur voit un appel à sleep sur un Dog, il appelle la méthode Dog::sleep. S'il voit un appel à sleep sur un Animal, il appelle la méthode Animal::sleep.

Si cette subtilité est familière aux programmeurs C++, je ne saurais trop insister dessus pour ceux qui viennent du monde Java ou C# où tous les appels de méthode sont dynamiques!

Remarque:


Vala ne supporte que le polymorphisme par héritage. Héritage de classe, comme dans cet exemple. Ou héritage d'interface. Voir le tutoriel Vala pour plus de détails.

Gestion automatique de la mémoire

Je l'ai dit un peu plus haut, Vala possède un opérateur new pour instancier de nouveaux objets. Mais (en temps normal) le programmeur n'a pas à se charger d'appeler delete pour les détruire explicitement. Parfait, vont me dire les programmeurs Java: il y a un ramasse-miette (garbage collector) chargé de libérer les objets quand c'est nécessaire. Et bien non, pas tout à fait...

En C++

Avant d'apporter plus d'explications, permettez moi de faire un retour rapide sur certaines stratégies d'allocation des objets utilisées dans différents langages. Ainsi, en C++, les objets peuvent être créés sur la pile. À la manière des variables locales:

#include <string>
#include <fcntl.h>
 
using namespace std;
 
class File {
    public:
    File(string fName) { _fDesc = open(fName.c_str(), O_RDONLY); }
    ~File() { close(_fDesc); }
 
    /* ... */
 
    private:
    int	    _fDesc;
};

Dans cet exemple volontairement simpliste, je déclare une classe File qui ouvre un fichier dans son constructeur, et le ferme dans son destructeur. L'utilisation de cette classe dans un bloc de code peut se faire ainsi:

{
    /* ... */
 
    File    passwd("/etc/passwd");
    /*
       ...
       Utilisation de l'objet ''passwd''
       ...
     */
}

Le point qui m'intéresse ici est le suivant: l'objet passwd est alloué sur la pile. Au même titre que n'importe quelle variable locale en C++. Il va donc exister jusqu'à la fin du bloc où il est déclaré. Trivialement parlant, jusqu'à l'accolade de fermeture de ce bloc. Une fois celle-ci atteinte – et juste avant que l'objet ne soit dépilé – le destructeur est appelé. Ce qui ici aura pour conséquence de fermer le fichier ouvert à la construction de l'objet.

J'attire votre attention sur le fait que le destructeur sera forcément appelé juste avant de dépiler l'objet. Quelle qu'en soit la raison: que le bloc de code se termine normalement en atteignant l'accolade de fermeture, ou qu'il soit interrompu en route par une exception ou une instruction return, break, etc.

Autrement dit, avec une stratégie d'allocation des objets sur la pile, le compilateur et la run-time garantissent non seulement que chaque objet créé sera détruit une et une seule fois, mais en plus permettent de précisément savoir quand sera détruit l'objet. Puisque ce comportement est déterministe, il est possible de s'en servir pour contrôler l'allocation de ressources et pour s'assurer de leur libération dans tous les cas. Dans la littérature, cette technique est connue sous l'accronyme RIIA (Resource Acquisition Is Initialization).

De façon complémentaire, C++ propose aussi un mécanisme d'allocation des objets sur le tas à l'aide de l'opérateur new. Cette solution permet d'étendre la durée de vie d'un objet au delà du bloc où il est déclaré. Mais si le programmeur fait ce choix, il doit aussi assumer la responsabilité de détruire cet objet lorsqu'il ne sera plus utile. Cela se fait avec l'opérateur complémentaire delete.

En Java

L'oubli du delete, son utilisation multiple sur un seul objet ou encore son utilisation sur un objet encore susceptible d'être utilisé par le programme entraînent des bugs difficiles à localiser. Fort de cette constatation, en développant Java, Sun a préféré doter son langage d'un mécanisme de gestion automatique de la mémoire:

public class File {
    public File(String fName) { System.out.printf("Openning %s%n", fName); }
    public void close() { System.out.printf("Closing it%n"); }
}
 
class Demo {
    public static void main(String args[]) {
 
	File    passwd = new File("/etc/passwd");
 
	try {
	    /* ... */
	}
	finally {
	    passwd.close();
	}
    }
}

Dans cet exemple, vous observez l'allocation d'un nouvel objet Java dans le tas à l'aide de l'opérateur new. Il n'y a pas d'opérateur delete en Java, car la libération de l'objet sera dévolue au ramasse-miettes (garbage collector). Il s'agit d'un sous-système de la run-time Java chargé de déterminer quels objets ne sont plus accessibles par le programme, et de les éliminer pour libérer de la place en mémoire. Le hic, c'est que rien ne garantit quand sera exécuté le ramasse-miette, ni même s'il le sera. La conséquence étant qu'un objet n'est certainement pas libéré dès qu'il sera inaccessible. Ce qui rend le modèle de conception RIIA inapplicable en Java. Observez d'ailleurs que dans mon exemple, un appel explicite à close est nécessaire pour fermer le fichier. Observez également que, conformément à un idiome Java, cet appel est dans un bloc finally qui garantit qu'il sera exécuté quoi qu'il se passe dans le bloc try.

Piège:

Gardez cependant en mémoire que le compilateur garantit l'appel à cette méthode ... uniquement à condition que le programmeur ne l'ai pas oubliée!

Pour palier à cet oubli, une stratégie de programmation défensive consiste à utiliser le finalizer des objets Java pour libérer les ressources oubliées par le programmeur:

public class File {
    public File(String fName) { System.out.printf("Openning %s%n", fName); }
    public void close() { System.out.printf("Closing it%n"); }
 
    /* Pour les distraits ? */
    protected void finalize() throws Throwable {
        try {
            close();
        }
        finally {
            super.finalize();
        }
    }
}
 
class Bogus {
    public static void main(String args[]) {
 
        File    passwd = new File("/etc/passwd");
 
        /* ... */
        /* Oups: pas d'appel explicite à close() */
    }
}

Normalement, finalize est une méthode invoquée par le garbage collector juste avant de libérer un objet. C'est ce qui s'apparenterait le plus au destructeur du C++. Malheureusement, c'est oublier que le ramasse-miette peut très bien:

  • ne pas être invoqué avant (très, très) longtemps pendant l'exécution du programme,
  • ne pas être invoqué du tout,
  • être dans une situation telle qu'il lui soit impossible d'appeler finalize.

Pour prouver ces affirmations, vous pouvez vous amuser à compiler et exécuter le programme donné au début de cet encadré. Sur ma machine et avec OpenJDK 6, voici ce que cela donne:

sh$ javac File.java
sh$ java Bogus
Openning /etc/passwd

Et oui: le fichier n'est jamais fermé. Ce n'est pas forcément très grave ici puisque le système d'exploitation s'en chargera lorsque le processus sera terminé. Mais quid d'un serveur qui va tourner pendant des jours entiers? Ou si la ressource à libérer n'est pas gérée par le système d'exploitation?

En résumé, retenez que le finalizer Java n'est pas un destructeur!

Et pour Vala?

Après cette longue digression, qu'en est-il pour Vala? Le choix fait par les développeurs de ce langage est d'essayer d'obtenir le meilleur des deux mondes. A savoir garantir l'appel d'un destructeur dès qu'un objet est inaccessible, et donc permettre l'utilisation du modèle de conception RIIA. Tout en donnant la liberté d'étendre la durée de vie d'un objet au dela du bloc où il est alloué (allocation dynamique avec new). Et enfin en dispensant le programmeur d'appeler explicitement delete lorsque l'objet n'est plus accessible.

Vala reference counter.png

Chaque référence à un objet Vala incrémente son compteur de références.


Vala cyclic references problem.png

Quand une référence disparaît, le compteur de référence de l'objet est décrémenté. Quand ce compteur atteint 0, l'objet n'est plus référencé et peut être détruit. Le défaut de ce système est qu'il ne peut pas gérer les références circulaires. Dans ce cas, des objets vont rester alloués dans le tas alors qu'ils ne sont plus accessibles.


Comment atteindre ce tour de force? L'idée de la gestion automatique de la mémoire par Vala repose sur un compteur de références. À chaque fois qu'une référence à un objet est ajoutée, le compilateur Vala ajoute le code nécessaire pour incrémenter son compteur d'utilisation. À chaque fois qu'une variable cesse de référencer un objet, le compteur de référence de celui-ci est décrémenté. Quand – ou plutôt, dès que ce compteur atteint 0, le destructeur de l'objet est appelé:

class File {
    public File(string fName) { stdout.printf("Opening %s\n", fName); }
    ~File() { stdout.printf("Closing it\n"); }
}
 
void main() {
    File    passwd = new File("/etc/passwd");
 
    /* ... */
}
sh$ valac destructor.vala
sh$ ./destructor 
Opening /etc/passwd
Closing it

Formidable! Pourquoi tout le monde ne fait pas comme cela? Ce choix a aussi un défaut dont il faut être averti: si deux objets se référencent mutuellement, leurs compteurs de références respectifs ne pourront jamais revenir à 0. Même s'ils ne sont plus référencés par personne d'autre:

class Node {
    public Node child { get; set; }
    public string name { get; private set; }
 
    public Node(string name) {
	this.name = name;
	stdout.printf("Constructing %s\n", name);
    }
    ~Node() { stdout.printf("Destructing %s\n", this.name); }
}
 
class Child : Node {
    public Node parent { get; private set; }
 
    public Child(string name, Node parent) {
	base(name);
	this.parent = parent;
	if (parent != null)
	    parent.child = this;
    }
    ~Child() { stdout.printf("Destructing child"); }
}
 
void main() {
    Node    orphan  = new Node("orphan");
    Node    parent  = new Node("parent");
    Child   child   = new Child("child",parent);
}
sh$ $ ./cyclic
Constructing orphan
Constructing parent
Constructing child
Destructing orphan

À l'exécution, on peut observer que l'objet orphan est bien détruit. Mais ni parent ni child puisque ceux-ci se référencent mutuellement – et que leur compteur de références ne repassera jamais à 0 pendant l'exécution du programme.

Plus formellement, l'utilisation d'un compteur de références ne fonctionne plus quand il existe des références cycliques entre les objets du programme. Autrement dit, pour garantir le fonctionnement de ce système, les références des objets doivent former un graphe orienté acyclique (un dag). Dans la pratique, c'est souvent le cas. Mais attention à certaines structures de données comme les listes doublement chaînées qui vont à l'encontre de cette contrainte. Pour résoudre ce problème, Vala propose des références faibles (weak) qui n'ont aucune incidence sur le compteur de références. Pour plus de détail, je vous encourage vivement à lire l'article correspondant sur le site de Vala: Vala's Memory Management Explained.

Multiparadigme

Vala fait parti de la mouvance des langages multiparadigmes – à la manière de Scala, par exemple. C'est à dire que si Vala supporte un style de programmation impérative orientée objets, le langage ne l'impose pas. Il est tout à fait possible de faire de la programmation procédurale en Vala, comme on le ferait en C.

De la même manière, Vala incorpore certains éléments de programmation fonctionnelle comme les fermetures (closure). Celles-ci sont des éléments de programmation qui ont longtemps été ignorés par les langages impératifs, mais qui depuis quelques temps deviennent un must have pour tout nouveau langage. Les fermetures permettent de manipuler des blocs de code anonymes comme des données. Et donc permettent de les passer en argument lors d'appels de fonctions ou de méthodes. Pensez par exemple à une fonction de tri, qui accepterait en argument le code nécessaire pour comparer deux valeurs.

Fonctions anonymes et delegate

Dans l'introduction de cette section, je faisais référence à l'utilisation de fermeture pour comparer deux valeurs. Prenons un exemple. Le code suivant – qui ne contient pas encore de fermeture – créé deux objets qui représentent deux enfants, avec leur âge et leur taille:

class Kid {
    // C# style properties
    public string name { get; private set; }
    public int age { get; private set; }
    public int height { get; private set; }
 
    public Kid(string name, int age, int height) {
        this._name = name;
        this._age = age;
        this._height = height;
    }
}
 
void main() {
    Kid[] kids = {
        new Kid("Mike", 12, 155),
        new Kid("Carol", 14, 148)
    };
 
    // ...
}

Maintenant, imaginions que nous souhaitions pouvoir comparer ces deux enfants pour savoir qui est le plus grand. Ce qui peut vouloir dire deux choses: « lequel est le plus agé? » ou « lequel a la plus grande taille? ». Toujours sans fermeture, une solution serait d'écrire deux fonctions de comparaison:

void greater_in_age(Kid a, Kid b) {
    if (a.age > b.age)
	stdout.printf("%s is greater than %s\n", a.name, b.name);
    else if (b.age > a.age)
	stdout.printf("%s is greater than %s\n", b.name, a.name);
    else
	stdout.printf("%s and %s are both the same\n", a.name, b.name);
}
 
void greater_in_height(Kid a, Kid b) {
    if (a.height > b.height)
	stdout.printf("%s is greater than %s\n", a.name, b.name);
    else if (b.height > a.height)
	stdout.printf("%s is greater than %s\n", b.name, a.name);
    else
	stdout.printf("%s and %s are both the same\n", a.name, b.name);
}

Effectivement, cela fonctionne. Mais observez le code: greater_in_age et greater_in_height sont exactement les mêmes fonctions, hormis le test effectué dans le if. Dans un cas, on compare sur l'age. Dans l'autre sur la taille. Pour lutter contre cette redondance de code, et pour ajouter de la souplesse en autorisant la définition de comparaison sur d'autres critères, on peut passer en paramètre une expression de comparaison:

delegate bool KidComparator(Kid a, Kid b);
 
void greater_than(Kid a, Kid b, KidComparator comp) {
    if (comp(a, b))
        stdout.printf("%s is greater than %s\n", a.name, b.name);
    else if (comp(b, a))
        stdout.printf("%s is greater than %s\n", b.name, a.name);
    else
        stdout.printf("%s and %s are both the same\n", a.name, b.name);
}

Dans ce code, le mot-clé delegate introduit un nouveau type représentant une méthode. Programmeur C/C++, pensez-y comme à un pointeur de fonction. Le type de retour et les paramètres de KidComparator forment la signature des méthodes compatibles avec ce type.

Vous constatez ensuite que la méthode greater_than est maintenant générique, et accepte en argument un comparateur qui sert pour les tests.

Examinons enfin le site d'appel:

void main() {
    Kid[] kids = {
        new Kid("Mike", 12, 155),
        new Kid("Carol", 14, 148)
    };
 
    stdout.printf("Age:\n");
    greater_than(kids[0], kids[1], (a, b) => { return a.age > b.age; });
 
    stdout.printf("Height:\n");
    greater_than(kids[0], kids[1], (a, b) => { return a.height > b.height; });
}

Remarquez que maintenant, la fonction de comparaison est passée directement en ligne lors de l'appel. Ce code anonyme est une fermeture. Pour la syntaxe exacte, je vous renvoie sur le tutoriel de Vala.

Fermetures

Mais les fermetures dans Vala ne sont pas que du sucre syntaxique autour des pointeurs de fonctions du C. Ce sont de véritables fermetures – avec l'accès aux variables locales du contexte de la définition:

delegate int RandomGenerator();
 
RandomGenerator trick_dice(int result) {
    return () => { return result; };
}
 
RandomGenerator fair_dice() {
    return () => { return (int)Random.int_range(1,7); };
}
 
void main() {
    RandomGenerator dice_0 = fair_dice();
    RandomGenerator dice_1 = trick_dice(1);
    RandomGenerator dice_2 = trick_dice(6);
 
    stdout.printf("%d %d %d\n", dice_0(), dice_1(), dice_2());
}

Ici, trick_dice et fair_dice sont deux méthodes qui renvoient un générateur pseudo-aléatoire sous la forme d'une lambda-expression. Même si ce n'est pas vraiment ce que fait le compilateur Vala, pour faciliter la compréhension, on peut considérer dans un premier temps qu'à chaque appel de ces méthodes, une nouvelle méthode est dynamiquement générée. Cette manière de voir les choses prend toute sa signification pour trick_dice. Comme vous le voyez, cette méthode renvoie une fonction anonyme qui elle-même renvoie l'argument passé lors de sa création. C'est à dire, que l'appel trick_dice(1) renvoie une méthode qui lors de son appel renverra toujours 1. De la même manière, l'appel trick_dice(6) renvoie une méthode qui lors de son appel renverra toujours 6. Tout ceci est confirmé par l'exécution du programme ci-dessus:

sylvain@vala-0_11_5:~$ valac curry.vala
/home/sylvain/curry.vala.c: In function 'trick_dice':
/home/sylvain/curry.vala.c:66: warning: assignment from incompatible pointer type
sylvain@vala-0_11_5:~$ ./curry
4 1 6
1 1 6
6 1 6

Warning: assignment from incompatible pointer type

Il semble qu'il existe un bug dans le compilateur Vala qui génère un code produisant un warning à la compilation. [4]

Visiblement, ce warning est sans conséquence sur le code. Je suis d'accord que ce n'est pas très propre ainsi. Mais une prochaine version de Vala devrait résoudre ce problème.

Dans l'entre-temps, si vous voulez absolument cacher les warnings du compilateur C, vous pouvez utiliser l'option -X -w:

sylvain@vala-0_11_5:~$ valac -X -w curry.vala

J'insiste bien sur le fait que cela va cacher les warnings du compilateur C chargé de transformer le code C intermédiaire en programme exécutable – pas ceux du compilateur Vala à proprement parlé et produisant le code C.

Performances

Pour conclure ce tour d'horizon de Vala, un mot sur les performances du langage. C'est souvent un sujet prétexte à d'interminables discutions, et je ne veux pas trop entrer dans le débat ici. Vous trouverez sur internet quelques comparatifs entre Vala et d'autres langages proches [5] [6] [7]. Globalement, les résultats montrent que les performances sur la partie opérationelle du code (calcul intensif) sont très proches de celles d'un programme écrit en C. À l'inverse, Vala semble pénalisé sur les opérations liées au langage (création/destruction d'objets, appel de méthode d'interface, vérification de type, déclenchements de signaux). Une partie de ces résultats peuvent être imputé à l'infrastructure GObject (GLib Object System) sur laquelle Vala s'appuie. Un autre projet Gnome, Dova permettrait en principe d'améliorer ces aspect des choses.

Que conclure de ces résultats mitigés? Que Vala n'est pas le langage à choisir pour écrire la boucle critique d'une application temps réel? Peut-être, mais ce n'est clairement pas l'objectif premier de ce langage. Qu'il est inadapté pour l'écriture d'un serveur? Il faudrait tester, mais je ne pense pas que Vala soit si mal placé que ça dans ce cas: en effet, les performances sur les threads et surtout l'absence de surcoûts liés à l'infrastructure (serveur d'application, objets managés à la EJB) devrait compenser ses points faibles. Quand aux programmes clients lourds ou aux applications graphiques, l'intégration de Gtk sera un plus.

De toute façon, étant donné la puissance du matériel actuel, les performances brutes d'un langage ou d'une run-time sont finalement un point mineur comparé à la facilité de développement et à la possibilité d'écrire du code clair et structuré. Ce qui réduit aussi le nombre de bugs potentiels, facilite la maintenance, et améliore la qualité globale du logiciel produit. D'ailleurs le succès des langages de scripts (Python, Ruby, etc.) prouvent bien que les performances brutes ne sont pas tout!

Toujours est-il qu'avec ses qualités, Vala méritait bien un petit coup de projecteur. Et je vous encourage fortement à tester ce langage pour vous faire votre propre opinion!

Ressources

Vala

Mono