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:
Other parts that import the 'answer' contract will get the integer value 42.
Our technique is:
- Keep a global table of 'part definitions' that can be updated as a Ruby script executes
- When a static export* method executes, create a function that can retrieve the export from a part instance (the 'export accessor')
- 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.
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.
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
Comments
Anonymous
December 22, 2008
Recently we were fortunate to have Nicholas Blumhardt join the MEF team. Before joining Microsoft NickAnonymous
December 22, 2008
Recently we were fortunate to have Nicholas Blumhardt join the MEF team. Before joining Microsoft NickAnonymous
December 22, 2008
Recently we were fortunate to have Nicholas Blumhardt join the MEF team. Before joining Microsoft, NickAnonymous
December 22, 2008
Since the last installment in this little series, I've started to consider how Ruby/C# hybrid MEF applications