From C# Ruby and Back Again, via IronRuby

Note: This is the first of two articles on integrating C# and scripting languages. You can read the second part (on Javascript) here.

I recently created an engine (I know, I know) to create text-based Interactive Fiction games in C#. My requirement was simple: create a core engine with some core functionality (rooms, commands, and objects) in .NET, and allow end-users to write their games in Ruby. In the process, I picked up IronRuby as the vehicle of interaction.

Let’s start with my requirements: I should be able to write core functionality classes in C#, instantiate and extend them in IronRuby, and return them back to C# for consumption by the game engine. I also need strong typing of objects (despite the dynamic nature of scripting languages), and the ability to create delegates or functions inside my scripting language (and return those to C#, too).

You can view the source code on GitHub. This post summarizes my implementation, and the challenges I ran though.

RubyRunner Class

I created a core “RubyRunner” class which encapsulates all the Ruby-centric invocation. It contains a simple constructor, and a method public T Execute(string script, IDictionary parameters).

The first parameter (the script) is the easiest part. The second parameter contains any variables from C# that I want to expose to Ruby (this makes up my “API” which users write against).

One issue I ran into is a little error that can't convert Meltdown.Core.Area to Meltdown.Core.Area.

Huh?

It’s a strange kink in IronRuby; I can include my assembly to instantiate objects from it, but they don’t have the same type as the CLR equivalents (despite being loaded from the same assembly). The solution was to call load_assembly 'Meltdown.Core' in every single Ruby code. I encapsulated this into a Common.rb script which is auto-included in every invocation of Execute.

Here’s how that looks, as of writing:


public RubyRunner()
{
if (File.Exists(@"Ruby\Common.rb"))
{
this.commonHeaderScript = File.ReadAllText(@"Ruby\Common.rb");
}
}

public T Execute(string script, IDictionary parameters)
{
var scope = engine.Runtime.CreateScope();

foreach (var kvp in parameters)
{
scope.SetVariable(kvp.Key, kvp.Value);
}

var finalScript = string.Format("{0}\n{1}", this.commonHeaderScript, script);
var toReturn = engine.Execute(finalScript, scope);

if (toReturn is T)
{
return (T)toReturn;
}
else
{
throw new ArgumentException("Expected an instance of " + typeof(T).FullName + " but got " + toReturn.GetType().FullName + " instead.");
}
}

Converting IronRuby Arrays to C# Lists

The second major hurdle is that IronRuby returns a RubyArray instance instead of an enumeration if I use typical Ruby list syntax, like this: $x = ['a', 17, 2.0 / 3].

To get around this, I created a ScriptHelper class. This class contains two methods: IsArray and ToList. In both methods, I rely on IronRuby-specific implementation, and since the object input is dynamic, I can just call Ruby methods and get back what I want. Here’s how that looks:


public static List ToList(dynamic source)
{
var found = new List();
for (int i = 0; i < source.Count; i++) { object next = source[i]; found.Add(GetAsType(next));
}

return found;
}

public static bool IsArray(dynamic source) {
return source.GetType().Name == "RubyArray";
}

This allows me to write “Ruby-like” code without worrying about types.

Returning Delegates/Functions

The last hurdle was related to functions; given a delegate or function type (like Action), how can I return an object which I can type to a delegate type?

The answer was surprisingly easy; I just need to return an instance of Proc from my Ruby code. Something like:


Proc.new {
puts "Action invoked. Yay!"
}

Conclusion

Given my requirements, I achieved them, and have code that allows me to write very “Ruby-like” code. That’s great; I can leverage C# to take care of some of the leaky abstractions, and everything works.

I suggest you download and play with the full source code from GitHub. You can look at the ScriptRunner.Tests project to see working Ruby code coming back to C#.

About Ashiq Alibhai, PMP

Ashiq has been coding C# since 2005. A desktop, web, and RIA application developer, he's touched ASP.NET MVC, ActiveRecord, Silverlight, NUnit, and all kinds of exciting .NET technologies. He started C# City in order to accelerate his .NET learning.
This entry was posted in Libraries, Wndows Forms and tagged , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *