A while back, I blogged some examples on how you can use .NET types within your IronRuby (IRuby) applications.  So after playing around with it a bit, I wanted to do something pretty basic, inheritance.  Here's what I tried:

# Basic inheritance from a dynamic type
# to a static type.
class MyForm < System::Windows::Forms::Form

end

When you try running this through the interactive console, you get this error:

System.InvalidOperationException: superclass must be a Class (DynamicType given)

Nice and descriptive, huh?  Well, essentially this means that your superclass needs to be a type that IRuby can understand, that is a class-type of DynamicType.  While looking around at the source of the Ruby project, I found my solution under the Builtins folder.  What are built-ins?  Built-ins are definitions (mappings) from Ruby types to .NET types.  For example, in Ruby string are mutable while in .NET strings are immutable.  The way they get around is issue is by creating a class called MutableString and "adding it" to the IRuby execution context (aka, what makes the magic happen).  They (John Lam and the DLR team) have defined other Ruby class extensions for integers, floats, threads, reg ex, etc.  Following on their lead, I created a class extension for my custom classes.  Here's how it went down.

1 - Create A WinForms Project

Create a simple project entitled, MyWindowsControls.  This is just a plain-vanilla WinForms control assembly that will host your custom WinForm class.  Create a class called MyForm and make it look like this (or something similar to it):

Custom WinForm class

Here's what the code-behind for the form looks like (as you can see, it's quite complex):

namespace MyWindowsControls
{
    // Plain old WindowsForm
    public partial class MyForm : Form
    {
        public MyForm()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            MessageBox.Show("Nice IronRuby hack! ;-)");
        }
    }
}

Compile and check for any errors.

2 - Create Your Ruby Extension Project

Alright, this is where the code gets tricky (somewhat).  Create a plain Class Library project and call it IronRubyExtensions.  Add references to the Microsoft.Scripting and Ruby assemblies that are part of the IRuby release.  Also, add are reference to the MyWindowsControls project.

IronRubyExtensions project references

You will now need to add the MyFormOps static class to the project.  This class will host the method mappings the IRuby runtime will call during execution.  The class should look like this:

using System.Windows.Forms;
using MyWindowsControls;
using Ruby;
using Ruby.Runtime;

namespace IronRubyExtensions
{
    // Wrapper class for the actions on the custom windows form.
    [RubyExtensionClass("TypedForm", typeof(MyForm), Inherits = typeof(object))]
    public static class MyFormOps
    {
        // Define the constructor for the class, aka a def for .new
        [RubyConstructor]
        public static MyForm Create()
        {
            return new MyForm();
        }

        // Define the .show method for the class
        [RubyMethod("show", RubyMethodAttributes.PublicInstance)]
        public static MyForm Show(MyForm cls)
        {
            if (cls != null)
            {
                Application.Run(cls);
            }

            return cls;
        }
    }
}

One important thing to note here is that the mapping actually takes place by using the RubyExtensionClass, RubyConstructor and RubyMethod attributes that are part of the Ruby namespace.  Here's a brief description of what each of them do:

  • RubyExtensionClass - Defines the Ruby dynamic type to create based on a static .NET type.  It allows you to name the dynamic type, specify the type it extends and the type it inherits.
  • RubyConstructor - Defines the .new method for the dynamic type.  When the execution context encounters the .new call, it will actually call this method to create the type and return it.
  • RubyMethod - Defines the method that is called from within your Ruby code.  It allows you to define the name and visibility for the method during execution.

Now, add a new class called CustomLibraryInitializer to the project.  Have this class inherit the abstract Ruby.Builtins.LibraryInitializer class and overload the LoadModules method.  The code should now look like this:

using System;
using Microsoft.Scripting;
using MyWindowsControls;
using Ruby.Builtins;
using Ruby.Runtime;

namespace IronRubyExtensions
{
    // Defines the custom initializer the ruby exec context will use for
    // the extension classes.
    public class CustomLibraryInitializer : LibraryInitializer
    {
        protected override void LoadModules()
        {
            RubyClass formSuper = this.Context.AsClass(typeof(System.Windows.Forms.Form));

            RegisterClass("TypedForm", typeof(MyForm), new Action<RubyModule>(LoadTypedForm), formSuper, RubyModule.EmptyArray,
                          new Delegate[]
                              {
                                new Function<MyForm>(MyFormOps.Create)
                              });
        }

        // Performs the actual dirty work of wiring the ruby method name with the
        // typed .NET one.
        private void LoadTypedForm(Ruby.Builtins.RubyModule/*!*/ module)
        {
            module.DefineInstanceMethod("show", RubyMethodVisibility.Public, new Delegate[]
                {
                    new Function<MyForm,MyForm>(MyFormOps.Show)
                });
        }
    }
}

As you can see within the CustomLibraryInitializer class, there is some reference to the RubyClass and RubyModule classes.  These classes represent the structure needed for execution.  Assemblies contain modules and modules contain classes (types).  Also, I'm calling the RegisterClass method to add my new extension class to the IRuby execution context.  Notice that in the method call I'm passing an array of Delegate containing a Function delegate that links to the MyFormOps.Create method.  The reason why this is done is to map the .new call to the corresponding RubyConstructor decorated method.  The LoadTypedForm method is in charge of adding any additional method definitions to the extension class.

Next, under your AssemblyInfo.cs file, add the assembly level RubyLibrary attribute and specify the library extension type so it can be picked up by the Ruby execution context (more on this later).  The code should look like this:

using System.Reflection;
using System.Runtime.InteropServices;
using IronRubyExtensions;
using Ruby;

// This is needed to register the ruby library extension
[assembly: RubyLibrary(typeof(CustomLibraryInitializer))]

Compile the assembly and you're all set to go!

3 - Executing Your Ruby Extension Project

Included in the downloads for this post, there is a new version of the Ruby.dll assembly that will dynamically load any LibraryInitializer sub-class from an assembly that contains the RubyLibrary attribute(s).  The out-of-the-box version of Ruby.dll does include this feature under the RubyExecutionContext.InitializeCaches method.  Here's the code (simple but effective) that I added to load the assemblies:

        // New method body definition
        internal void InitializeCaches()
        {
            // HACK: THIS IS DEMO CODE
            try
            {
                List<LibraryInitializer> initializers = LoadLibraryInitializers();

                Utils.Assert.NotNullItems(initializers);

                foreach (LibraryInitializer initializer in initializers)
                {
                    initializer.LoadModules(this);
                }
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ex.Message);
            }

            Debug.Assert(_classClass != null && _nilClass != null && _exceptionClass != null &&
                _runtimeErrorClass != null && _standardErrorClass != null);
        }

        internal List<LibraryInitializer> LoadLibraryInitializers()
        {
            List<LibraryInitializer> initializers = new List<LibraryInitializer>();
            // Load the default built-in initializer
            initializers.Add(new BuiltinsInitializer());

            AppDomain current = AppDomain.CurrentDomain;
            string baseDirectory = current.BaseDirectory;

            try
            {
                string[] assemblyFiles = Directory.GetFiles(baseDirectory, "*.dll"); ;

                if (assemblyFiles != null)
                {
                    foreach (string file in assemblyFiles)
                    {
                        try
                        {
                            Assembly asm = Assembly.LoadFile(file);

                            RubyLibraryAttribute[] attribs = FindInitializers(asm);

                            if(attribs != null && attribs.Length != 0)
                            {
                                foreach (RubyLibraryAttribute attrib in attribs)
                                {
                                    LibraryInitializer initializer = LoadInitializer(attrib);
                                    initializers.Add(initializer);
                                }
                                
                            }
                        }
                        catch
                        {
                            continue;
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ex);
            }

            return initializers;
        }

        internal RubyLibraryAttribute[] FindInitializers(Assembly asm)
        {
            Utils.Assert.NotNull(asm);
            return asm.GetCustomAttributes(typeof (RubyLibraryAttribute), false) as RubyLibraryAttribute[];
        }

        internal LibraryInitializer LoadInitializer(RubyLibraryAttribute attrib)
        {
            Utils.Assert.NotNull(attrib);
            return Activator.CreateInstance(attrib.Initializer) as LibraryInitializer;
        }

See, what did I tell you? Pretty simple? ;-)  The IRuby file, RubyExtensions.rb, that contains the inherited class can now look like this:

require 'mscorlib'

Console = System::Console;

# a IronRuby class inheriting a Typed class (C#)
# via extension classes
class InheritedForm < TypedForm
end

The IRuby that gets executed is the following:

require 'RubyExtensions'

my_form = InheritedForm.new
my_form.show

Console.Write 'PRESS <ENTER> to exit...'
Console.ReadLine

To make the magic happen do the following:

  1. Run Build.cmd to build the source files to their correct Bin folder (note that Release is the default)
  2. Run Run.cmd to launch the sample ExtensionTest.rb.  The output will be as follows:

extension_inherited_form

Viola!  A hacked IronRuby inheritance demo!  Let me know what you guys think.

Source: IronRubyExtensions.zip