Re.Mark

My Life As A Blog

Embedding IronPython as a scripting language

leave a comment »

A great way of extending an application is to allow users to write their own scripts.  Fortunately, with the Dynamic Language Runtime it is fairly simple to provide scripting in your application.  Let’s look at how to do it with a simple Windows Forms application and IronPython.

Creating the Windows Forms application
Firstly, let’s create a form called MainForm.  Add three TextBoxes (TopTextBox, MiddleTextBox and BottomTextBox), three Buttons (UpButton, DownButton and ResetButton), a multi-line TextBox (OutputTextBox) and a Menu with a MenuItem called ScriptMenuItem and two MenuItems within the ScriptMenuItem called WriteToolStripMenuItem and RunToolStripMenuItem.  Here’s a shot of what that looks like:

 Scriptable1

Next we need a simple form to be able to type in our script.  Create a form called ScriptForm with a multiline TextBox called ScriptTextBox and a couple of Buttons (called SaveButton and ClearButton.)  Here’s what the form looks like:

 ScriptForm

The first form doesn’t do too much – just lets us move the values on the form around.  That means the click events for the buttons looks like this:

private void UpButton_Click(object sender, EventArgs e) {      SetText(MiddleTextBox.Text, BottomTextBox.Text, TopTextBox.Text); } private void DownButton_Click(object sender, EventArgs e) {     SetText(BottomTextBox.Text, TopTextBox.Text, MiddleTextBox.Text); } private void ResetButton_Click(object sender, EventArgs e) {      SetText(string.Empty, string.Empty, string.Empty); }

They all call into a simple SetText method:

private void SetText(string top, string middle, string bottom) {      TopTextBox.Text = top;      MiddleTextBox.Text = middle;      BottomTextBox.Text = bottom; }

The next thing to do is to hook up the WriteToolStripMenuItem. To do that, let’s hold a reference to ScriptForm:

private ScriptForm scriptForm = new ScriptForm();

Then edit the click event handler for WriteToolStripMenuItem like so:

private void WriteToolStripMenuItem_Click(object sender, EventArgs e) {

     scriptForm.Show(); }

Once that’s done, let’s add some simple handlers for the click events for the buttons on ScriptForm:

private void ClearButton_Click(object sender, EventArgs e) {      this.ScriptTextBox.Text = string.Empty;      this.Visible = false; } private void SaveButton_Click(object sender, EventArgs e) {     this.Visible = false; }

That’s the basics taken care of.  We can open the script form and type in some Python code.  But we can’t run it yet.  To do that we need to add scripting support.

 

Adding Scripting Support
The first step in adding scripting support is to add the appropriate references to the application.  The references you need are IronPython, Microsoft.Scripting and Microsoft.Scripting.Core.  You’ll find these dlls in the IronPython distribution and / or the DLR distribution (depending on what you’ve downloaded and installed.)

Once we’ve got the right references, we need to create a script engine and a scope in which our script will execute.  To make this simple, let’s hold references to the engine and scope at form level:

private ScriptScope scope; private ScriptEngine engine;


 

And create a method to set the runtime up:

private void CreateScriptRuntime() {     this.engine = Python.CreateEngine();     this.scope = engine.CreateScope(); }

Now we can wire up the click event handler for RunToolStripMenuItem:

private void RunToolStripMenuItem_Click(object sender, EventArgs e) { RunScript(this.scriptForm.ScriptTextBox.Text); } private void RunScript(string code) { if (!string.IsNullOrEmpty(code) { ScriptSource source = this.engine.CreateScriptSourceFromString(code, Microsoft.Scripting.SourceCodeKind.Statements); try { source.Execute(this.scope); } catch (Exception ex) { ExceptionOperations ops = this.engine.GetService(); MessageBox.Show(ops.FormatException(ex)); } } }

The ScriptSource allows us to execute the Python typed into the ScriptForm.  The exception handler catches any exceptions in the Python code we write and display them nicely in a Message Box.  But there is a problem.  You can’t interact with the application.

Setting variables
By setting variables, we can enable script to interact with the application while controlling what aspects we expose to script.  In this example, let’s expose the TextBoxes.  To do that, we need to add them into the scope we created.  Add these lines to the CreateScriptRuntime method:

scope.SetVariable(this.TopTextBox.Name, this.TopTextBox); scope.SetVariable(this.MiddleTextBox.Name, this.MiddleTextBox); scope.SetVariable(this.BottomTextBox.Name, this.BottomTextBox);

Adding References
We might also want to add references to some .NET assemblies -  so that the script can take advantage of existing functions or to be able to interact with an object model, for example.  Let’s say we wanted to add a reference to mscorlib, in which case we’d add the following line to the CreateScriptRuntime method:

this.engine.Runtime.LoadAssembly(typeof(string).Assembly);

Getting output
At this point, we can write and execute Python that interacts with the application.  The final step is to hook our application up to receive output from the Python code so that, for example, we can see the result of print statements.  To do this we need to redirect the output of the script runtime.  The simplest thing to do is to redirect it to the Console:

this.engine.Runtime.IO.RedirectToConsole();

If we want to see the output in our application, we’ll also need to redirect the Console.  We can redirect the Console to any TextWriter.  It’d be good to see the output in the OutputTextBox.  Firstly, let’s create a class that subclasses TextWriter and writes out to a TextBox:

using System; using System.Text; using System.IO; using System.Windows.Forms; namespace EmbedScript { public class TextBoxWriter : TextWriter { private readonly Encoding encoding; private readonly TextBox textBox; public TextBoxWriter(TextBox textBox) : this(textBox, Encoding.UTF8) { } public TextBoxWriter(TextBox textBox, Encoding encoding) { this.textBox = textBox; this.encoding = encoding; } public override void WriteLine(string value) { this.textBox.AppendText(value); } public override void Write(char value) { this.textBox.AppendText(value.ToString()); } public override void Write(string value) { this.textBox.AppendText(value); } public override void Write(char[] buffer, int index, int count) { this.textBox.AppendText(new string(buffer)); } public override Encoding Encoding { get { return this.encoding; } } } }

With that class, we can now redirect the Console to the OutputTextBox:

Console.SetOut(new TextBoxWriter(this.OutputTextBox));

Running the application
With all that done, here’s one simple line of Python that will exercise all the code we’ve written:

print “Top is %s, Middle is %s, Bottom is %s” % (TopTextBox.Text, MiddleTextBox.Text, BottomTextBox.Text)

This code will extract values from the TextBoxes and print their values (which given our redirection will end up on the main form.)  Here’s the result of running it:

 Scriptable2

As you’d expect, if we click the Up Button a couple of times and run the script again we get this:

 Scriptable3

Conclusion
It’s simple to add powerful scripting support to your applications, while maintaining control over the scripting runtime means that you can restrict what any user entered code can do.  I chose to show this example in IronPython, but the principles for doing this with Ironruby are identical (of course you’ll need a reference to IronRuby.)  In fact, you could offer users the choice of languages.










			

Written by remark

June 10, 2009 at 6:01 pm

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: