Chris Umbel

Scripting Your .Net Applications with IronPython

At several points in my .Net development career I've had the need to make an application I wrote scriptable. Sometimes it was to provide easy product extension to customers or lower level information workers. Sometimes it was to ease maintenance of very fine grained logic that has the capacity to change frequently or unpredictably. But every time I found it to be one of the more interesting facets of the project at hand.

Early in .Net's history this was made easy by using Visual Studio for Applications (VSA) which allowed you to host arbitrary C# or VB.Net code within the executing AppDomain. Unfortunately VSA was plagued with resource leak problems and was therefore impractical in most enterprise situations. VSA was eventually deprecated.

One of the many alternatives is to perform dynamic, on-the-fly code compilation. While certainly quite manageable it was a bit more complex and much akin to cutting down a sapling with a chainsaw.

Another option that came along later is Visual Studio Tools for Applications which brought the Visual Studio IDE to the scripter.

My favorite avenue, however, is to host a Dynamic Language Runtime (DLR) and use a language like IronPython. Not only is it disgustingly simple to implement from a plumbing point of view but Python itself seems like a natural fit due to it's simplicity. IronRuby's another wonderful choice but I'll stick to IronPython for the scope of this post.

Examples

The demonstration I'm about to show you was done using Visual Studio 2008 and IronPython 2.6 RC2. All you have to do is reference:

  • IronPython.dll
  • Microsoft.Scripting.dll
  • Microsoft.Scripting.Core.dll
and your project is ready to go. You may also want to reference IronPython.Modules.dll to get access to python's standard library.

All of the following examples require the following imports:

using System;
using IronPython.Hosting;
using Microsoft.Scripting.Hosting;

This is very basic. It simply executes a Python print statement.

static void Main(string[] args)
{
    /* bring up an IronPython runtime */
    ScriptEngine engine = Python.CreateEngine();
    ScriptScope scope = engine.CreateScope();
    
    /* create a source tree from code */
    ScriptSource source =
        engine.CreateScriptSourceFromString("print 'hello from python'");
        
    /* run the script in the IronPython runtime */
    source.Execute(scope);
}

which produces:

hello from python

Scripting isn't very useful if the script can't affect the AppDomain around it. Here's an example that modifies an integer from the calling program.

static void Main(string[] args)
{
    ScriptEngine engine = Python.CreateEngine();
    ScriptScope scope = engine.CreateScope();
    
    /* create a Python variable "i" with the value 1 */
    scope.SetVariable("i", 1);
    
    /* this script will simply add 1 to it */
    ScriptSource source = engine.CreateScriptSourceFromString("i += 1");
    source.Execute(scope);
    
    /* pull the value back out of IronPython and display it */
    Console.WriteLine(scope.GetVariable<int>("i").ToString());
}

producing

2

Naturally scripts would frequently operate on domain objects in the real world:

public class Employee
{ 
    public double Salary { get; set; }
    public bool Good { get; set; }
}

The following code conditionally modifies an Employee object.

static void Main(string[] args)
{
    Employee employee = new Employee() { Salary = 50000, Good = true };

    ScriptEngine engine = Python.CreateEngine();
    ScriptScope scope = engine.CreateScope();
    scope.SetVariable("employee", employee);

    /* a more complex script this time */
    ScriptSource source = engine.CreateScriptSourceFromString(
@"
def evaluate(e):
    if e.Good:
        e.Salary *= 1.05

evaluate(employee)
");

    source.Execute(scope);
    Console.WriteLine(scope.GetVariable<Employee>("employee").Salary);
}

You can also call functions in a python script:

ScriptSource source = engine.CreateScriptSourceFromString(  
@"  
def fun():
        print 'hello from example function'
");  

engine.Operations.Invoke(scope.GetVariable("fun"));

Conclusion

As you can see there really isn't much plumbing involved in hosting an IronPython runtime. In my opinion it combines both ease and power producing nearly perfect extension.

Tue Nov 03 2009 22:11:00 GMT+0000 (UTC)

Follow Chris
RSS Feed
Twitter
Facebook
CodePlex
github
LinkedIn
Google