Ruby on MEF: Imports and Exports

Well, the "Ruby Parts" implementation is slowly taking shape. If you haven't read the first article you probably should before you read on.

Exports

An export is an object that a part will hand to the outside world to fulfil a contract. Sometimes, this is the part itself. Other times, the part will expose a property or a method as an export.

MEF requires us to publish the information about what exports are available (our ComposablePartDefinition) before any object that provides the export has been created.

We can achieve this in Ruby because class definitions are executed. The export* methods can run arbitrary code and have access to the class definition that they're running within.

Without any syntactic sugar, a part definition that exports the 'answer' contract looks like:

image 

Other parts that import the 'answer' contract will get the integer value 42.

Our technique is:

  1. Keep a global table of 'part definitions' that can be updated as a Ruby script executes
  2. When a static export* method executes, create a function that can retrieve the export from a part instance (the 'export accessor')
  3. Store the export accessor along with the part's class in the table of part definitions

Each part definition ends up mapping a Ruby class to a table of contract name/function pairs.

When an export is requested from our part, the value returned is the result of executing the function that we stored for that export in the part definition.

The function can return either the object itself, an attribute, or a Proc object representing a method, which seems like a good generic foundation to build our different kinds of exports on top of.

Because self or a Proc can be returned from a property, we'll implement property exports first.

In the example, to get things started the export accessor function is explicitly specified in a block argument to the export_attr method.

The export_attr Method

This method is defined as a class method on PartDefinition.

image

So, while the MyPart class is being defined in the Ruby runtime, export_attr will create an export accessor that runs the provided block in the context of the part instance.

RubyCatalog

The meat of the add_export method creates or finds a RubyPartDefinition instance and adds an export definition to it.

image

The mysterious MefPartsCollection variable that appears in the above code is a named instance provided by the MEF RubyCatalog to the IronRuby runtime.

 public class RubyCatalog : ComposablePartCatalog
{
    readonly ScriptRuntime _runtime;
    readonly ScriptEngine _ruby;
    readonly IQueryable<ComposablePartDefinition> _parts;

    public RubyCatalog(string script)
    {
        if (script == null)
            throw new ArgumentNullException("script");

        var rubySetup = new LanguageSetup(typeof(RubyContext).AssemblyQualifiedName);
        rubySetup.FileExtensions.Add("rb");
        rubySetup.Names.Add("IronRuby");

        var setup = new ScriptRuntimeSetup();
        setup.LanguageSetups.Add(rubySetup);

        _runtime = new ScriptRuntime(setup);
        _runtime.LoadAssembly(typeof(RubyPartDefinition).Assembly);
        _runtime.LoadAssembly(typeof(ExportDefinition).Assembly);
        _runtime.LoadAssembly(typeof(IEnumerable<int>).Assembly);

        _ruby = _runtime.GetEngine("IronRuby");

        var parts = new Hashtable();
        _runtime.Globals.SetVariable("MefPartsCollection", parts);
        _ruby.Execute(Script.PartDefinition);
        _ruby.Execute(script);

        _parts = parts.Values.Cast<ComposablePartDefinition>().AsQueryable();
    }

    public override IQueryable<ComposablePartDefinition> Parts
    {
        get { return _parts; }
    }
}

You can see how the "MefPartsCollection" variable is being set to a Hashtable before the script executes, and afterwards the values are retrieved as a list of ComposablePartDefinition instances.

The DLR setup in the catalog is a bit tentative. We'll refactor and extend it to be a little more user-friendly in the future.

(This catalog design assumes a single catalog per IronRuby runtime. This probably isn't going to be the best outcome for pure-Ruby applications, but for C# applications hosting Ruby parts this will work for now.)

Ruby Programming Model

The items in the MefPartsCollection are instances of the RubyPartDefinition class, indexed by Ruby part class.

RubyPartDefinition collaborates with RubyPart, RubyExportDefinition and RubyImportDefinition to create what MEF terms a 'programming model'.

I've implemented the programming model classes in C#, but they should be able to be implemented equally well in Ruby. Porting them might be an interesting exercise for the reader :)

Imports

Imports are defined using a similar technique. You can see how they work in the example code.

The Unit Tests

The solution you'll find attached to this article doesn't include an application - just a meagre selection of unit tests for the catalog.

The most interesting is this one:

 [TestMethod]
public void ImportsValues()
{
    var script = @"
        class MyPart < PartDefinition
            import 'input_value' do |input|
                @input_value = input
            end

            export_attr 'output_value' do
                @input_value.get_exported_object
            end
        end
    ";

    ComposablePart part = CreateMyPart(script).CreatePart();
    Assert.AreEqual(1, part.ImportDefinitions.Count());

    var inputImport = part.ImportDefinitions.Single(
        i => ((ContractBasedImportDefinition)i).ContractName == "input_value");
    Assert.AreEqual(1, part.ExportDefinitions.Count());

    var outputExport = part.ExportDefinitions.Single();
    var testValue = "Hello, world";
    part.SetImport(inputImport, new[] { new Export(
        new ExportDefinition("input_value", null),
        () => testValue)});
    var output = part.GetExportedObject(outputExport);
    Assert.AreEqual(testValue, output);
}

Here you can see a complete roundtrip from import to export. Not groundbreaking, but I think this puts us on the right track.

Coming up...

We'll implement the basics of metadata, tidy up the syntax (I'm considering Dave's post a challenge ;)), and start to look at a hybrid application. Stay tuned!

Files

MefDlr-2008-12-13.zip