Search website

C# as a self hosting scripting language

This topic isn’t new, but I find myself writing the same code again and again. Often times I have a project, written in C#, and I find I have need of a ‘scripting language’. I think: What is the best language to use as a scripting language for a program written in C#? The language I choose has to be easy to integrate with the host language, and I have to be able to compile it on demand, and there are invariably a dozen other requirements. What integrates well with C#? Why, C# does. As it turns out, C# is often an excellent choice for scripting in programs written in C#. Amazing, I know.

Thankfully, the process of compiling C# from C# has been trivial since roundabout .net 2.0, by using the CodeDom. This will all be replaced by Roslyn rather soon, but in the meantime it’s already so painless that it hardly needs replacing.

(Note: WordPress apparently doesn’t understand that ‘code’ means ‘preserve my formatting’. Until I figure out how to fix it, I’ll put code in gists too. https://gist.github.com/Blecki/4efe214ffbb5acedfedf )

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.CodeDom.Compiler;
using System.Reflection;

namespace Gem
{
    public class ScriptBuilder
    {
        private CodeDomProvider CodeProvider = CodeDomProvider.CreateProvider("CSharp");
        private CompilerParameters CompilerParameters = null;
        private Action<String> ReportErrors = null;

        private String UsingBlock = @"using System;
using System.Collections.Generic;
using System.Linq;
";
        private String TypeHeader = @"class CSharpScript {
    public static void Execute() {
";

        private String TypeFooter = @"
    }
}";

        public ScriptBuilder()
        {
            PrepareCompilerParameters();
        }

        public ScriptBuilder(Action<String> ReportErrors)
        {
            this.ReportErrors = ReportErrors;
            PrepareCompilerParameters();
        }

        private void PrepareCompilerParameters()
        {
            if (CompilerParameters != null) return;

            CompilerParameters = new CompilerParameters();
            CompilerParameters.GenerateExecutable = false;
            CompilerParameters.GenerateInMemory = true;

            CompilerParameters.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().Location);
            CompilerParameters.ReferencedAssemblies.Add("mscorlib.dll");
            CompilerParameters.ReferencedAssemblies.Add("System.dll");
            CompilerParameters.ReferencedAssemblies.Add("System.Core.dll");
            CompilerParameters.ReferencedAssemblies.Add("System.Data.Linq.dll");
        }

        public void AddReference(String AssemblyName, String AssemblyPath)
        {
            if (!String.IsNullOrEmpty(AssemblyName))
                UsingBlock += "using " + AssemblyName + ";\n";
            if (!String.IsNullOrEmpty(AssemblyPath))
                CompilerParameters.ReferencedAssemblies.Add(AssemblyPath);
        }

        public Assembly CompileFile(String FileContents)
        {
            var compilationResults = CodeProvider.CompileAssemblyFromSource(CompilerParameters, FileContents);

            if (compilationResults.Errors.Count > 0 && ReportErrors != null)
                foreach (var error in compilationResults.Errors)
                    ReportErrors((error as CompilerError).ErrorText);

            return compilationResults.CompiledAssembly;
        }

        public Action CompileScript(String Script)
        {
            var fileContents = UsingBlock + TypeHeader + Script + TypeFooter;
            var assembly = CompileFile(fileContents);

            if (assembly != null)
            {
                var scriptType = assembly.GetType("CSharpScript");
                if (scriptType == null) throw new InvalidOperationException("CSharpScript wasn't found in the generated assembly. This should be impossible.");
                var function = scriptType.GetMethod("Execute", BindingFlags.Public | BindingFlags.Static);
                if (function == null) throw new InvalidOperationException("static void Execute wasn't found in the CSharpScript type. This should be impossible.");
                return new Action(() => function.Invoke(null, null));
            }
            else
                return null;
        }
    }
}

Well, there it is. Compile away.

Switch to our mobile site