La DLR: une autre CLR ?

   Auteur : Lionel Laské (llaske@c2s.fr)

Pourquoi une DLR ?
    DLR = CLR + ?
    C'est quoi un langage statique ?
    C'est quoi un langage dynamique ?
    Pourquoi des langages dynamiques en .NET ?
    C'est quoi la DLR ?
Comment créer une DLR ?
    Mon langage dynamique: MyJScript
    MyDLR: Interface
    Méthodes d'extension
    MyDLR sur la CLR
    Conversion de type: comment faire ?
    Conversion de type: comment l'utiliser ?
    Création d'instance
    Un nouveau langage dynamique: Liogo
    Conclusion
Pour aller plus loin
    MyDLR
    DLR
    IronPython
    Compilateur
    JavaScript
    Liogo
    PowerShell

Dans cet article, je vous propose de découvrir la DLR de Microsoft. Mais comment parler de quelque chose qui n'existe pas encore ? Pour cela, ma présentation se fera en deux étapes: une première partie qui explique l'objectif et les concepts de la DLR et une deuxième partie qui se propose d'expérimenter ces concepts à travers l'implémentation d'une DLR inspirée des éléments connus aujourd'hui. A chacun de choisir son niveau de lecture: la première partie pour une simple découverte, la deuxième partie pour se plonger dans le code.

Pourquoi une DLR ?

DLR = CLR + ?

CLR signifie Common Language Runtime. La CLR c'est l'environnement minimal qui permet d'exécuter un programme .NET.

Mais au fait quels sont ces "Language"s dont parle le mot CLR ? C'est bien sûr le C#, le VB.NET, le C++, le J#, le JScript.NET, … Et qu'est ce qu'ils ont en "Common" ces langages ? Et bien à dire vrai au départ, il n'y avait pas beaucoup de choses en commun entre un langage objet et fortement typé comme le C#, un langage souple comme le VB (celui du temps de VB6), un langage à pointeurs comme le C++, et un langage de script comme le JavaScript. Pourtant en lançant .NET 1.0, Microsoft a décidé de les unifier. Parfois au prix de quelques ajustements syntaxiques ou structurels, Microsoft a également décidé qu'ils partageront cette fameuse CLR. Du coup, Microsoft propose une situation de rêve où tous les développeurs se retrouvent sur la même plate-forme: .NET et peuvent même interopérer (du code C# qui appelle du code VB.NET). La réponse de Microsoft au fameux "un langage pour toutes les plates-formes: Java" c'est bien "n'importe quel langage mais sur une seule plate-forme: .NET". D'ailleurs, personne ne s'y est trompé, le J# était bien là pour essayer de séduire les développeurs Java.

Mais alors si la CLR est là pour regrouper tous les langages, quel besoin y a-t-il de créer une "DLR" ? Et pourquoi une Dynamic Language Runtime ? Dans sa volonté d'uniformiser les langages, Microsoft aurait-il fait l'impasse sur certains langages ? La "Common" Language Runtime n'est-elle pas aussi pour les langages "Dynamiques" ? Voilà exactement les questions auxquelles je vais essayer de répondre.

C'est quoi un langage statique ?

Les langages de la CLR ont tous un typage "statique". Cela veut simplement dire que dés la compilation on peut déterminer quels sont les types de données manipulés par votre application. Forcément, lorsque vous utilisez une variable, vous devez au préalable la déclarer et donc lui donner un type.

Bien sûr, par les mécanismes d'héritage ou par l'utilisation d'interfaces vous pouvez éviter de manipuler le type réel des objets mais, quoi qu'il en soit, le compilateur peut toujours déterminer le type des objets que vous manipulez. Et le compilateur a un but précis: il veut s'assurer que vous utilisez correctement un objet, c'est-à-dire s'assurer que vous appelez une méthode ou une propriété que l'objet supporte.

D'ailleurs le .NET Framework 1.1 souffrait d'un manque de typage sur les collections, il était facile de déclarer un "ArrayList" et d'y mettre tout et n'importe quoi pour bluffer le compilateur et, accessoirement, planter l'application à l'exécution. C'est précisément pour éviter cet inconvénient que le .NET Framework 2.0 nous propose maintenant la notion de générique.

Bref, la CLR impose bien un typage "statique".

C'est quoi un langage dynamique ?

Dans un langage dynamique, impossible de déterminer avant l'exécution de quels types vont être les objets. Pire, dans les langages dynamiques, on peut souvent créer "à la volée" de nouveaux types.

Regardez les quelques lignes de JavaScript ci-dessous:

    function Cat(name) {
        this.name = name;
        this.miaow = function () { alert(this.name+' said Miaow'); };
    }
    
    x = "Hello";
    alert(x);
    x = new Cat('Felix');
    x.miaow();

Impossible de déterminer à la compilation le type de la variable "x". Impossible non plus de vérifier que l'appel de la méthode "miaow" est valide. En fait, comme nous le verrons plus loin, la notion même de classe "Cat" n'a pas de sens en JavaScript avant l'appel du "new". On ne peut pas faire plus dynamique !

JavaScript n'est pas une exception, beaucoup de langages sont dans le même cas. Essayons la même chose en python:

    class Dog:
        def __init__(self, name):
            self.name = name
        def waof(self):
            print self.name,'said Waof'
            
    y = 'Hello'
    print y
    y = Dog('Snoopy')
    y.waof()
    del Dog

Ici aussi, la variable "y" change de type dynamiquement étant d'abord une chaine puis un objet "Dog". Autre fonctionnalité intéressante ici, la commande "del Dog" qui supprime la définition de la classe. Autrement dit, après l'exécution de cette commande, la classe "Dog" n'existe plus et ne peut plus être instanciée.

C'est effectivement une caractéristique qu'on retrouve dans les langages dynamiques, les types sont créés dynamiquement en mémoire et n'existent que pendant l'exécution du programme. C'est toute la différence avec un langage statique où les types sont "gravés dans le marbre", par exemple dans une assembly .NET.

Pourquoi des langages dynamiques en .NET ?

.NET a-t-il besoin de langages dynamiques ? C'est une question qui prête à polémiques tant le choix d'un langage est surtout affaire de gout. On peut néanmoins dégager quelques éléments de réponses.

Les technologies du web se prêtent bien à l'utilisation des langages dynamiques. Parce que le plus souvent les écrans sont décrits de manière dynamique et que les données peuvent également être dynamiques il est séduisant de les manipuler avec un langage dynamique. De plus les langages dynamiques s'adaptent bien au contraintes d'exécution dans un SandBox pour limiter les risques d'exécution d'un code malveillant ou aux restrictions de chargement de code binaire qu'imposent souvent les navigateurs. La preuve: la DLR est l'un des socles technique du très sexy SilverLight.

Les langages dynamiques ne cessent de séduire les développeurs. Pour s'en convaincre, il suffit de voir l'importance des communautés autour de Ruby, de Python, de PHP ou de Perl. Notamment parce que leur syntaxe est simple et facile à comprendre et qu'ils sont donc faciles à apprendre. Microsoft ne peut ignorer ces communautés alors même que, comme nous l'avons dit, la CLR a précisément été imaginée pour être multi-langages.

Les nouvelles fonctionnalités que Microsoft intègre au Framework nécessitent un certain dynamisme. La preuve évidente: LINQ. Regardons l'exemple suivant. Il s'agit d'une requête LINQ en C# sur une classe "Doctor":

    var query = from d in doctors
        where d.City == "Chicago"
        select new { d.GivenFirstName, d.FamilyLastName };

Quelle est la classe des objets retournés par la requête ? Ce ne sont pas des "Doctor" puisqu'il n'y a qu'un nom et qu'un prénom. En fait, il s'agit d'une nouvelle classe créée dynamiquement pour le besoin de cette requête. Alors pour être précis, il ne s'agit pas vraiment d'un type dynamique puisque le compilateur créé un nouveau type statique à la compilation en analysant l'expression. Mais avouez quand même qu'on n'est pas très loin d'un langage dynamique !

Voilà qui devrait vous convaincre que les langages dynamiques ont bien leur place en .NET.

C'est quoi la DLR ?

Un langage dynamique en .NET a nécessairement besoin d'accéder à des classes de la CLR. Dans l'exemple ci-dessous en python, on affiche une fenêtre WPF :

    w = Window()
    w.Title = "WPF from IronPython"
    w.Show()

Pour que cela fonctionne, il faut pouvoir manipuler tous les objets de la CLR depuis un langage dynamique comme python. L'une des fonctionnalité de la DLR est de proposer des mécanismes génériques permettant l'appel dynamique de n'importe quel classe de la CLR ou de vos assemblys. Nous verrons comment cela est possible plus loin mais l'idée est de pouvoir simplement créer un objet, appeler une méthode et manipuler une propriété.

Dans les premiers exemples ci-dessus, nous avons créé une classe "Cat" en JavaScript et une classe "Dog" en python. Et si je souhaitais créer un programme qui gère une ménagerie et donc que j'avais besoin de manipuler ces deux objets dans le même programme ? Il est nécessaire dans ce cas de permettre une interopérabilité des langages dynamiques: ici entre python et JavaScript. Autrement dit, tous les langages doivent partager les notions élémentaires que sont, encore une fois: créer un objet, appeler une méthode ou manipuler une propriété. De la même manière que la CLR propose l'interopérabilité des langages statiques, la DLR propose l'interopérabilité des langages dynamiques.

Autre objectif de la DLR: faciliter la création d'un nouveau langage dynamique. En effet, créer son propre langage nécessite toujours les mêmes étapes: écrire un analyseur lexical, écrire un analyseur syntaxique, construire un arbre syntaxique, … Bref, plusieurs étapes sont génériques, la DLR apporte donc un certain nombre de classes helper afin d'aider à la réalisation de ces différentes tâches.

Comment créer une DLR ?

Maintenant que les concepts ont été abordés, essayons de créer notre DLR. En effet, comment mieux comprendre ce qu'est la DLR qu'en essayant d'en implémenter une ?

Mon langage dynamique: MyJScript

Mais pour illustrer la DLR, il faut d'abord disposer d'un langage dynamique. Notre première est étape est donc de créer un langage dynamique. Pour cela, nous allons nous inspirer du JavaScript qui intègre tous les concepts nécessaires à notre démonstration. Faute de temps néanmoins, nous nous limiterons à en conserver uniquement les fonctionnalités du runtime et oublieront le parsing proprement dit.

En JavaScript, un objet est simplement un conteneur, plus précisément c'est un dictionnaire de couples: nom, valeur. Une bonne manière de le démontrer est d'observer sa représentation JSON (JavaScript Object Notation). Ainsi, par exemple l'objet "Cat" manipulé précédemment peut être déclaré traditionnellement:

    function Cat(name) {
        this.name = name;
        this.miaow = function () { alert(this.name+' said Miaow'); };
    }

    x = new Cat('Felix');

ou en utilisant uniquement JSON:

    x = { "name":"Felix", "miaow": function() { alert(this.name+' said Miaow'); } }

Les deux représentations sont identiques et donnent le même résultat. En JavaScript, il n'y a donc pas de déclaration préalable d'une classe, de ses variables d'instances et de ce méthodes d'instance comme dans un langage statique. Un objet JavaScript se constitu au fur et à mesure qu'on affecte ses membres: variables ou méthodes. La syntaxe "new Constructor(…)" du JavaScript n'est en fait qu'un sucre syntaxique qui créé un objet vide puis appelle le constructeur. Ainsi, une troisième manière d'initialiser notre objet est:

    x = {};
    Cat.call(x, 'Felix');

Au passage, nous remarquons qu'une fonction JavaScript peut être manipulée comme une valeur. En fait, une fonction n'est qu'un objet particulier supportant une méthode spécifique: "call" qui prend en paramètre l'objet représentant this et les paramètres du constructeur. C'est d'ailleurs parce qu'une fonction est une valeur que nous pouvons affecter la méthode d'instance "miaow" dans notre classe.

Pour les besoins de notre moteur JavaScript, que nous appellerons MyJScript, nous considérerons également types de données: String et Number qui correspondent aux classes de base du même nom en JavaScript. Ces classes dérivent également de notre classe objet JavaScript et y ajoutent des méthodes prédéfinies.

Voilà donc un schéma de l'implémentation du runtime du MyJScript:

Voici maintenant un extrait de l'implémentation en C# de la classe MJSObject représentant un objet MyJScript. On utilise un dictionnaire pour stocker les membres: variables ou méthodes. Vous constaterez qu'il n'y a pas de nom de la classe, en fait ce concept n'existe pas réellement en JavaScript, seul le nom du constructeur est manipulé.

        public class MJSObject
        {
            Dictionary<string, MJSObject> members;
    
            public MJSObject()
            {
                members = new Dictionary<string, MJSObject>();
            }
    
            public virtual void Set(string name, MJSObject value)
            {
                if (members.ContainsKey(name))
                    members.Remove(name);
    
                members.Add(name, value);
            }
    
            public virtual MJSObject Get(string name)
            {
                if (!members.ContainsKey(name))
                    return null;
    
                return members[name];
            }
    
            …
        }

Continuons par l'implémentation d'une fonction MyJScript. Elle dérive de la classe MJSObject, elle peut donc elle-même avoir des membres (si, si: faites l'essai en JavaScript vous verrez) mais stocke en plus les instructions correspondant à la fonction. Comme nous ne voulons pas écrire un interpréteur pour notre exemple, nous utiliserons un delegate pour l'implémentation. Dans la réalité, l'interpréteur JavaScript stocke les instructions dans une chaine de caractères. N.B.: Ne soyez pas effrayé par la syntaxe "@this", le préfixe "@" en C# permet simplement d'appeler une variable comme un mot clé du langage.

        public class MJSFunction : MJSObject
        {
            public delegate MJSObject FunctionProcess(MJSObject @this, params MJSObject[] args);
    
            FunctionProcess instructions;
    
            public MJSFunction(FunctionProcess instructions)
            {
                this.instructions = instructions;
            }
    
            public MJSObject Call(MJSObject @this, params MJSObject[] args)
            {
                if (instructions != null)
                    return instructions(@this, args);
    
                return null;
            }
            …
        }

Si on regarde la méthode "Call", elle correspond bien à celle du JavaScript. Elle reçoit un objet qui est le "this" utilisé dans le corps de la méthode, et une liste d'arguments.

Faisons également un zoom sur l'une de nos classes de base: MJSString. Elle hérite simplement de la classe MJSObject et y rajoute la valeur et les méthodes et propriétés standards du Framework JavaScript qui permettent de la manipuler. Elle redéfinie également la méthode "Set" afin d'interdire la redéfinition ou l'ajout de nouvelles propriétés.

        public class MJSString : MJSObject
        {
            string value;
    
            public MJSString(string value)
            {
                this.value = value;
                base.Set("length", new MJSNumber(value == null ? 0 : value.Length));
                base.Set("blink", new MJSFunction(PrimitiveBlink));
                base.Set("bold", new MJSFunction(PrimitiveBold));
                base.Set("substr", new MJSFunction(PrimitiveSubstr));
            }
    
            public override void Set(string name, MJSObject value)
            {
            }
            …
        }

Pour compléter notre langage, il manque encore un point: le contexte global. En effet, en JavaScript tout ce qui est défini de manière globale est en fait défini comme membre d'un objet global. Ainsi, une variable ou une fonction déclarée globale est en fait attachée à cet objet global. En voici un premier exemple à lancer en dehors de toute fonction:

    x = 'Hello';
    alert(this.x);     // Affiche Hello

Encore plus fort, transformons notre objet global en un objet "Cat" (à éviter dans la réalité). Pour cela, il nous suffit d'appeler le constructeur "Cat()" qui n'est rien d'autre qu'une variable d'instance de l'objet global:

    this.Cat('Felix');
    alert(name);    // Affiche Felix
    miaow();        // Affiche Felix said Miaow

Pour représenter ce contexte global en MyJScript, il nous suffit d'utiliser une classe qui stocke un singleton. Voilà ce que cela donne:

        public class MJSContext
        {
            static MJSObject @this = null;
    
            public static MJSObject GetContext()
            {
                if (@this == null)
                    @this = new MJSObject();
                return @this;
            }
        }

Voilà, notre moteur est terminé. Reprenons l'exemple JavaScript du départ:

    function Cat(name) {
        this.name = name;
        this.miaow = function () { alert(this.name+' said Miaow'); };
    }
    
    s = "Hello";
    x = new Cat('Felix');
    x.miaow();

Voyons la suite d'instructions qui s'exécuterait dans notre moteur en réponse à ces quelques lignes.

    // Enregistre la définition de la méthode globale "Cat"
    MJSContext.GetContext().Set("Cat", new MJSFunction(CatInstructions));
    
    // Stocke la chaine "Hello" dans la variable globale "s"
    MJSContext.GetContext().Set("s", new MJSString("Hello"));
    
    // Créé un nouvel objet et appelle la méthode globale "Cat" pour l'initialiser
    MJSObject newObject = new MJSObject();
    MJSFunction constructor = MJSContext.GetContext().Get("Cat") as MJSFunction;
    constructor.Call(newObject, new MJSString("Felix"));
    MJSContext.GetContext().Set("x", newObject);
    
    // Appelle la méthode miaow de la variable globale "x"
    MJSObject x = MJSContext.GetContext().Get("x");
    MJSFunction method = x.Get("miaow") as MJSFunction;
    method.Call(x);

Evidemment c'est un peu long à écrire mais c'est assez fidèle aux ordres qui doivent être exécutées par un interpréteur JavaScript.

Cette implémentation du MyJScript doit déjà vous convaincre que nous sommes en présence d'un langage dynamique:

  • Les variables peuvent changer de types ("x" ici),
  • Les propriétés et les méthodes sont créées "à la volée" ("x", "Cat" et "miaow" ici),
  • Les classes n'existent qu'on moment de leur appel ("Cat" ici).

MyDLR: interface

Le premier objectif de la DLR est de permettre l'appel dynamique des objets d'un langage dynamique. Plus précisément, l'objectif est de pouvoir appeler une méthode ou une propriété dynamiquement. Pour implémenter notre DLR (que nous appellerons MyDLR), nous regroupons ces fonctionnalités dans une interface appelée ICallable.

        public interface ICallable
        {
            object CallMethod(string name, params object[] args);
            object GetProperty(string name);
            void SetProperty(string name, object value);
        }

Tout objet utilisable par notre DLR devra supporter cette interface. Le langage MyJScript étant le premier langage à supporter MyDLR, modifions donc son implémentation pour supporter cette interface. Nous ajoutons l'interface ICallable à la définition de notre classe MJSObject.

        public class MJSObject : ICallable
        {
            …
        }

Commençons par implémenter l'appel de méthode. Pour cela on recherche une propriété ayant le nom de la méthode, on la converti en MJSFunction (pour simplifier, je ne traite pas les cas d'erreurs ici), on converti les paramètres (je laisse de côté ce point pour l'instant) et on lance simplement l'appel sur l'objet courant.

            public object CallMethod(string name, params object[] args)
            {
                MJSObject value = Get(name);
                MJSFunction method = value as MJSFunction;
    
                // Convertir chaque paramètre en MJSObject
                int length = args.Length;
                MJSObject[] mjsargs = new MJSObject[length];
                for (int i = 0; i < length; i++)
                {
                    …
                }
    
                return method.Call(this, mjsargs);
            }

La méthode GetProperty est encore plus simple puisqu'il suffit de récupérer une variable d'instance. A l'exception de la conversion que nous reverrons plus loin, la méthode SetProperty est également triviale.

            public object GetProperty(string name)
            {
                return Get(name);
            }
    
            public void SetProperty(string name, object value)
            {
                // Convertir la valeur en MJSObject
                MJSObject mjsvalue;
                mjsvalue = …;
                
                Set(name, mjsvalue);
            }

Avec cette nouvelle interface, nous pouvons donc manipuler dynamiquement les objets créés dans l'exemple précédent:

    Console.WriteLine(s.GetProperty("length"));           // Affiche 5
    Console.WriteLine(s.CallMethod("substr", 1, 2));      // Affiche el
    Console.WriteLine(x.GetProperty("name"));             // Affiche Felix
    x.CallMethod("miaow");             // Appel la méthode miaow

Méthodes d'extension

Manipuler dynamiquement des objets dynamiques est amusant mais une des promesses de la DLR est de pouvoir manipuler également des objets de la CLR. Dans l'idéal, nous souhaiterions donc pouvoir écrire quelque chose comme ça:

    String s = "Hello";
    Console.WriteLine(s.GetProperty("Length")); 
    Console.WriteLine(s.CallMethod("ToLower")); 

Mais pour faire cela, il nous faudrait d'abord pouvoir définir l'interface ICallable pour tous les types de la CLR.  Impossible ? Non, car c'est pratiquement ce que nous allons faire en utilisant une nouveauté du C# 3.0: les méthodes d'extension.

Une méthode d'extension est une nouveauté syntaxique du C# 3.0 qui permet d'ajouter des méthodes à des classes sans en changer l'implémentation interne. Dans le cas qui nous intéresse, nous souhaiterions que tous les objets de la CLR se comportent comme s'ils disposaient des méthodes de notre interface ICallable. Pour cela, voilà la nouvelle classe que nous devons créer en C# 3.0:

        public static class DynamicObjectExtension
        {
            public static object CallMethod(this object @this, string name, params object[] args)
            {
                ...
            }
            public static object GetProperty(this object @this, string name)
            {
                …
            }
            public static void SetProperty(this object @this, string name, object value)
            {
                …
            }
        }

Noter la syntaxe de cette déclaration: nous créons une classe "statique" (!) avec des méthodes statiques dont le premier paramètre est de type "this object". Cette syntaxe surprenante signifie en fait que nous souhaitons rajouter des méthodes sur la classe "object" de la CLR. Si vous intégrez ces définitions dans votre projet (directement ou via une assembly), vous pouvez désormais appeler ces méthodes sur tout objet de type "Object" ou d'une de ses classes dérivées comme si elles étaient réellement membre de la classe. Attention, cela n'a rien de magique, nous ne modifions pas réellement la classe "Object" de la CLR. Il s'agit simplement d'un sucre syntaxique du compilateur. En fait un appel de:

    s.GetProperty("Length");

Est simplement traduit par le compilateur par:

    DynamicObjectExtension.GetProperty(s, "Length");

Avouez néanmoins que l'appel direct depuis l'instance est plus élégant. Et surtout il correspond exactement à ce que nous souhaitions faire: ajouter les méthodes de l'interface ICallable sur toutes les classes de la CLR !

MyDLR sur la CLR

Nous savons comment ajouter les méthodes de l'interface ICallable aux classes de la CLR, étudions maintenant le code de ces méthodes. Evidemment nous n'allons pas faire une comparaison de chaque type pour faire le branchement sur la bonne méthode ou la bonne propriété. En fait, nous allons utiliser la Reflection .NET qui permet d'interroger à l'exécution les caractéristiques d'un type quelconque. Commençons par la méthode "GetProperty".

            public static object GetProperty(this object @this, string name)
            {
                if (@this is ICallable)
                {
                    ICallable dynthis = @this as ICallable;
                    return dynthis.GetProperty(name);
                }
    
                Type myType = @this.GetType();
                PropertyInfo[] properties = myType.GetProperties();
                int length = properties.Length;
                for (int i = 0; i < length; i++)
                {
                    PropertyInfo current = properties[i];
                    if (name.ToLower() == current.Name.ToLower()
                        && current.CanRead
                        && !current.IsSpecialName)
                    {
                        return current.GetValue(@this, null);
                    }
                }
    
                throw new NotSupportedException();
            }

Dans un premier temps, nous testons si l'objet est un type de la DLR. En effet, l'ajout d'une extension est valable également pour les objets de la DLR, il est donc nécessaire de faire un test préalable et de rediriger sur l'interface ICallable si elle est implémentée. Si l'objet n'est pas un objet de la DLR, nous reposons complètement sur la Reflection .NET: nous interrogeons les propriétés de son type (méthode "Type.GetProperties") pour rechercher celle qui porte le même nom. Il suffit ensuite d'un appel à "PropertyInfo.GetValue" pour récupérer sa valeur dans l'objet courant.

La méthode "CallMethod" est assez similaire:

            public static object CallMethod(this object @this, string name, params object[] args)
            {
                if (@this is ICallable)
                {
                    ICallable dynthis = @this as ICallable;
                    return dynthis.CallMethod(name, args);
                }
    
                Type myType = @this.GetType();
                MethodInfo[] methods = myType.GetMethods();
                int length = methods.Length;
                for (int i = 0; i < length; i++)
                {
                    MethodInfo current = methods[i];
                    ParameterInfo[] argsinfo = current.GetParameters();
                    if (name.ToLower() == current.Name.ToLower()
                        && current.IsPublic
                        && !current.IsStatic
                        && !current.IsSpecialName
                        && args.Length == argsinfo.Length)
                    {
                        bool typeMatch = true;
                        for(int j = 0; j < argsinfo.Length && typeMatch; j++)
                        {
                            ParameterInfo arginfo = argsinfo[j];
                            ...
                        }
                        if (!typeMatch)
                            continue;
    
                        return current.Invoke(@this, args);
                    }
                }
    
                throw new NotSupportedException(name);
            }

Cette fois ci, nous faisons une recherche sur les différentes méthodes de la classe: "Type.GetMethods". Le nom de la méthode doit correspondre mais nous devons également nous assurer que le nombre d'arguments correspond et que leurs types correspond également. L'appel se fait par l'intermédiaire d'un appel de "MethodInfo.Invoke". Je passe l'implémentation de la méthode "SetProperty" qui est très similaire.

Voilà, nous pouvons désormais faire fonctionner dynamiquement toutes les classes de la CLR:

    String s = "Hello";
    Console.WriteLine(s.GetProperty("Length"));        // Affiche 5
    Console.WriteLine(s.CallMethod("ToLower"));        // Affiche hello

Conversion de type : comment faire ?

Tout cela parait simple mais nous avons ignoré pour l'instant un problème important: la conversion de type. En effet pour être utilisable de manière transparente, la DLR doit proposer un mécanisme permettant de prendre en charge des conversions de type. Des conversions sont en effet potentiellement nécessaires chaque fois que l'on appelle une méthode ou un propriété. Plusieurs cas peuvent se présenter:

  • Appel d'une méthode d'un objet de la DLR avec en paramètre un objet de la DLR. C'est un cas simple, il n'y a rien à faire.
  • Appel d'une méthode d'un objet de la DLR avec en paramètre un objet de la CLR. Il faut convertir le paramètre dans un type de la DLR.
  • Appel d'une méthode d'un objet de la CLR avec en paramètre un objet de la DLR. Il faut convertir le paramètre dans un type de la CLR.

Cette fonctionnalité est peu commentée actuellement par Microsoft, il est difficile d'imaginer l'implémentation qui sera choisie pour la DLR. Néanmoins, une fonctionnalité de la CLR traite déjà la notion de conversion: TypeConverter. La CLR utilise des TypeConverter pour gérer les conversions de type d'un objet à l'autre, tout naturellement nous allons également l'utiliser dans MyDLR pour réaliser nos méthodes de conversion. Un TypeConverter est un objet qui défini les conversions possibles pour un type donné. Pour créer un objet TypeConverter, il faut hériter de la classe TypeConverter et redéfinir les méthodes de conversion. De la même manière que la CLR définie de manière standard des TypeConverter pour ses classes de base (StringConverter, Int32Converter, ...), nous allons créer un TypeConverter pour MyJScript:

    public class MJSConverter : TypeConverter
    {
            public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { ... }
            public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { … }
            public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { … }
            public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { … }
    }

Etudions l'implémentation de ces méthodes. La première méthode, "CanConvertFrom" consiste à tester si l'objet peut être construit à partir d'un type particulier. Si c'est le cas, la conversion peut être lancée par l'intermédiaire de la méthode "ConvertFrom" à partir de la valeur de départ.

        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            if (sourceType == typeof(string) || sourceType == typeof(Int32))
                return true;

            return false;
        }

        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            if (value is string)
                return new MJSString(value as string);
            else if (value is Int32)
                return new MJSNumber((int)value);

            throw new NotSupportedException(value.ToString());
        }

Bien sûr, nous ne pouvons convertir que depuis des classes de base de la CLR vers les classes de base de MyJScript. Néanmoins, ces deux méthodes nous permettent déjà de faire des conversions standard vers des objets MJScript:

    MJSObject o1 = MJSConverter.ConvertFrom("Hello");     // Une String est convertie en un MJSString
    MJSObject o2 = MJSConverter.ConvertFrom(30);             // Un entier est converti en un MJSNumber

Inversement la méthode "CanConvertTo" permet de tester si l'objet peut être converti en un type particulier. Si c'est la cas, la méthode "ConvertTo" permet de lancer la conversion.

        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
        {
            if (destinationType == typeof(string) ||
                destinationType == typeof(Int32))
                return true;
            else
                return false;
        }

        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
        {
            MJSObject mjsvalue = value as MJSObject;
            if (destinationType == typeof(string))
            {
                if (value is MJSString)
                    return mjsvalue.ValueOf();
            }
            else if (destinationType == typeof(Int32))
            {
                if (value is MJSNumber)
                    return mjsvalue.ValueOf();
            }

            throw new NotSupportedException(destinationType.ToString());
        }

Là aussi nous convertissons les classes de base du MyJScript vers les classes de base de la CLR. Nous pouvons donc maintenant utiliser les conversions suivantes:

    Object s = MJSConverter.ConvertTo(o1, typeof(string));  // L'objet MJSString est converti en String
    Object i = MJSConverter.ConvertTo(o2, typeof(Int32));   // L'objet MJSNumber est converti en int

Pour terminer, il nous suffit d'attacher notre TypeConverter à la classe MJSObject en utilisant un attribut spécial du .NET Framework: TypeConverter.

       [TypeConverter(typeof(MJSConverter))]
       public class MJSObject : ICallable
        {
            …
        }

Conversion de type: comment l'utiliser ?

La conversion de type doit être utilisable de la manière la plus simple possible. Dans chacune des méthodes sur lequel nous avions besoin d'une conversion, nous voudrions simplement pouvoir écrire pour un objet quelconque:

    o.ConvertTo(type);

Encore une fois, nous allons donc faire appel aux méthodes d'extension. L'objectif est de disposer sur la classe "object" d'une méthode de conversion "ConvertTo":

        public static class DynamicObjectExtension
        {
            public static object ConvertTo(this object @this, Type destination)
            {
                ...
            }
        }

L'implémentation de la méthode ConvertTo essaye d'abord d'utiliser les règles d'assignation (méthode Type.IsAssignableFrom) qui vérifie les conversions simples: classe A dans classe A, classe dérivée de A dans classe A, A dans interface implémentée par A, … Ensuite nous utilisons les objets TypeConverter en essayant d'abord sur le type source puis sur le type destination. En effet, un StringConverter ne sait pas convertir vers un objet MJSObject (puisque ce n'est pas un type de la CLR) mais le MJSConverter sait créer un MJSObject à partir d'un objet String.

        public static object ConvertTo(this object @this, Type destination)
        {
            // Affectation standard
            Type source = @this.GetType();
            if (destination.IsAssignableFrom(source))
                return @this;

            // On utilise le converter de la source
            TypeConverter converter = TypeDescriptor.GetConverter(source);
            if (converter != null)
            {
                if (converter.CanConvertTo(destination))
                    return converter.ConvertTo(@this, destination);
            }

            // On utilise le converter de la destination
            converter = TypeDescriptor.GetConverter(destination);
            if (converter != null)
            {
                if (converter.CanConvertFrom(source))
                    return converter.ConvertFrom(@this);
            }

            throw new NotSupportedException(destination.ToString());
        }

Voilà, nous pouvons maintenant écrire:

    "Hello world !".ConvertTo(typeof(MJSObject));
    (new MJSString("DotNetGuru")).ConvertTo(typeof(String));

Création d'instance

Une fonctionnalité de la DLR est de pouvoir créer dynamiquement des instance d'objets: que ce soit des objets du langage MyJScript ou des objets de la CLR. Plus concrètement nous voulons pouvoir créer une  instance à partir d'un nom de classe et des valeurs des paramètres d'initialisation du constructeur. Une dernière fois nous allons abuser des méthodes d'extension afin d'unifier cette fonctionnalité pour pouvoir créer toutes les sortes d'objets. En fait nous voulons écrire:

    object mjs = "String".CreateInstance("Hello");            // Créé un objet MJSString
    object mjc = "Cat".CreateInstance("Felix");               // Créé un objet de la classe Cat du MyJScript
    object s = "System.String".CreateInstance(new Char[3] {'D', 'N', 'G'});    // Créé un objet String

La nouvelle méthode d'extension à créer ressemble donc à ceci:

    public static class DynamicObjectExtension
    {
        public static object CreateInstance(this string classname, params object[] args)
        {
            ...
        }
    }

La méthode s'ajoute à la classe String de la CLR. Elle récupérer le nom de la classe à instancier et reçoit un tableau de paramètres pour le constructeur. Pour implémenter la méthode, commençons par étudier la création d'instance en MyJScript. Nous avons besoin d'une interface ressemblant à ceci:

    public interface IDLRProvider
    {
        bool HasClass(string classname);
        object CreateInstance(string classname, params object[] args);
    }

La première méthode test si la classe existe dans le langage, la deuxième lance réellement la création. Chaque langage qui supporte MyDLR doit implémenter cette interface dans une classe spécifique. Voici ce que cela donne pour MyJScript:

    public class MJSDLRProvider : IDLRProvider
    {
        public bool HasClass(string classname)
        {
            // Classes standard
            if (classname.Equals("String") || classname.Equals("Number"))
                return true;

            // Rechercher dans le contexte global
            MJSObject o = MJSContext.GetContext().Get(classname);
            if (o == null)
                return false;

            return (o is MJSFunction);
        }

        public object CreateInstance(string classname, params object[] args)
        {
            // Classes standard
            if (classname.Equals("String"))
            {
                object param = args[0];
                return new MJSString(param.ConvertTo(typeof(string)) as string);
            }
            else if (classname.Equals("Number"))
            {
                object param = args[0];
                 return new MJSNumber((Int32)param.ConvertTo(typeof(Int32)));
            }

            // Converti chaque paramètre en MJSObject
            int length = args.Length;
            MJSObject[] mjsargs = new MJSObject[length];
            for (int i = 0; i < length; i++)
            {
                object param = args[i];
                Type type = param.GetType();
                 mjsargs[i] = new MJSString(param.ConvertTo(typeof(string)) as string);
            }

            // Créé l'objet et appelle le constructeur
            MJSObject newobject = new MJSObject();
            MJSFunction constructor = MJSContext.GetContext().Get(classname) as MJSFunction;
            constructor.Call(newobject, mjsargs);

            return newobject;
        }

L'implémentation est simple à comprendre car elle est la synthèse de ce que nous avons vu jusqu'à présent. En MyJScript pour tester si une classe existe, on recherche une méthode globale portant le nom de la classe à créer. Seule exception, on vérifie au préalable qu'il ne s'agit pas d'une des classes standard ("String" ou "Number"). Pour créer une instance sur les classes standard on utilise les constructeurs, pour créer une instance sur les autres classes, on créé un objet et on appelle la méthode sur l'objet. On notera également les conversions qui sont réalisées à chaque fois.

Revenons maintenant à l'implémentation de notre méthode d'extension "CreateInstance". Voilà son code:

        public static object CreateInstance(this string classname, params object[] args)
        {
            // Création d'objets de la DLR
            foreach (IDLRProvider dle in Providers.languages)
            {
                if (dle.HasClass(classname))
                    return dle.CreateInstance(classname, args);
            }

            // Création d'objets de la CLR
            Type type = Type.GetType(classname);
            if (type != null)
                return Activator.CreateInstance(type, args);

            throw new NotSupportedException(classname);
        }

On cherche d'abord si la classe correspond à un objet dynamique en parcourant tous les IDLRProvider. Si c'est le cas on appelle la méthode CreateInstance correspondante. Si ce n'est pas le cas, on explore la CLR en utilisant la méthode "Type.GetType" qui permet de créer une description de type à partir de son nom. Par Reflection grâce à la méthode "Activator.CreateInstance" nous pouvons ensuite lancer la création de l'objet.

Un nouveau langage dynamique: Liogo

L'interopérabilité est une fonctionnalité importante de la DLR, elle doit permettre à plusieurs langages dynamiques de communiquer de manière transparente. Dans notre exemple de départ, nous souhaitions faire cohabiter un programme JavaScript avec un programme python. Il serait intéressant pour aller au bout de la démarche de mettre en œuvre cette fonctionnalité sur MyDLR.

Pour cela, il nous faut tout d'abord un deuxième langage dynamique. Plutôt que d'en réinventer un, j'ai choisi d'utiliser le langage Logo car je disposais d'une implémentation à travers mon compilateur pour .NET: Liogo. Sans entrer dans les détails, j'ai simplement repris les structures de base utilisée dans le compilateur Liogo et j'y ai ajouté les interfaces et les classes nécessaires pour MyDLR. Le langage Logo défini différentes classes de base. Pour notre exemple, nous nous intéresserons uniquement au type "Word" qui est globalement l'équivalent d'une chaîne de caractères. Après implémentation des structures de MyDLR dans Liogo, voici donc ce que nous pouvons écrire:

    object w = "Word".CreateInstance("Logo");    // Créé un objet Word Liogo
    w.CallMethod("butfirst");                    // Appelle la méthode "butfirst" qui retourne "ogo"

Bien sûr, comme avec MyJScript, nous pouvons faire interagir le Liogo avec les types de la CLR car le Liogo a aussi son TypeConverter. Dans les lignes ci-dessus par exemple, il y a des conversions automatiques de l'objet "System.String" lors de la création d'instance et lors de l'appel de la méthode "butfirst".

La question est néanmoins de savoir si nous pouvons faire interagir MyJScript et Liogo afin qu'ils partagent des données de base à travers la DLR. Concrètement, nous voudrions écrire:

    object w = "Word".CreateInstance("Logo");    // Créé un objet Word Liogo
    object c = "Cat".CreateInstance(w);          // Construit un objet Cat MyJScript en utilisant un Word
    object x = "Word".CreateInstance(c.GetProperty("name")); // Créé un objet Word à partir d'un MJSString

Ici, nous utilisons un objet Word du Liogo pour initialiser l'objet MyJScript "c" puis nous utilisons un objet String du MyJScript pour initialiser l'objet Liogo "x".

Jusqu'à présent nous avons utilisé des TypeConverter pour gérer les conversions. Cela fonctionne parfaitement lorsque nous souhaitons faire des conversions entre la CLR et la DLR. Par contre, c'est impossible depuis deux types de la DLR qui, par définition, ne se connaissent pas. Pour convertir deux types de la DLR, nous allons donc repasser par la CLR et plus particulièrement par des chaînes de caractères. Voici notre méthode d'extension "ConvertTo" modifiée pour intégrer cette contrainte:

        public static object ConvertTo(this object @this, Type destination)
        {
             …
            // Si la classe de départ et la classe d'arrivée sont de la DLR
            if (@this is ICallable && Util.IsDLRType(destination))
            {
                // On passe par une conversion intermédiaire en chaîne de caractère
                if (srcconverter.CanConvertTo(typeof(string)) && destconverter.CanConvertFrom(typeof(string)))
                {
                    object asstring = srcconverter.ConvertTo(@this, typeof(string));
                    return destconverter.ConvertFrom(asstring);
                }
            }

            throw new NotSupportedException(destination.ToString());
        }

Cette modification nous permet, si aucune des conversions via des TypeConverter n'a fonctionnée de tenter en dernier ressort une conversion en chaine de caractères. Bien sûr cela ne peut fonctionner qu'avec des types de base des langages mais cela couvre les cas les plus courants d'utilisation.

Conclusion

Voici un exemple complet d'utilisation de MyDLR reprenant tous les mécanismes décrits ci-dessus.

            // 1) Créer un objet String du MyJScript
            object mjs = "String".CreateInstance("DotNetGuru");
            PrintValue(mjs);
            PrintValue(mjs.GetProperty("length"));
            PrintValue(mjs.CallMethod("substr", 6));

            // 2) Créer un objet Cat MyJScript
            object mjc = "Cat".CreateInstance("Felix");
            PrintValue(mjc);
            PrintValue(mjc.GetProperty("name"));
            mjc.CallMethod("miaow");

            // 3) Créer un objet System.String de la CLR
            object s = "System.String".CreateInstance(new Char[3] {'A', 'B', 'C'});
            PrintValue(s.GetProperty("length"));
            PrintValue(s.CallMethod("indexof", "B"));

            // 4) Créer un objet Word du Liogo
            object lw = "Word".CreateInstance("Hello World !");
            PrintValue(lw);
            PrintValue(lw.GetProperty("count"));
            PrintValue(lw.CallMethod("butfirst"));

            // 5) Interopérabilité entre les types de la DLR MyJScript et Liogo
            object c = "Word".CreateInstance("B");
            PrintValue(s.CallMethod("indexof", c));
            object t = "String".CreateInstance("2");
            PrintValue(lw.CallMethod("item", t));

Vous noterez que tous les objets sont manipulés en utilisant la classe "object" et que toutes les méthodes et propriétés sont manipulées sous forme de chaînes de caractères. Il n'y a donc aucun fonctionnement statique.

Cet exemple démontre donc que MyDLR répond aux objectifs que nous nous étions fixés:

  • Créer dynamiquement un objet de la DLR: exemple (1), (2) et (4),
  • Créer dynamiquement un objet de la CLR: exemple (3),
  • Appeler dynamiquement des méthodes ou propriétés d'un objet de la DLR: exemple (1), (2) et (4),
  • Appeler dynamiquement des méthodes ou propriétés d'un objet de la CLR: exemple (3),
  • Conversion explicite entre objets de la CLR et de la DLR: (1), (2) et (4),
  • Conversion explicite entre objets de la DLR (5).

Pour aller plus loin

MyDLR

L'implémentation de MyDLR décrite dans cette article est téléchargeable ici. Elle est abondamment commentée et intègre des tests unitaires pour la plupart des fonctions. La solution nécessite Visual Studio 2008 Beta 2 pour fonctionner.

Le code ne fait qu'illustrer le propos de cet article et n'est pas une partie de l'implémentation actuelle de la DLR. D'ailleurs de nombreux points de la DLR ne sont pas abordés par MyDLR. Voici les principaux:

  • En plus des méthodes et propriétés, les langages de la DLR doivent également savoir gérer les événements et les delegates.
  • Les opérateurs (arithmétiques, comparaisons ou autres) sont pris en charge par la DLR,
  • Chaque langage de la DLR gère l'héritage et ses conséquences sur les appels de méthodes,
  • La DLR supporte la notion de variables et méthodes de classes en plus des variables et méthodes d'instance.
  • La DLR intègre de nombreuses classes permettant de représenter les nœuds d'un arbre syntaxique.

DLR

Le blog de Jim Hugunin's est la bible des développeurs intéressés par la DLR. Il y décrit avec parcimonie mais de manière très dense les avancées du développement de la DLR. L'implémentation de MyDLR est directement issue des articles de ce blog. Jim a donné au Mix07 de Las Vegas une session montrant son fonctionnement dans Silverlight. Miguel de Icaza a commenté cette session sur son blog.

IronPython

Microsoft ayant décidé de développer la DLR sous forme "Shared Source", nous avons la chance de voir éclore en direct le code de la DLR. La première implémentation se trouve dans le projet IronPython dans le namespace Microsoft.Scripting.

Compilateur

Un de mes précédents articles sur DNG traitait de la création d'un compilateur. L'article aborde les notions d'analyse lexicale et syntaxique et d'arbres syntaxiques qui sont également présentes dans la DLR.

JavaScript

L'implémentation du moteur MyJScript décrite ici est inspirée de l'excellent article de Ray Djajadinata publié dans le MSDN de mai 2007. A lire absolument pour redécouvrir ce langage.

Liogo

Liogo est un compilateur Logo en .NET. Il est distribué en Open Source sur http://liogo.sourceforge.net.

PowerShell

PowerShell est le langage de script de Windows Server 2008. Il ne fait pas partie des langages cibles de la DLR (on peut s'en étonner) mais propose des mécanismes très proches qui permettent en plus des objets de la CLR de manipuler dynamiquement des objets COM et WMI. Lire le très complet PowerShell in Action sur le sujet.

Lionel Laské est architecte et chef du service Nouvelles Technologies à C2S. C2S est la société de services informatique du groupe Bouygues. C2S est spécialisée dans les technologie .NET et est Microsoft Gold Certified Partner. C2S est également à l'origine d'une offre de migration des applications NS-DK/NatStar vers .NET.