Compilation dynamique en C#

Evaluer une expression mathématique en compilant du code C# au runtime

Introduction

Il existe beaucoup d'articles sur la manière d'évaluer dynamiquement des expressions mathématiques. Cette semaine, je vais vous montrer comment on peut, à l'aide du Framework .NET 2.0, compiler du code C# à la volée et l'utiliser dans une application.

Imaginons que nous ayons besoin de calculer une fonction 3D telle que celle-ci ;

z = f(x,y)

Nous voulons laisser à l'utilisateur la possibilité de définir lui-même la fonction f. Par exemple :

z = Math.Cos(x) * Math.Sin(y)

Une solution possible pour résoudre ce problème en moins de 10 minutes serait de :

  • Laisser l'utilisateur saisir sa fonction
  • Compiler cette fonction en mémoire
  • Exécuter la fonction en lui passant les paramètres x et y
  • Afficher le résultat z

Un peu de code...

Pour commencer, nous allons définir une classe avec une méthode virtuelle qui servira de modèle pour l'implémentation de notre fonction :

namespace MathParser
{
public class MathBaseClass
{
public MathBaseClass()
{
}
public virtual double eval(double x, double y)
{
return 0.0;
}
}
}

La classe MathExpressionParser

Cette classe va compiler au vol une nouvelle classe myParser qui héritera de la classe MathBaseClass définie ci-dessus et implémentera la méthode eval en utilisant la fonction choisie par l'utilisateur et passée en paramètre.

La nouvelle classe est compilée dans une assembly créée en mémoire. Elle est ensuite immédiatement instanciée (myObj). La classe MathExpressionParser contient également une méthode eval() qui nous permettra d'invoquer la méthode eval() de la classe dynamique et de récupérer sa valeur de retour.

using System;
using System.Reflection;

namespace MathParser
{
public class MathExpressionParser
{
MathBaseClass myObj = null;

public MathExpressionParser() {}

public bool init(string expr)
{
Microsoft.CSharp.CSharpCodeProvider cp
= new Microsoft.CSharp.CSharpCodeProvider();

System.CodeDom.Compiler.ICodeCompiler ic =
cp.CreateCompiler();

System.CodeDom.Compiler.CompilerParameters cpar
= new System.CodeDom.Compiler.CompilerParameters();

cpar.GenerateInMemory = true;
cpar.GenerateExecutable = false;

cpar.ReferencedAssemblies.Add("system.dll");
cpar.ReferencedAssemblies.Add("MathParser.exe");

string src = "using System;" +
"class myParser: MathParser.MathBaseClass" +
"{" +
"public myParser(){}" +
"public override double eval(double x,double y)" +
"{" +
"return " + expr + ";" +
"}" +
"}";

System.CodeDom.Compiler.CompilerResults cr
= ic.CompileAssemblyFromSource(cpar, src);

foreach (System.CodeDom.Compiler.CompilerError ce in cr.Errors)
Console.WriteLine(ce.ErrorText);

if (cr.Errors.Count == 0 && cr.CompiledAssembly != null)
{
Type ObjType = cr.CompiledAssembly.GetType("myParser");
try
{
if (ObjType != null)
{
myObj = (MathBaseClass)Activator.
CreateInstance(ObjType);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return true;
}
else
return false;
}

public double eval(double x, double y)
{
double val = 0.0;
if (myObj != null)
{
val = myObj.eval(x, y);
}
return val;
}
}
}

Utilisation

Enfin, il ne nous reste plus qu'à créer une petite application console pour tester notre mécanique.
On commence par instancier la classe MathExpressionParser et par appeler la méthode init() en lui passant la fonction mathématique à évaluer. La compilation a donc lieu une seule fois et les appels successifs à la méthodes eval() sont extrêmement rapides.

using System;
using System.Collections.Generic;
using System.Text;

namespace MathParser
{
class Program
{
static void Main(string[] args)
{
MathExpressionParser exp = new MathExpressionParser();
exp.init("Math.Cos(x)*Math.Sin(y)");

for (int x = 0; x < 10; x++)
{
Console.WriteLine( exp.eval((double)x, (double)x) );
}
Console.ReadKey();
}
}
}

Commentaires