Runtime Scripting in .Net

The ability to ship source code with your product that can be compiled or interpreted at runtime is a very valuable asset. Video games do this all the time with things like AI allowing the end-user to easily mod the game. In compiled languages like C++, this technique is highly advantageous as it allows you to avoid the costly compile-link cycle for every small change to a source file. This is also helpful in an environment where creating a new build, generating the installer, and deploying it is overkill or difficult to achieve. In this article we will examine how you can achieve this from .Net programs consuming a C# or VB.Net script. I’ll refer to this as “scripting”, even though an unambiguous definition of a “script” is somewhat difficult to nail down.

Using a C# or VB script from a .Net language is actually very simple. The basic idea is to read in a source file, which could come from the network or disk, and compile it into a temporary assembly on file or IL in memory. Then you can instantiate types from the script file and use them just as easily as any other types already defined. Normally the hard part in this process is locating and invoking the proper compiler. Luckily, we can compile IL code using a CodeDomProvider like CSharpCodeProvider. And, since these types are part of the .Net framework we know they are available on the end-user PC! Life is great eh?

Ok, so first make sure you have referenced the following namespaces:

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

Next you need to setup the compiler parameters. These are simply the options given to the compiler such as whether to create the resulting assembly in memory or if debug information should be included in the generated code. You can do this with the CompilerParameters type. Here is an example for creating an in memory assembly with debug information:

 CompilerParameters cparams = new CompilerParameters();
 
 cparams.GenerateInMemory = false;
 cparams.IncludeDebugInformation = true;
 cparams.CompilerOptions += " /debug:pdbonly";
 
 cparams.TreatWarningsAsErrors = false;
 cparams.GenerateExecutable = false;

You would also want to add any references that your code needs using CompilerParameters. ReferencedAssemblies. So, after you have the compiler options set you need to actually invoke the C# or Visual Basic compiler. You do this by creating a CodeDomCompiler of the appropriate type:

VBCodeProvider – for Visual Basic
CSharpCodeProvider – for C Sharp

Then you can use the provider to compile an assembly like so:

CodeDomProvider provider = CSharpCodeProvider();
CompilerResults result = provider.CompileAssemblyFromFile(cparams, script);

Where cparams are your compiler parameters and result is well, your results. Within results you will find out if the compilation was successful or generated warnings. It will also give you access to the resulting assembly. You can check for errors using results.Errors.HasErrors and reference the assembly with results.CompiledAssembly.

Now that you have a compiled assembly in memory, there is one last issue to address. How do you instantiate types from the assembly? The easiest way to attack the problem is to create an interface that your application can access and derive a type from that interface in your script. I’ve created a full example below (explained in code comments) to illustrate the technique I am talking about.

C# Example

Main application – Script.cs

using System;
using System.CodeDom.Compiler;
using Microsoft.CSharp;
using Microsoft.VisualBasic;
using System.Reflection;
using System.IO;
 
namespace ScriptingExample
{
    public class Script
    {
        public LoadedType LoadScript<LoadedType>(string script, string referenceString) where LoadedType:class
        {
            // this will load/compile the script source file, "script", and return a type of LoadedType
            // it is assumed that a type of "LoadedType" is defined in the script
            // the referenceString is a comma delimited string identifying all references needed by the script
            // e.g. "System.dll,System.Windows.Forms.dll"
            // either the VB or C# compiler will be invoked, based upon the file extenstion .vb or .cs
 
            // try and compile the script
            CompilerParameters cparams = CreateCompilerParameters(referenceString);
            CompilerResults results = CompileScript(script, cparams);
 
            // check for errors
            if (results.Errors.HasErrors)
            {
                string e = "";
                foreach (CompilerError error in results.Errors)
                    e += (error.ErrorText + Environment.NewLine);
 
                throw new Exception("ERROR IN SCRIPT: " + Environment.NewLine + e);
            }
 
            // find the "exported" type to load
            LoadedType scriptObject = FindScriptObject<LoadedType>(results.CompiledAssembly);
 
            if (scriptObject == null)
                throw new Exception("CAN'T LOAD SCRIPT, TARGET TYPE NOT IMPLEMENTED IN SCRIPT.");
            else
                return scriptObject;
        }
 
        private CompilerResults CompileScript(string script, CompilerParameters cparams)
        {
            // compiles the script using the specified parameters
            // either the VB or C# compiler will be invoked, based upon the file extenstion .vb or .cs
            CodeDomProvider provider = GetCompiler(script);
            CompilerResults result = provider.CompileAssemblyFromFile(cparams, script);
 
            return result;
        }
 
        CodeDomProvider GetCompiler(string script)
        {
            // returns the correct compiler to use based on the file extension of the script
            string extension = Path.GetExtension(script);
 
            if (extension == ".vb")
                return new VBCodeProvider();
            else if (extension == ".cs")
                return new CSharpCodeProvider();
            else
                throw new Exception("UNKNOWN SCRIPT TYPE");
        }
 
        private LoadedType FindScriptObject<LoadedType>(Assembly assembly) where LoadedType : class
        {
            // attempts to locate the type "LoadedType" within the assembly that was created from the "script"
            foreach (Type t in assembly.GetTypes())
                if (typeof(LoadedType).IsAssignableFrom(t))
                    return Activator.CreateInstance(t) as LoadedType;
 
            return default(LoadedType);
        }
 
        private CompilerParameters CreateCompilerParameters(string referenceString)
        {
            // creates compiler parameters to generate an in memory assembly, with debug
            // symbols if this is a debug build
            // the referenceString is a comma delimited string identifying all references needed by the script
            CompilerParameters cparams = new CompilerParameters();
 
            cparams.GenerateInMemory = true;
 
            #if DEBUG
                // create debug symbols if this is a debug build
                cparams.IncludeDebugInformation = true;
                cparams.CompilerOptions += " /debug:pdbonly";
            #endif
 
            cparams.TreatWarningsAsErrors = false;
            cparams.GenerateExecutable = false; 
 
            // seperate reference string
            string[] references = referenceString.Split(',');
 
            foreach (string reference in references)
                cparams.ReferencedAssemblies.Add(reference);
 
            return cparams;
        }
    }
}

App Showing How to Use Script.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.IO;
 
namespace ScriptingExample
{
    // we have to define an interface so we know how to "talk" to
    // the script, that is, there must be a type that we *know* the
    // script implements so we can call methods on it
    public interface MyScript
    {
        void Execute();
    }
 
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
 
        private void Form1_Load(object sender, EventArgs e)
        {
            // just populate the combo box with all "scripts" located in the directory "Scripts"
            foreach (string file in Directory.GetFiles("Scripts"))
                comboBox1.Items.Add(file);
 
            if (comboBox1.Items.Count > 0)
                comboBox1.SelectedIndex = 0;
            else
            {
                button1.Enabled = false;
                comboBox1.Enabled = false;
            }
        }
 
        private void button1_Click(object sender, EventArgs e)
        {
            // load the script, which contains a type that implements MyScript
            // and call the execute method on it
            Script s = new Script();
 
            // these are the references the script will need, we must reference this executable
            // because the script needs to see the MyScript data type
            // this is just a sample, in a real application such types would be defined in an assembly dll
            // that is shared between the app consuming the script and the script itself
            string references = "System.dll,System.Windows.Forms.dll," + Directory.GetCurrentDirectory() + "\\ScriptingExample.exe";
 
            MyScript script = s.LoadScript<MyScript>(comboBox1.SelectedItem as string, references);
 
            script.Execute();
        }
    }
}

A VB and C# Script

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
 
namespace ScriptingExample.Scripts
{
    public class CSharpScript : MyScript
    {
        public void Execute()
        {
            MessageBox.Show("I AM A C# SCRIPT");
        }
    }
}
Imports System
Imports System.Collections.Generic
Imports System.Text
Imports System.Windows.Forms
 
Namespace ScriptingExample.Scripts
    Public Class VBScript
        Implements MyScript
        Public Sub Execute() Implements MyScript.Execute
            MessageBox.Show("I AM A VB SCRIPT!!")
        End Sub
    End Class
End Namespace

Conclusion

And that’s it! A very simple technique that can be used in many interesting ways. I’ve included a link to a Visual Studio project containing an implementation using the above information. The project can be found here As always, let me know if you have any questions or ideas!

See ya next time!

September 30, 2009 · crow · 4 Comments
Posted in: C#

4 Responses

  1. Bodyc - October 1, 2009

    Thank you! I would now go on this blog every day!
    Bodyc

  2. lazy - January 10, 2010

    thnak nice

    http://csharptalk.com

    lazy

  3. ajay - March 9, 2010

    its really nice.
    is it possible to creat script engine. in c#?
    actually i have one VB6 application through which i can execute vbscript and java script text file .Named as Script Tester.
    But whatever the script i write it is in .tst or .Seq file.
    we dont use .vbs extension for script file.
    i am not getting how we can compile code writen in .txt file using vb application.
    i tried to compile these file using Wscript.exe but it not executeing.
    can you please guide me on this how this application works
    is it possible create simillar application in dotnet.
    Please Mail me if any suggestion on below mailID.
    ajay_dhuppe@rediffmail.com

  4. when did i ovulate - January 14, 2012

    Websites you should visit…

    [...]below you’ll find the link to some sites that we think you should visit[...]……

Leave a Reply

Connect with Facebook

*