Mathematische Ausdrücke evaluieren, C#-Code zur Laufzeit compilieren

Einleitung

Das .NET-Framework bietet über das CodeDomProvider-Objekt eine Möglichkeit zur Laufzeit Sourcecode zu kompilieren und diesen auszuführen. Es kann somit auf elegante Art und Weise beliebiger Code, u.A. mathematische Ausdrücke im C#-Syntax, evaluiert werden. Die Klasse CCodeCompiler kompiliert eine zur Laufzeit in C# „geschriebene“ Funktion (sbCode) und gibt deren Referenz für die Ausführung zurück. Die genaue Anwendung ist im unten stehenden Anwendungsbeispiel beschrieben.

Code

using Microsoft.CSharp;
using System.Reflection;
using System.CodeDom.Compiler;

/// <summary>
/// helper class for compiling C#-code at runtime
/// free code (W) 2010 by admin of codezentrale.de
/// </summary>
public class CCodeCompiler
{
    private Assembly _assCompiledAssembly = null;
    private MethodInfo _miMethodInfo = null;
    private Type _tyContainerType = null;
    private bool _bCompiled = false;
    private string _sClassName = string.Empty;
    private CompilerErrorCollection _cecCompilerErrors = null;
    /// <summary>
    /// reference to the compiled assembly
    /// </summary>
    public Assembly CompiledAssembly
    {
        get { return CompiledAssembly; }
    }
    /// <summary>
    /// reference to the compiled method
    /// </summary>
    public MethodInfo MethodInfo
    {
        get { return _miMethodInfo; }
    }
    /// <summary>
    /// type of the compiled method
    /// </summary>
    public Type ContainerType
    {
        get { return _tyContainerType; }
    }
    /// <summary>
    /// flag, if compiling was successfully
    /// </summary>
    public bool IsCompiled
    {
        get { return _bCompiled; }
    }
    /// <summary>
    /// classname of the compiled method
    /// </summary>
    public string ClassName
    {
        get { return _sClassName; }
    }
    /// <summary>
    /// a string with all the errors stored in the CompilerErrorCollection
    /// </summary>
    public string CompilerErrors
    {
        get
        {
            string sRetVal = string.Empty;

            if (_cecCompilerErrors != null)
            {
                foreach (CompilerError ce in _cecCompilerErrors)
                {
                    sRetVal += ce.ToString() + Environment.NewLine;
                }
            }

            return sRetVal;
        }
    }
    /// <summary>
    /// creates a virtual method from given sourcecode
    /// </summary>
    /// <param name="sSourceCode">the complete sourcecode of the method</param>
    /// <param name="sMethodName">the name of the method</param>
    /// <param name="bIncludeDebugInfo">include debuginfo into the compiled assembly</param>
    /// <param name="psaReferences">some referenced assmblies (.NET DLLs) beiing used for working the code, full pathname needed</param>
    /// <returns>true if the compilation was successfull, else false</returns>
    public bool CreateMethodRef(string sSourceCode, string sMethodName, bool bIncludeDebugInfo, params string[] psaReferences)
    {
        _assCompiledAssembly = null;
        _miMethodInfo = null;
        _tyContainerType = null;
        _bCompiled = false;
        _sClassName = sMethodName + "Container";
        _cecCompilerErrors = null;

        // the compiler object
        CodeDomProvider cdpCompilerObject = CodeDomProvider.CreateProvider("CSharp");

        // build options
        CompilerParameters cpParams = new CompilerParameters();
        cpParams.GenerateInMemory = true;
        cpParams.GenerateExecutable = false;
        cpParams.IncludeDebugInformation = bIncludeDebugInfo;
        cpParams.ReferencedAssemblies.AddRange(psaReferences);

        // string for the used namespace
        string sNameSpace = cpParams.OutputAssembly;
        if (sNameSpace == null) sNameSpace = "Evaluated";

        // build the complete code
        StringBuilder sbCode = new StringBuilder();

        // include the needed assemblies
        foreach (string sAssemblies in cpParams.ReferencedAssemblies)
        {
            sbCode.Append("using " + Assembly.LoadFrom(sAssemblies).GetName().Name + ";" + Environment.NewLine);
        }

        // include namespace
        sbCode.Append("namespace " + sNameSpace + Environment.NewLine);
        sbCode.Append("{" + Environment.NewLine);
        // include the class
        sbCode.Append("    public class " + _sClassName + Environment.NewLine);
        sbCode.Append("    {" + Environment.NewLine);
        // include the given sourcecode-part
        sbCode.Append(sSourceCode);
        sbCode.Append("    }" + Environment.NewLine);
        sbCode.Append("}" + Environment.NewLine);

        // compile the sourcecode
        CompilerResults crResults = cdpCompilerObject.CompileAssemblyFromSource(cpParams, sbCode.ToString());

        // some errors?
        if (crResults.Errors.HasErrors)
        {
            _cecCompilerErrors = crResults.Errors;
        }
        else
        {
            // when everything was fine, set some variables
            // assembly
            _assCompiledAssembly = crResults.CompiledAssembly;
            // type of method
            _tyContainerType = _assCompiledAssembly.GetType(sNameSpace + "." + _sClassName, true);
            // the method
            _miMethodInfo = _tyContainerType.GetMethod(sMethodName);
            
            _bCompiled = true;
        }

        return _bCompiled;
    }
    /// <summary>
    /// creates a virtual method from given sourcecode
    /// </summary>
    /// <param name="sSourceCode">the complete sourcecode of the method</param>
    /// <param name="sNameSpace">a namespace where the source is compiled</param>
    /// <param name="sMethodName">the name of the method</param>
    /// <param name="bIncludeDebugInfo">include debuginfo into the compiled assembly</param>
    /// <returns>true if the compilation was successfull, else false</returns>
    public bool CreateMethodSimple(string sSourceCode, string sNameSpace, string sMethodName, bool bIncludeDebugInfo)
    {
        _assCompiledAssembly = null;
        _miMethodInfo = null;
        _tyContainerType = null;
        _bCompiled = false;
        _sClassName = sMethodName + "Container";
        _cecCompilerErrors = null;

        // the compiler object
        CodeDomProvider cdpCompilerObject = CodeDomProvider.CreateProvider("CSharp");

        // build options
        CompilerParameters cpParams = new CompilerParameters();
        cpParams.GenerateInMemory = true;
        cpParams.GenerateExecutable = false;
        cpParams.IncludeDebugInformation = bIncludeDebugInfo;

        // add (reference) all global available assemblies
        foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
        {
            cpParams.ReferencedAssemblies.Add(asm.Location);
        }

        // compile the sourcecode
        CompilerResults crResults = cdpCompilerObject.CompileAssemblyFromSource(cpParams, sSourceCode);

        // some errors?
        if (crResults.Errors.HasErrors)
        {
            _cecCompilerErrors = crResults.Errors;
        }
        else
        {
            // when everything was fine, set some variables
            // assembly
            _assCompiledAssembly = crResults.CompiledAssembly;
            // type of method
            _tyContainerType = _assCompiledAssembly.GetType(sNameSpace + "." + _sClassName, true);
            // the method
            _miMethodInfo = _tyContainerType.GetMethod(sMethodName);

            _bCompiled = true;
        }

        return _bCompiled;
    }
}

Anwendungsbeispiel

Nachfolgend sind zwei Beispiele für die Anwendung von CCodeCompiler aufgezeigt. Folgender C#-Code wird dabei zur Laufzeit kompiliert und ausgeführt:

using System;
namespace Evaluated
{
    public class CalcContainer
    {
        public double Calc(double[] x)
        {
            double dResult = 0.0;
            dResult = x[0] + x[1];
            return dResult;
        }
    }
}

Im Beispiel 1 werden explizit die zu referenzierenden Assemblies mit vollständigem Pfad und Namen angegeben, im Beispiel 2 dagegen intern von der Funktion alle verfügbaren Assemblies automatisch eingeladen.

using Microsoft.CSharp;
using System.Reflection;
using System.CodeDom.Compiler;

// example 1
private void btnEvaluateRef_Click(object sender, System.EventArgs e)
{
    // the methodname
    string sMethodName = "Calc";

    // example code
    StringBuilder sbCode = new StringBuilder();
    sbCode.Append("        public double " + sMethodName + "(double[] x)" + Environment.NewLine);
    sbCode.Append("        {" + Environment.NewLine);
    sbCode.Append("            double dResult = 0.0;" + Environment.NewLine);
    sbCode.Append("            dResult = x[0] + x[1];" + Environment.NewLine);
    sbCode.Append("            return dResult;" + Environment.NewLine);
    sbCode.Append("        }" + Environment.NewLine);

    // reference one needed assembly for "Math" called "System"
    string[] saReferenceAssemblyDLLs = new string[1];
    saReferenceAssemblyDLLs[0] = @"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.dll"; // for "using System;"

    try
    {
        // create a codecompiler object
        CCodeCompiler rtc = new CCodeCompiler();

        // compile and create the Calc-Method
        if (rtc.CreateMethodRef(sbCode.ToString(), sMethodName, false, saReferenceAssemblyDLLs))
        {
            // reference to the method
            MethodInfo miMethod = rtc.MethodInfo;

            // some input values for test calculation
            double[] daX = new double[2];
            daX[0] = 1.2;
            daX[1] = 2.0;

            // call the compiled method with input values
            string sResult = miMethod.Invoke(Activator.CreateInstance(rtc.ContainerType), new object[] { daX } ).ToString();
        }
        else
        {
            // show compiler errors
            MessageBox.Show(rtc.CompilerErrors);
        }
    }
    catch (Exception ce)
    {
        // if there is a exception during execution, show it
        MessageBox.Show(ce.Message + Environment.NewLine + ce.StackTrace);
    }
}

// example 2
private void btnEvaluateSimple_Click(object sender, System.EventArgs e)
{
    // the namespace
    string sNameSpace = "Evaluated";
    // the methodname
    string sMethodName = "Calc";

    // example code
    StringBuilder sbCode = new StringBuilder();
    // some assembly references
    sbCode.Append("using System;" + Environment.NewLine);
    // include namespace
    sbCode.Append("namespace " + sNameSpace + Environment.NewLine);
    sbCode.Append("{" + Environment.NewLine);
    // include the class
    sbCode.Append("    public class " + sMethodName + "Container" + Environment.NewLine);
    sbCode.Append("    {" + Environment.NewLine);
    sbCode.Append("        public double " + sMethodName + "(double[] x)" + Environment.NewLine);
    sbCode.Append("        {" + Environment.NewLine);
    sbCode.Append("            double dResult = 0.0;" + Environment.NewLine);
    sbCode.Append("            dResult = x[0] + x[1];" + Environment.NewLine);
    sbCode.Append("            return dResult;" + Environment.NewLine);
    sbCode.Append("        }" + Environment.NewLine);
    sbCode.Append("    }" + Environment.NewLine);
    sbCode.Append("}" + Environment.NewLine);

    try
    {
        // create a codecompiler object
        CCodeCompiler rtc = new CCodeCompiler();

        // compile and create the Calc-Method without any assembly
        // these will be referenced all internally
        if (rtc.CreateMethodSimple(sbCode.ToString(), sNameSpace, sMethodName, false))
        {
            // reference to the method
            MethodInfo miMethod = rtc.MethodInfo;

            // some input values for test calculation
            double[] daX = new double[2];
            daX[0] = 1.2;
            daX[1] = 2.0;

            // call the compiled method with input values
            string sResult = miMethod.Invoke(Activator.CreateInstance(rtc.ContainerType), new object[] { daX } ).ToString();
        }
        else
        {
            // show compiler errors
            MessageBox.Show(rtc.CompilerErrors);
        }
    }
    catch (Exception ce)
    {
        // if there is a exception during execution, show it
        MessageBox.Show(ce.Message + Environment.NewLine + ce.StackTrace);
    }
}