Using .NET to make your Application Scriptable

Dynamic Compilation

Finally on to the meat of the article. As I said already, compiler services are built in to the .NET framework. These live in the System.CodeDom namespace. I say compiler services and not compilers because CodeDom encompasses a lot more than that. However, the bits we're interested in live in the System.CodeDom.Compiler namespace.

To start off with we need an instance of a class that inherits from CodeDomProvider. This class provides methods to create instances of other helper classes specific to that language. Derivatives of CodeDomProvider are shipped with the framework for all four .NET languages, however the only two we're interested in are VB and C#, the most popular. They are Microsoft.VisualBasic.VBCodeProvider and Microsoft.CSharp.CSharpCodeProvider. The design of this system is so good that after choosing which of these to use, the steps are exactly the same.

The first thing we use is the CodeDomProvider's CreateCompiler method to get an instance of a class implementing ICodeCompiler. Once we have this that's the last we need from our CodeDomProvider. Our helper class will have a CompileScript function, which will accept script source, the path to an assembly to reference and a language to use. We'll overload it so the user can use their own codeprovider if they want to support scripting in a language other than VB or C#.

The next step once we have our ICodeCompiler is to configure the compiler. This is done using the CompilerParameters class. We create an instance of it using the parameterless constructor and configure the following things:

  • We don't want an executable, so set GenerateExecutable to false.
  • We don't want a file on disk, so set GenerateInMemory to true.
  • We don't want debugging symbols in it, so set IncludeDebugInformation to false.
  • We add a reference to the assembly passed as a parameter.
  • We add a reference to System.dll and System.Windows.Forms.dll to make the TextBox usable.

As a side note, to provide information for compilers specific to the language you are using (such as specifying Option Strict for VB) you use the CompilerOptions property to add extra command-line switches.

Once we have our CompilerParameters set up we use our compiler's CompileAssemblyFromSource method passing only our parameters and a string containing the script source. This method returns an instance of the CompilerResults class. This includes everything we need to know about whether the compile succeeded - if it did, the location of the assembly produced and if it didn't, what errors occured.

That is all this helper function will do. It will return the CompilerResults instance to the application for further processing.

[VB]
Public Shared Function CompileScript(ByVal Source As String, ByVal Reference As String, _
ByVal Provider As CodeDomProvider) As CompilerResults
    Dim compiler As ICodeCompiler = Provider.CreateCompiler()
    Dim params As New CompilerParameters()
    Dim results As CompilerResults
    'Configure parameters
    With params
        .GenerateExecutable = False
        .GenerateInMemory = True
        .IncludeDebugInformation = False
        If Not Reference Is Nothing AndAlso Reference.Length <> 0 Then _
            .ReferencedAssemblies.Add(Reference)
        .ReferencedAssemblies.Add("System.Windows.Forms.dll")
        .ReferencedAssemblies.Add("System.dll")
    End With
    'Compile
    results = compiler.CompileAssemblyFromSource(params, Source)
    Return results
End Function

[C#]
public static CompilerResults CompileScript(string Source, string Reference,
CodeDomProvider Provider)
{
    ICodeCompiler compiler = Provider.CreateCompiler();
    CompilerParameters parms = new CompilerParameters();
    CompilerResults results;
    // Configure parameters
    parms.GenerateExecutable = false;
    parms.GenerateInMemory = true;
    parms.IncludeDebugInformation = false;
    if (Reference != null && Reference.Length != 0)
        parms.ReferencedAssemblies.Add(Reference);
    parms.ReferencedAssemblies.Add("System.Windows.Forms.dll");
    parms.ReferencedAssemblies.Add("System.dll");
    // Compile
    results = compiler.CompileAssemblyFromSource(parms, Source);
    return results;
}

We also need one more helper function, which we will pretty much completely take straight from the plugin services we developed in my previous tutorial. This is the function that examines a loaded assembly for a type implementing a given interface and returns an instance of that class.

[VB]
Public Shared Function FindInterface(ByVal DLL As Reflection.Assembly, _
ByVal InterfaceName As String) As Object
    Dim t As Type
    'Loop through types looking for one that implements the given interface
    For Each t In DLL.GetTypes()
        If Not (t.GetInterface(InterfaceName, True) Is Nothing) Then
            Return DLL.CreateInstance(t.FullName)
        End If
    Next
    Return Nothing
End Function

[C#]
public static object FindInterface(System.Reflection.Assembly DLL, string InterfaceName)
{
    // Loop through types looking for one that implements the given interface
    foreach(Type t in DLL.GetTypes())
    {
        if (t.GetInterface(InterfaceName, true) != null)
            return DLL.CreateInstance(t.FullName);
    }
    return null;
}

You might also like...

Comments

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.” - Antoine de Saint Exupéry