Posterous theme by Cory Watilo

Filed under: IronPython

Scriptability via the DLR and PostSharp

Making an application scriptable (particularly in a static language) has historically been difficult. With the advent of the DLR (Dynamic Language Runtime) on the .NET platform it becomes almost trivial to add scripting support to any application. For a recent project I needed the ability to add scripting hooks throughout the application and coupling the DLR with PostSharp AOP attributes made this effort pretty straightforward. Here's how it was done:

Bring in the DLR
The DLR is implemented as a set of plain old DLL's on top of the .NET Framework version 3.5...contrary to popular belief, you do not have to be running VS2010 or .NET 4 to use it.  Simply go to the CodePlex page for the DLR (http://dlr.codeplex.com), grab the source, and compile it.  There are solution files for VS2008 and VS2010. For my purposes I used IronPython as the scripting language but IronRuby is available in the download as well.  To use the DLR your project will need to reference the following assemblies:

  • Microsoft.Dynamic.dll
  • Microsoft.Scripting.Core.dll
  • Microsoft.Scripting.dll

In order to use IronPython, you will also need to reference these:

  • IronPython.dll
  • IronPython.Modules.dll
  • Microsoft.Scripting.ExtensionAttribute.dll

Register the Languages
Now that your project contains all the references it needs it's time to set up the little bit of code needed to host the DLR.  First you'll need to add a few things to your configuration file (App.config or Web.config depending on your project type...this works equally well with either).  Just add the following to the node of your config file:

<section name="microsoft.scripting" 
        type="Microsoft.Scripting.Hosting.Configuration.Section, Microsoft.Scripting, 
                Version=0.9.6.20, Culture=neutral, PublicKeyToken=null" 
        requirePermission="false" />

And add the following somewhere in the root node:

<microsoft.scripting>
    <languages>
        <language names="IronPython;Python;py" 
        extensions=".py" displayName="IronPython 2.6" 
        type="IronPython.Runtime.PythonContext, IronPython, 
        Version=2.6.10920.0, Culture=neutral, PublicKeyToken=null" />
    </languages>
</microsoft.scripting>

This basically just tells the runtime what languages to register, what versions of those languages, and some info about file extensions, etc.

Bring in PostSharp
Now I'm going to skip over to PostSharp for a minute. If you're not familiar with PostSharp you should run over to the website (http://www.postsharp.org) and check it out before continuing. In short, it's an AOP (Aspect Oriented Programming) library for .NET that allows easy injection of cross-cutting concerns through an application primarily via attributes. The 10sec. version of how it works is this...it injects code in a post-compilation step. This sounds scary, but it's not. In fact, this allows it to be extremely fast at runtime even if a small penalty is paid at compile-time. In order to get PostSharp setup, it's a simple download and install. If you don't want to install it on your machine, you can simply bin deploy it but it does require a tweak to your .proj file to get the post-compilation to work. If you want to go that route, you can find instructions here. At this point you will either follow those instructions or if you've installed directly, just include the following assemblies in your project:

  • PostSharp.Laos.dll
  • PostSharp.Public.dll

The ScriptHook Attribute
PostSharp's primary way of injecting an aspect is through attributes. In our case we're going to inherit from the OnMethodBoundaryAspect attribute in the PostSharp.Laos namespace. This attribute exposes four virtual methods that allow injection of code at different points in the execution of a method:

  • OnEntry
  • OnExit
  • OnException
  • OnSuccess

Each of these methods accept a MethodExecutionEventArgs as an argument that provides access (including arguments) to the method on which the attribute is applied. Here's the ScriptHook attribute:

[Serializable]
public class ScriptHookAttribute : OnMethodBoundaryAspect {

    private readonly ScriptEventTypes _scriptEventType;

    public ScriptEventAttribute(ScriptEventTypes scriptEventType) {
        _scriptEventType = scriptEventType;
    }

    public override void OnSuccess(MethodExecutionEventArgs eventArgs) {
        var args = eventArgs.GetReadOnlyArgumentArray();
        ScriptRunner.ExecuteEvent(_scriptEventType, args);
    }
}

For now, ignore the ScriptRunner reference, but notice that I've created an enum (ScriptEventTypes) that distinguishes different "types" of scripts to be run, by types, I mean different usages. At some point you can switch on the type of event that's occurring and change which scripts are loaded or how they are handled, etc. You would use the attribute like this:

[ScriptHook(ScriptEventTypes.ThingSaved)]
public void SaveThing(Thing t) { }

[ScriptHook(ScriptEventTypes.ThingDeleted)]
public void DeleteThing(Thing t) { }

The ScriptRunner
At this point we've got the DLR available and we have the PostSharp attribute that's injecting our hooks. Now we need something to execute our scripts. Enter the ScriptRunner:

public static class ScriptRunner {

    public static void ExecuteEvent(ScriptEventTypes scriptEventType, object[] eventContext) {

    IScriptEvent scriptEvent = null;

    switch (scriptEventType) {
        case ScriptEventTypes.ThingSaved:
            scriptEvent = new ThingSavedScriptEvent();
            break;
        case ScriptEventTypes.ThingDeleted:
            scriptEvent = new ThingDeletedScriptEvent;
            break;
        }

        if (scriptEvent != null) {
            try {
                scriptEvent.ExecuteScripts(eventContext);
            } catch {
                throw new ScriptExecutionExecption();
            }
        }
    }
}

The runner is just a simple static class with a single ExecuteEvent method...it's so simple in fact that it speaks for itself. The one thing to note is the eventContext argument to the ExecuteEvent method. If you look back at the ScriptHook attribute you can see that is' passing an object array from the MethodExecutionEventArgs instance. This object array contains all the arguments to the executing method. So for SaveThing() or DeleteThing() for example the eventContext array contains the instance of Thing that was passed.

The IScriptEvent Classes
Lastly, we need something to actually grab the scripts and execute them in something like the context of the executing method. So that's where our example ThingSavedScriptEvent and ThingDeletedScriptEvent come in to play. Here's an example:

public class ThingSavedScriptEvent : IScriptEvent {

    private readonly ScriptRuntime _scriptRuntime;
    private readonly IThingRepository _thingRepo;

    public ProjectCreatedScriptEvent() {
        _thingRepo = new ThingRepository();
        _scriptRuntime = ScriptRuntime.CreateFromConfiguration();
    }

    public void ExecuteScripts(object[] contextValues) {

        var thing = (Thing)contextValues.SingleOrDefault(x => x is Thing);
        
                var scriptScope = _scriptRuntime.CreateScope();
        scriptScope.SetVariable("thingRepo", _thingRepo);
        scriptScope.SetVariable("thing", thing);

        var script = GetScript();

        var scriptEngine = _scriptRuntime.GetEngine("IronPython");
        scriptEngine.Execute(script, scriptScope);
    }

    private static string GetScript() {

        var script = string.Empty;
        try {
            code = File.ReadAllText(HttpContext.Current.Server.MapPath(
                ConfigSettings.ProjectTemplateStorePath + "ThingSaved.py"));
        } catch (FileNotFoundException) { }
        return script;
    }
}

The ScriptRuntime class is where the DLR magic happens. We create an instance of it and then ask it for an engine that can execute IronPython scripts. ScriptScope is a class used for constructing the scope that the script will have available to it. Here for example we're "passing" the 'thingRepo' and 'thing' variables into the script via the ScriptScope. Lastly, we get the engine from the ScriptRuntime, pass it both the ScriptScope and the literal text of the script. And that's that...pretty easy eh?