Introduction
Présentation de la DLR
MyJScript encore
Contexte du langage
Créer un arbre syntaxique via la DLR
Générer l'arbre syntaxique depuis la grammaire
Génération des variables
Utilisation des variables
Type de données
Règles d'exécution de l'arbre syntaxique
La programmation objet en MyJScript
Manipulation des propriétés des objets
Construction et appel des méthondes
Contexte global et fonctions built-in
Interopérabilité avec la CLR
Interpréteur et ligne de commande
Interopérabilité avec d'autres langages de la DLR
Pour aller plus loin
Conclusion
Dans une application, on a parfois besoin de mettre à la disposition de l'utilisateur un langage de paramétrage: par exemple pour offrir la possibilité de paramétrer les règles d'un moteur de règles ou de modifier le détail des opérations à réaliser pour le calcul d'un champ. Pour cela le développeur doit construire son interpréteur ou son compilateur et l'interfacer avec son code .NET. Il doit alors non seulement maîtriser les arcanes de l'analyse lexicale, de l'analyse syntaxique et de la compilation, mais aussi se plonger dans la génération de code pour exécuter les instructions de son nouveau langage.
La Dynamic Language Runtime (DLR) est une surcouche du .NET Framework 3.5 permettant de faciliter la création de langages dynamiques pour .NET. Cela peut être, comme décrit ci-dessus, un langage intégré à une application ou plus largement l'implémentation d'un nouveau langage pour .NET à l'image du IronPython ou du IronRuby que propose Microsoft.
Dans un précédent article ("La DLR: une autre CLR ?"), je présentais les objectifs et les principales fonctionnalités de la DLR. En résumé, le rôle de la DLR est de:
Ce précédent article utilisait une implémentation "maison" de la DLR, l'objectif de ce nouvel article est de détailler le mode de fonctionnement réel de la DLR en suivant pas à pas l'implémentation d'un nouveau langage pour la DLR.
La DLR se présente comme une DLL nommée "Microsoft.Scripting.dll". Actuellement, elle est distribuée avec SilverLight et, en code source, avec les différents langages dynamiques proposés par Microsoft, notamment IronPython ou IronRuby. La DLR est distribuée sous licence Open Source de type Microsoft Public Licence.
La DLR définie le namespace "Microsoft.Scripting" et plusieurs sous-namespace dont les principaux que nous utiliserons ici :
La DLR s'est construite (et continue à se construire) au fur et à mesure de l'avancement des implémentations des langages dynamiques de Microsoft. Le code source actuel montre d'ailleurs que l'organisation n'est pas complètement stabilisée à ce jour. La version décrite ici est la v1.0.0.1000 fournie avec IronPython 2.0 beta 1, le code décrit dans cet article sera néanmoins mis à jour lorsque la version définitive sera distribuée.
Pour illustrer cet article, nous allons encore une fois utiliser un langage dérivé du JavaScript, MyJScript. Le JavaScript est en effet assez intéressant car il est suffisamment éloigné du C# et VB.NET que nous connaissons et n'est pas trop complexe à implémenter si l'on ce limite à ses fonctionnalités importantes. Les caractéristiques principales du MyJScript sont:
Voilà déjà un objectif assez ambitieux pour étudier la DLR. A noter que l'implémentation proposée a été réalisée à titre d'exemple et ne se base par sur l'implémentation proposée par Microsoft du futur "Managed JScript".
Pour illustrer mon propos, voici deux exemples simples d'utilisation de MyJScript:
function fact(n) { if (n==0) return 1; return n*fact(n-1); } write("!4 = " + fact(4));
Dans ce premier exemple, on déclare une fonction factorielle et on l'appelle de manière récursive, on notera la conversion automatique d'entier en chaîne de caractères qui est réalisée pour permettre d'afficher un libellé puis sa valeur. Une telle conversion n'est pas autorisée en C#.
function Cat(name) { this.name = name; this.miaow = function () { write(this.name+' said Miaow'); }; } a = new Cat('Felix'); a.miaow(); Cat("this"); this.miaow();
Dans ce deuxième exemple, on manipule les concepts objets de MyJScript. On créé un constructeur pour une classe "Cat" et on créé une instance de cet objet. Nous illustrons également l'utilisation de "this": soit en tant qu'instance actuelle dans la méthode "Cat", soit en tant que contexte global. A noter la double représentation des chaînes de caractères (simple quote ou double-quote).
Pour créer son langage pour la DLR, la première chose à réaliser est de décrire son "contexte". Ce contexte permet de définir les propriétés du langage (nom, identifiant, version, …) et de définir les points d'entrée utilisés par la DLR. Voici l'interface du contexte pour MyJScript, il doit s'agir d'une classe qui dérive de Microsoft.Scripting.LanguageContext:
class MJSLanguageContext : LanguageContext { // Constructeur: initialise le "binder" public MJSLanguageContext(ScriptDomainManager); // Description du langage public override Guid LanguageGuid; public override string DisplayName; public override Version LanguageVersion; // Point d'entrée du parseur public override LambdaExpression ParseSourceCode(CompilerContext); // Recherche d'un symbole en global public override bool TryLookupGlobal(CodeContext, SymbolId, out object ); // Personnalisation de la ligne de commande public override ServiceType GetService<ServiceType>(params object[]); public override string FormatException(Exception exception); }
Le constructeur est l'une des méthodes essentielles, il permet d'effectuer les initialisations globales du contexte. Dans notre cas nous le verrons, cela concerne l'affectation des fonctions initiales du contexte MyJScript ("write" notamment) et surtout l'affectation du "binder" que nous détaillerons plus tard et qui est le moteur de règle du langage.
L'autre méthode importante du contexte est la méthode ParseSourceCode. Voici son implémentation pour MyJScript.
public override LambdaExpression ParseSourceCode(CompilerContext context) { // Call MyJScript parser Parser parser = new Parser(context.SourceUnit); parser.Parse(); // Return the generated AST return parser.Result; }
C'est cette méthode qui est le réel point d'entrée pour interpréter du code de notre langage MyJScript. La méthode ParseSourceCode prend en paramètre un contexte qui contient le code à exécuter, elle doit retourner l'arbre syntaxique correspondant sous la forme d'une "Lambda Expression", qui sera alors exécuté par la DLR.
Un arbre syntaxique est une représentation hiérarchique d'un programme de notre langage. Chaque nœud de l'arbre représente un élément: instruction, opérateur ou valeur. Par exemple l'instruction:
res = n * (n-1);
Peut se présenter comme ceci:
Le namespace "Microsoft.Scripting.Ast" propose des objets pour tous les différents éléments manipulables depuis un langage reposant sur la CLS (Common Language Specification). Ainsi pour construire l'instruction ci-dessus, on écrit le code C# suivant:
Ast.Statement(span, Ast.Assign( res, Ast.Action.Operator( Operators.Multiply, typeof(object), Ast.Read(n), Ast.Action.Operator( Operators.Subtract, typeof(object), Ast.Read(n), Ast.Constant(1) ) ) ) );
C'est évidement assez "verbeux" et pour ceux qui connaissent l'API CodeDOM, vous retrouverez pas mal de similitudes. Dans les deux cas il s'agit de représenter un programme mais les AST de la DLR se traduisent en code IL alors que l'API CodeDOM se traduit en du code source C# ou VB.NET.
Générer un arbre syntaxique à partir du texte d'un programme langage nécessite plusieurs étapes:
J'ai détaillé les deux premières étapes dans un article précédent ("Ecrire un compilateur pour .NET"), nous ne rentrerons donc pas à nouveau dans le détail ici. L'implémentation de MyJScript sur la DLR ne modifie pas le code de ces étapes qui est généré par Jay, un dérivé de YACC. Voilà l'interface de notre parser:
class Parser { public Parser (SourceUnit source) { ... } public bool Parse() { ... } public LambdaExpression Result { get { return generator.Result; } } }
L'objet parser s'initialise avec un code source représenté par un SourceUnit. Il s'agit d'un objet construit par la DLR qui représente des lignes de code du langage en provenance d'une ligne de commande ou un fichier. Il est d'ailleurs envisageable d'avoir un traitement différent selon le cas. Le parsing s'exécute par l'appel de la méthode Parse. La construction de l'arbre syntaxique s'effectue au fur et à mesure du parcours de la grammaire exécutée par la méthode Parse. Pour plus de clarté, nous l'avons isolé dans une classe de génération.
Prenons un exemple: la règle de construction du IF dans la grammaire du MyJScript. Voilà comment elle s'exprime en Jay/YACC:
if_else: IF LPAR cond_expr RPAR block_or_statement ELSE block_or_statement { $$ = generator.IfElse($3 as Expression, $5 as Expression, $7 as Expression); } | IF LPAR cond_expr RPAR block_or_statement { $$ = generator.IfElse($3 as Expression, $5 as Expression, null); }
Voici le code de la méthode de génération du IF dans le générateur MyJScript pour la DLR:
public Expression IfElse(Expression condition, Expression ifTrue, Expression ifFalse) { if (ifFalse == null) return Ast.IfThen( Ast.Convert(condition, typeof(bool)), ifTrue ); return Ast.IfThenElse( Ast.Convert(condition, typeof(bool)), ifTrue, ifFalse ); }
Vous remarquez qu'on se contente de construire les nœuds correspondants au IfThen ou au IfThenElse. On retrouve ce même type de construction pour les autres instructions du langage.
Un zoom particulier doit être réalisé sur la création des variables avec la DLR. La portée des variables est fortement dépendante du langage de programmation que l'on utilise. D'un langage à un autre, une variable peut être: globale, locale, être liée à une classe, à une instance, à un bloc, ... Néanmoins la portée se détecte toujours au niveau de la grammaire. Ainsi par exemple en C#, lorsqu'on rencontre l'utilisation d'une variable dans une méthode, ce sont les règles de syntaxe qui permettent de la retrouver. On cherchera ainsi d'abord dans les variables du bloc, puis dans les paramètres, et enfin dans les variables d'instance (si on est dans une méthode d'instance) ou dans les variables de classe (si on est dans une méthode statique).
La DLR permet de déclarer des variables au niveau de l'objet LambdaBuilder. Ce nœud représente une LambdaExpression correspondant soit au code d'une fonction, soit à un bloc interne. La méthode LambdaBuilder.CreateLocalVariable permet de créer une variable pour le bloc actuel. La méthode LambdaBuilder.CreateParameter permet de créer un paramètre.
En MyJScript, une variable peut être déclarée localement à une fonction, être un paramètre ou, par défaut (car la déclaration est optionnelle) globale. L'exemple de code suivant:
a = 100; function foo(n) { var b = n + a; return b; }
Peut donc se représenter de manière simplifiée par la LambdaExpression:
Le code de notre compilateur MyJScript gère lui-même la portée des variables en utilisant des dictionnaires. Un dictionnaire pour les variables globales et un dictionnaire pour les variables locales. Pour les paramètres, ils sont créés directement lors de la déclaration. Pour illustrer ce mode de fonctionnement, voici le code appelé par le compilateur MyJScript lorsqu'il rencontre un début de fonction.
public void BeginFunction(String name, List<String> arguments) { if (name != null && globalVariables.ContainsKey(name)) { report.Error(…); // function déjà définie return; } previousMethod = currentMethod; currentMethod = Ast.Lambda(name ?? "<member function>", typeof(object)); localVariables = new Dictionary<string, Variable>(); currentMethod.CreateParameter(SymbolTable.StringToId("this"), typeof(object)); if (arguments.Count > 0) { foreach (string parameter in arguments) currentMethod.CreateParameter(SymbolTable.StringToId(parameter), typeof(object)); } }
Plusieurs choses sont à remarquer:
Maintenant que nous avons compris comment nous stockions les variables, il est facile d'écrire le code qui va générer l'arbre pour récupérer la valeur d'une variable. Voici comment cela se passe en MyJScript:
public Expression Variable(string name) { Variable variable = null; if (localVariables.ContainsKey(name)) variable = localVariables[name]; // Variable locale else { if (currentMethod != null) { foreach (Variable param in currentMethod.Parameters) if (SymbolTable.IdToString(param.Name).Equals(name)) variable = param; if (variable != null) return Ast.Read(variable); // Paramètre } if (variable == null) { if (!globalVariables.ContainsKey(name)) return Ast.Read(SymbolTable.StringToId(name)); // Variable inconnue variable = globalVariables[name]; // Variable globale } } return Ast.Read(variable); // Variable connue }
Nous recherchons d'abord dans le dictionnaire des variables locales, puis dans les paramètres et enfin dans les variables globales. Dans ce dernier cas:
La représentation des données dans la DLR est l'un des choix de conception les plus important qui a guidé son implémentation. C'est ce que nous explique Jim Hugunin sur son blog. Le problème est simple: généralement chaque langage de programmation a sa propre représentation des types de données. Ainsi, les chaînes de caractères sont représentés par des String dans la CLR et peuvent être représentés par des PyString en Python ou par des MJSString en MyJScript (comme dans mon précédent article).
A chaque fois, ces classes implémentent des méthodes et des propriétés particulières sur les types de base, ce qui fait la richesse de chaque langage. Or, comment peut-on imaginer faire intéropérer les types de la CLR ou les types de différents langages de la DLR s'ils ne partagent pas les mêmes types de base ?
La DLR propose donc de n'utiliser que des types de base. Une String doit être représentée par un objet String de la CLR quelque soit le langage dans lequel elle est manipulée. Pour cela, la DLR repose sur les méthodes d'extension ajoutées au .NET 3.5. Pour MyJScript, nous voulons ajouter au String les méthodes blink, bold et substr du JavaScript. Voilà donc le code à écrire:
[assembly: ExtensionType(typeof(string), typeof(MyJScript.Runtime.StringExtensions))] public static class StringExtensions { public static string blink(string @this) { StringBuilder res = new StringBuilder("<blink>"); res.Append(@this); res.Append("</blink>"); return res.ToString(); } public static string bold(string @this) { StringBuilder res = new StringBuilder("<bold>"); res.Append(@this); res.Append("</bold>"); return res.ToString(); } public static string substr(string @this, int index) { return @this.Substring(index); } public static string substr(string @this, int index, int length) { return @this.Substring(index, length); } }
Avec ces quelques lignes nous ajoutons quatre méthodes d'extension à la classe String, les String peuvent donc désormais se manipuler comme en JavaScript. A noter que les méthodes d'extensions, comme leur nom l'indique, ne permettent que de rajouter des méthodes et pas de propriétés. Pour ajouter des propriétés, il faudra passer par des règles comme nous le verrons plus loin.
Résumons ce que nous avons appris jusqu'à présent. Pour exécuter une instruction du MyJScript, par exemple, "1 + 1", il nous suffit de retourner dans le point d'entrée ParseSourceCode de notre contexte MJSLanguageContext, un arbre syntaxique correspondant à l'expression. Ici, nous allons simplement retourner:
Ast.Action.Operator( Operators.Add, typeof(object), Ast.Constant(1), Ast.Constant(1) )
Comment fait-on ensuite pour exécuter ce code ? En fait, il n'y a rien à faire ! Lorsque la DLR reçoit un arbre syntaxique, elle le traduit en code IL et elle l'exécute. Dans notre cas, cela signifie qu'elle appelle l'addition de deux entiers.
Cela fonctionne car la DLR connait de manière standard les règles d'exécution pour de nombreux types de base dont les entiers, nous n'avons donc pas à écrire du code pour l'exécution.
Modifions maintenant notre exemple et essayons d'exécuter " 1 + 'A' ". Ce code est tout à fait valide en MyJScript puisque le MyJScript autorise comme le JavaScript les conversions implicites. Nous le traduisons par:
Ast.Action.Operator( Operators.Add, typeof(object), Ast.Constant(1), Ast.Constant("A") )
Et bien sûr, là ça ne fonctionne pas, nous avons une erreur à l'exécution. La DLR ne sait pas ajouter une chaîne de caractères à un entier, elle ne connait pas cette règle, il nous faut donc lui apprendre.
Les règles à ajouter sont à implémenter dans l'objet Binder qui est construit à l'initialisation du contexte du langage. Voici une partie du code de la classe MJSBinder:
public class MJSBinder : ActionBinder { public MJSBinder(CodeContext context) : base(context) { } protected override StandardRule<T> MakeRule<T>(CodeContext callerContext, DynamicAction action, object[] args) { if (operation.Operation == Operators.Add && args[0] is int && args[1] is string) { // Règle d'addition entier et chaînes du MyJScript return MakeAddStringRule<T>(callerContext, action, args); } // Laisser la DLR trouver une règle return base.MakeRule<T>(callerContext, action, args); } ... }
Le cœur de cet objet est la méthode MakeRule. Elle est appelée par la DLR chaque fois qu'elle se trouve devant une expression du langage qu'elle ne sait pas exécuter. La méthode doit retourner une règle c'est-à-dire une condition décrivant le cas d'utilisation de la règle et le code à exécuter. Dans les deux cas, cela va se faire par l'intermédiaire des AST que nous avons vu précédemment. Voici le code de la méthode MakeAddStringRule appelée ci-dessus:
private StandardRule<T> MakeAddStringRule<T>(CodeContext callerContext, DynamicAction action, object[] args) { StandardRule<T> rule = new StandardRule<T>(); // Equivalent à: (p0 is int) && (p1 is string) rule.Test = Ast.AndAlso( Ast.TypeIs(rule.Parameters[0], typeof(int)), Ast.TypeIs(rule.Parameters[1], typeof(string)) ); // Equivalent à: string.Contat(p0.ToString(), p1) rule.Target = rule.MakeReturn(this, Ast.Call( typeof(string).GetMethod("Concat", new Type[] { typeof(string), typeof(string) }), Ast.Call( Ast.Convert(rule.Parameters[0], typeof(object)), typeof(object).GetMethod("ToString", new Type[0]) ), Ast.Convert(rule.Parameters[1], typeof(string)) ); return rule; }
Notre règle est constituée d'un test et d'une cible. Le test (positionné dans la propriété StandardRule.Test) consiste à vérifier que le premier paramètre est un entier et le second est une chaîne. La cible (positionné dans la propriété StandardRule.Target) est l'arbre à exécuter pour obtenir le résultat de l'expression. Il s'agit de convertir le premier paramètre en chaîne de caractères et de le concaténer avec le deuxième paramètre.
Une fois que cette règle est construite, la DLR dispose désormais de toutes les informations pour exécuter notre expression "1 + 'A'". Mieux: si elle rencontre à nouveau ce cas de figure elle appliquera la même construction sans désormais faire appel à notre méthode MJSBinder.MakeRule.
Evidemment, il faut de la même manière construire toutes les règles du langage ce qui peut être un peu verbeux. Il peut également être assez complexe de construire un arbre lorsque la cible nécessite plusieurs opérations. Rien ne nous empêche alors d'appeler une fonction depuis notre arbre. Prenons par exemple l'expression MyJScript suivante:
'10' < 2
Si on effectue une conversion du membre droit en chaîne de caractères, on va alors comparer deux chaînes de caractères et le chiffre 1 précédent le chiffre 2 dans l'ordre lexicographique, on obtiendra un résultat faux. Pour obtenir un résultat correct, il faut convertir le membre gauche en entier avant de faire la comparaison. C'est d'ailleurs ce que fait JavaScript. Le traitement nécessite néanmoins d'essayer la conversion et de faire une comparaison différente selon son résultat. C'est un peu "lourd" à exprimer dans un AST, on peut alors passer par une méthode. C'est la méthode "MJSLibrary.Compare" de notre langage qui effectue ce traitement, la règle correspondante est alors beaucoup plus simple:
private StandardRule<T> MakeCompareStringRule<T>(CodeContext callerContext, DynamicAction action, object[] args) { StandardRule<T> rule = new StandardRule<T>(); // Equivalent à: (p0 is string) && (p1 is int) rule.Test = Ast.AndAlso( Ast.TypeIs(rule.Parameters[0], typeof(string)), Ast.TypeIs(rule.Parameters[1], typeof(int)) ); // Equivalent à: MJSLibrary.CompareTo(p0, p1) < 0 rule.Target = rule.MakeReturn(this, Ast.LessThan( Ast.Call( typeof(MJSLibrary).GetMethod("CompareTo"), rule.Parameters[0], rule.Parameters[1] ), Ast.Constant(0) ); return rule; }
Nous avons jusqu'à présent étudié uniquement la manipulation de types de données de base. Mais la DLR peut aussi être utilisée pour manipuler des instances d'objets d'un langage objet. Pour cela intéressons nous d'abord à la programmation objet en JavaScript.
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é par:
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 ses méthodes d'instance comme dans un langage statique. Un objet JavaScript se constitue 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ée 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 MyJScript nous allons réutiliser ce principe, voici une partie de l'implémentation de la classe MJSObject qui représente une instance d'objet en MyJScript:
public class MJSObject { Dictionary<string, object> members; public MJSObject() { this.members = new Dictionary<string, object>(); } public bool HasMember(string name) { return members.ContainsKey(name); } public virtual void Set(string name, object value) { if (members.ContainsKey(name)) members.Remove(name); members.Add(name, value); } public virtual object Get(string name) { if (!members.ContainsKey(name)) return null; return members[name]; } }
La classe MyJScript est constituée d'un simple dictionnaire qui va stocker les valeurs de toutes les variables d'instance: propriétés ou méthodes (comme "name" ou "miaow" dans notre exemple).
Notre compilateur MyJScript supporte l'initialisation vide d'un objet, l'instruction:
x = {};
Est ainsi traduite par le parseur MyJScript par l'arbre:
Ast.Statement(span, Ast.Assign( x, Ast.New( typeof(MJSObject).GetConstructor(new Type[0]) ) ) );
C'est-à-dire que nous affectons à la variable "x" le résultat de l'appel du constructeur de MJSObject sans paramètre.
Pour affecter ou récupérer des membres de notre nouvel objet, nous allons apprendre à la DLR les opérations nécessaires. Pour cela, nous ajoutons deux nouvelles règles à notre objet MJSBinder.
private StandardRule<T> MakeSetMemberRule<T>(CodeContext callerContext, DynamicAction action, object[] args) { SetMemberAction setmember = (SetMemberAction)action; StandardRule<T> rule = new StandardRule<T>(); // Equivalent à: (p0 is MJSObject) rule.Test = Ast.TypeIs(rule.Parameters[0], typeof(MJSObject)); // Equivalent à: (p0 as MJSObject).Set(name, p1) rule.Target = rule.MakeReturn(this, Ast.Call( Ast.Convert(rule.Parameters[0], typeof(MJSObject)), typeof(MJSObject).GetMethod("Set", new Type[] { typeof(string), typeof(object) }), Ast.Constant(SymbolTable.IdToString(setmember.Name)), Ast.Convert(rule.Parameters[1], typeof(object)) ) ); return rule; } private StandardRule<T> MakeGetMemberObjectRule<T>(CodeContext callerContext, DynamicAction action, object[] args) { GetMemberAction getmember = (GetMemberAction)action; StandardRule<T> rule = new StandardRule<T>(); // Equivalent à: (p0 is MJSObject) rule.Test = Ast.TypeIs(rule.Parameters[0], typeof(MJSObject)); // Equivalent à: (p0 as MJSObject).Get(name) rule.Target = rule.MakeReturn(this, Ast.Call( Ast.Convert(rule.Parameters[0], typeof(MJSObject)), typeof(MJSObject).GetMethod("Get", new Type[] { typeof(string) }), Ast.Constant(SymbolTable.IdToString(getmember.Name)) ) ); return rule; }
La première règle est appelée lorsque la méthode MJSBinder.MakeRule rencontre une opération de type "SetMember". La condition d'exécution est que le type de l'objet soit un MJSObject. La règle à exécuter et l'appel de la méthode "Set" de notre objet "MJSObject" en lui passant en paramètre le nom de la propriété et sa valeur. La deuxième règle est appelée pour les opérations de type "GetMember", elle est similaire à la première mais déclenche un appel de la méthode "MJSObject.Get".
Grâce à ses règles, notre compilateur MyJScript sait désormais exécuter les instructions:
x.name = "Hello";
write(x.name);
Intéressons nous maintenant à l'utilisation des méthodes. Voici les instructions MyJScript pour déclarer et appeler une méthode membre:
x.foo = function(n) { write(n); } x.foo("Hello");
Nous avons décrit un peu plus haut qu'une fonction était transformée par notre parseur en objet LambdaExpression. Lorsque la DLR génère le code correspondant à un AST, elle transforme les objets LambdaExpression en Delegate. Un bloc peut ainsi être appelé très simplement d'autant que la DLR fourni un nœud spécifique pour gérer un appel. Nous avons donc peu de chose à écrire pour exécuter l'appel d'une fonction d'instance. Voici l'arbre qui est construit:
public Expression MethodCall(Expression instance, String function, List<Expression> values) { int length = values.Count; Expression[] array = new Expression[length+1]; array[0] = instance; for (int i = 0; i < length; i++) array[i+1] = values[i]; return Ast.Action.InvokeMember( SymbolTable.StringToId(function), typeof(object), InvokeMemberActionFlags.None, new CallSignature(values.Count), array ); }
En fait, il s'agit simplement de construire un tableau des paramètres en passant l'instance comme premier paramètre puis de lancer l'appel de la fonction.
Cela est suffisant pour des appels de méthodes habituels mais en MyJScript, nous ajoutons systématiquement l'instance en paramètre de manière à ce qu'il soit récupéré par le paramètre "this", nous devons donc ajouter une nouvelle règle à notre classe MJSBinder. Cette règle sera utilisée pour les opérations "InvokeMember" sur les MJSObject:
private StandardRule<T> MakeInvokeMemberRule<T>(CodeContext callerContext, DynamicAction action, object[] args) { StandardRule<T> rule = new StandardRule<T>(); InvokeMemberAction invokeMember = (InvokeMemberAction)action; // Equivalent à: (p0 is MJSObject) rule.Test = Ast.TypeIs(rule.Parameters[0], typeof(MJSObject)); Expression method = Ast.Action.GetMember( invokeMember.Name, typeof(object), rule.Parameters[0] ); Expression[] newparam = new Expression[rule.Parameters.Length + 1]; newparam[0] = method; for (int i = 0; i < rule.Parameters.Length; i++) newparam[i + 1] = rule.Parameters[i]; // Equivalent à: p0.name(p0, p1, … pn) rule.Target = rule.MakeReturn(this, Ast.Action.Call( typeof(object), newparam ) ); return rule; }
Après avoir testé qu'elle était appelée sur un MJSObject, cette règle effectue trois opérations: récupérer la valeur du membre, ajouter aux paramètres l'objet courant (qui représente le paramètre "this") et appeler la méthode avec les nouveaux paramètres.
Pour compléter le langage MyJScript, 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; } }
Les manipulations des variables globales et des fonctions doivent ensuite se faire dans ce contexte. Ainsi, lorsque nous assignons la valeur d'une variable ou lorsque nous créons une fonction, nous générons deux instructions dans l'arbre AST:
public Expression AssignGlobalVariable(Variable variable, Expression value) { List<Expression> statements = new List<Expression>(); // variable = value statements.Add( Ast.Assign( variable, Ast.Convert(value, typeof(object)) ) ); // MJSContext.GetContext().Set(variable, value) statements.Add( Ast.Action.SetMember( SymbolTable.StringToId(variable.Name), typeof(object), Ast.Call( typeof(MJSContext).GetMethod("GetContext", new Type[0])) ), Ast.Read(variable) ); return Ast.Block(statements); }
Ceci permet à la fois d'assigner la variable pour la DLR mais aussi d'assigner la variable dans notre contexte global (MJSContext.GetContext()). Nous utilisons également le contexte global pour stocker les fonctions "built-in" de notre langage, ici la fonction "write":
public class MJSLibrary { internal static void Initialize() { MJSObject mjscontext = MJSContext.GetContext(); mjscontext.Set( "write", ReflectionUtils.CreateDelegate( typeof(MJSLibrary).GetMethod("Write"), typeof(MyJScriptCallTarget) ) ); ... } public static object Write(object @this, object value) { Console.WriteLine(value != null ? value.ToString() : "<null>"); return null; } }
Revenons maintenant sur la méthode MJSContext.TryLookupGlobal du contexte du langage que nous évoquions dans les premiers paragraphes de l'article. Cette méthode est utilisée par la DLR pour résoudre les noms des variables qu'elle rencontre et qu'elle ne connait pas. Dans notre cas, il suffit de rechercher leur valeur dans le contexte global. Voici donc l'implémentation de la méthode:
public override bool TryLookupGlobal(CodeContext context, SymbolId name, out object value) { MJSObject mjscontext = MJSContext.GetContext(); string memberName = SymbolTable.IdToString(name); if (mjscontext.HasMember(memberName)) { value = mjscontext.Get(memberName); return true; } return base.TryLookupGlobal(context, name, out value); }
Nous avons déjà vu comment les types de base comme "string" ou "int" servaient également de type de base pour les langages de la DLR. Grâce aux règles de base de la DLR, on peut ainsi utiliser de manière transparente leurs valeurs, mais aussi leurs propriétés et méthodes. En voici quelques exemples en MyJScript:
a='Hello'; write(a.Length); write(a.ToUpper() + ' WORLD!');
On notera que les appels des propriétés et des méthodes ne déclenchent pas les règles spécifiques des objets du MyJScript puisque celles-ci sont uniquement configurées pour les objets MJSObject. Par contre, la concaténation entre le résultat de "ToUpper()" et d'une constante est bien une règle que nous avons ajouté spécifiquement en MyJScript.
La DLR autorise également l'import d'objets d'autres assembly de la CLR. Pour que cela soit possible en MyJScript, nous avons emprunté la syntaxe "using" du C# car il n'y a pas d'équivalent en JavaScript. Voilà l'exemple de code que nous pouvons exécuter en MyJScript:
using System; var date = System.DateTime; var now=date.Now; write(now.Year); write(now.ToString());
Nous ne détaillerons pas l'implémentation du "using". Elle consiste simplement à déclencher à l'exécution l'appel d'une méthode spécifique de la DLR appelée "LanguageContext.DomainManager.Globals.TryGetName(name)". Cette méthode se charge de créer dans le contexte actuel une variable pour le namespace fourni en y intégrant des membres pour les différentes classes et objets. Tous les appels de propriétés et méthodes sont alors à nouveau réalisés de manière transparente par les règles de la DLR.
Une fois le compilateur de notre langage réalisé, nous aimerions écrire un interpréteur en ligne de commande pour exécuter des commandes et des scripts. La DLR fournit en standard les moyens de réaliser très simplement une console. Voici le code nécessaire, qui nécessite simplement de dériver de la classe Microsoft.Script.Hosting.ConsoleHost:
public class MJSConsole : ConsoleHost { protected override void Initialize() { base.Initialize(); this.Options.ScriptEngine = ScriptEnvironment.GetEnvironment().GetEngine(typeof(MJSLanguageContext)); Environment.LoadAssembly(typeof(string).Assembly) } [STAThread] static int Main(string[] args) { return new MJSConsole().Run(args); } }
Pour que cela soit moins "brut", on y ajoute un peu de personnalisation sur le logo et le prompt en ajoutant une classe MJSCommandLine qui est référencée par notre MJSLanguageContext.
class MJSCommandLine : CommandLine { protected override string Logo { get { return "MyJScript Command line\r\nLGPL Copyright (c) Lionel Laské 2008\r\nType CTRL-Z and RETURN to quit\r\n\r\n"; } } protected override string Prompt { get { return "mjs> "; } } }
Avec ce peu de ligne de code nous disposons déjà:
Nous avons vu comment le MyJScript pouvait intéropérer avec des types de la CLR. Etudions maintenant comment peut se faire l'interopérabilité avec d'autres langages reposant sur la DLR et en particulier le plus célèbre d'entre eux, IronPython.
Le paragraphe précédent montrait l'utilisation de la console standard qui se charge elle-même du hosting de la DLR. Il est néanmoins possible de gérer dans son application le hosting d'un langage de la DLR. Cela se réalise en tout d'abord en créant un runtime d'exécution pour la DLR.
// Créer un runtime pour la DLR ScriptRuntime runtime = ScriptRuntime.Create(); // Créer le niveau global ScriptScope globals = runtime.CreateScope();
Il faut ensuite charger le moteur du langage à partir du type de son contexte. Ce qui renvoi un objet Microsoft.Script.Hosting.ScriptEngine :
// Charge le moteur MyJScript ScriptEngine myjscript = runtime.GetEngine(typeof(MyJScript.DLR.MJSLanguageContext)); // Exécuter une commande MyJScript ScriptSource mjssrc = myjscript.CreateScriptSourceFromString("write('Hello world!');"); mjssrc.Execute(globals);
Le code précédent charge le moteur MyJScript puis lance l'exécution d'une instruction. Chargeons de la même manière le moteur IronPython:
// Charge le moteur IronPython ScriptEngine python = runtime.GetEngine(typeof(IronPython.Runtime.PythonContext)); // Exécuter une commande IronPython SriptSource pysrc = python.CreateScriptSourceFromString("print 'Hello world!';", SourceCodeKind.Statements); pysrc.Execute(globals);
Ecrivons un fonction utilitaire RunProgram pour faciliter l'écriture des exemples suivants.
static private void RunProgram(ScriptEngine engine, string command) { ScriptSource src = engine.CreateScriptSourceFromString(command, SourceCodeKind.Statements); src.Execute(globals); }
Nous pouvons maintenant mélanger les appels des deux compilateurs IronPython et MyJScript:
// Appelle une variable MyJScript depuis IronPython RunProgram(myjscript, "a='MyJScript';"); RunProgram(python, "print 'Hello',a;"); // Appelle une variable IronPython depuis MyJScript RunProgram(python, "b='IronPython';"); RunProgram(myjscript, "write('Hello '+b);");
Ici, nous initialisons à chaque fois une variable dans un des langages puis manipulons sa valeur dans l'autre langage. A chaque exécution, ce sont les règles spécifiques de chaque langage qui sont appliquées.
Peut-on allons plus loin, par exemple en créant des objets MyJScript et en les manipulant dans IronPython ou l'inverse ? Pas exactement. En effet, comme nous l'avons vu plus haut, la manipulation des objets nécessite des règles spécifiques qui ne sont utilisées que dans le contexte d'exécution du langage. Autrement dit, en IronPython, le moteur ne sait pas manipuler que des MJSObject et inversement, MyJScript ne sait pas manipuler des PyObject. Il y a donc un peu plus à creuser sur le mode de fonctionnement multi-langages pour arriver à échanger des objets.
Peu de documentation est publiée sur la DLR aujourd'hui. Le blog de Jim Hugunin's est la bible de la DLR mais hélas, il est mis à jour très rarement. En fait il n'a pas évolué depuis mon précédent article sur la DLR il y a plusieurs mois. Sa lecture est néanmoins intéressante à la lumière de ce que nous venons de voir pour mieux comprendre la conception de la DLR.
Le blog de John Lam est également une source précieuse d'information sur la DLR. John travaille sur IronRuby. Son blog est mis à jour très régulièrement même s'il ne traite pas exclusivement de la DLR.
La seule source d'information est sinon l'étude du code source de la DLR lui-même. Il est hélas très peu commenté et la seule documentation fournie est une compilation des commentaires ! Il peut être par contre instructif d'étudier l'implémentation des langages utilisant la DLR: le langage Toyscript est livré avec le code source de la DLR, c'est une sorte de Basic simplifié réalisé comme exemple. Le blog de Martin Maly donne de nombreuses explications de son fonctionnement mais hélas, sur une ancienne version de la DLR. L'étude de IronPython ou IronRuby est également intéressante mais beaucoup plus complexe vu leur taille actuelle.
Plusieurs webcast permettent également de se plonger dans la DLR, on notera en particulier, si vous avez accès aux vidéos du TechEd 2007, la session WEB404. Plus récemment, le symposium Lang.Net propose également des vidéos indispensables pour tous ceux qui s'intéressent à la compilation.
L'ensemble du code source de MyJScript est téléchargeable sur CodePlex (http://www.codeplex.com/MyJScript). Le code est largement commenté et intègre des tests unitaires. MyJScript est un JavaScript suffisamment complexe pour aborder des problèmes de l'implémentation d'un langage objet mais son implémentation est simplifiée pour se concentrer sur l'utilisation de la DLR. Il ne peut donc être réellement utilisé comme un "vrai" compilateur JavaScript.
Deux simplifications réalisées en MyJScript par rapport à JavaScript sont à noter:
Cet article a abordé les trois points les plus importants de la DLR:
Grâce à ces trois concepts, la DLR permet facilement d'implémenter son propre langage en disposant d'une interopérabilité native avec la CLR et les autres langages de la DLR. C'est incontestablement la grande force de la DLR.
Lionel Laské est architecte et Directeur Innovation à 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 de NSCAPE, une offre de migration des applications NS-DK/NatStar vers .NET. |