NOTE: This post was originally written in 2009 so it may be dated. I’m resurrecting it due to relative popularity. This post has been copied between several blogging systems (some of which were home-brewed) and some formatting has been lost along the way.
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.