Managed Extensibility FrameworkからIronRubyを簡単に使う

MEF記事の連続投下第三弾。あまり一般受けしないネタを連投です。(^^;

IronRubyをアプリケーションにホスティングは難しくありませんが、dynamicを使うなど特別扱いが必要だったりするし、何よりもVS上でコードを書いてビルドという流れの中に「rubyでコードを書いてC#ホスティングコードを書く」が入ると開発のテンポが悪くなります。私的にはrubyでコードを書いたら勝手に取り込んでくれるくらいでないとIronRubyホスティングしようとは思わないです。

ということで今回のミッション。

C#で作成したインタフェースをrubyで実装してC#のクラスにインポートさせたい。

rubyで簡易的に実装して、あとでC#のコードで差し替えたりとかそんな使い方を想定しています。

必要なもの

  • RubyCodeExportProvider : rubyで書いたクラスに対するエクスポートプロバイダ。

これだけでOK.前回のAOPに比べたら実装は全然楽ですね。


RubyCodeExportProvider.cs

using System.Collections.Generic;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Primitives;
using System.IO;
using System.Linq;
using IronRuby;
using Microsoft.Scripting.Hosting;

namespace RubyCodeSample
{
    /// <summary>
    /// Rubyクラスのエクスポートプロバイダ
    /// </summary>
    public class RubyCodeExportProvider : ExportProvider
    {
        private const string Ext = ".rb";
        private readonly ScriptRuntime _runtime;

        /// <summary>
        /// コンストラクタ。
        /// </summary>
        /// <param name="path">rubyのソースファイルが置いてあるパス。</param>
        public RubyCodeExportProvider(string path)
        {
            _path = path;
            _runtime = Ruby.CreateRuntime(); 
        }

        private readonly string _path;
        public string Path { get { return _path; } }

        protected override IEnumerable<Export> GetExportsCore(ImportDefinition definition, AtomicComposition atomicComposition)
        {
            if (!(definition is ContractBasedImportDefinition))
                return Enumerable.Empty<Export>();

            var importDef = definition as ContractBasedImportDefinition;
            var path = System.IO.Path.Combine(Path, importDef.RequiredTypeIdentity + Ext);
            if (!File.Exists(path))
                return Enumerable.Empty<Export>();

            // ネーミングルールとしてIFooの実装クラス名はFooとする
            var className = importDef.RequiredTypeIdentity.Split('.').Last().Substring(1);

            var def = new ExportDefinition(definition.ContractName, new Dictionary<string, object>());
            return new[] {new Export(def, () =>
                                                    {
                                                        _runtime.ExecuteFile(path);
                                                        var theClass = _runtime.Globals.GetVariable(className);
                                                        return _runtime.Operations.CreateInstance(theClass);
                                                    })};   
        }
    }
}

インタフェースとRuby側のソースファイル(および実装クラス)の関連付けはネーミングルールで行っています。設定より規約(CoC:Convention over Configuration)ってことで(と茶を濁してみる)。

これでrubyで書いたクラスをC#から呼び出し放題です。では、実験君。

まずは、rubyで実装させるインタフェースを用意します。
ICalc.cs

using System.ComponentModel.Composition;

namespace RubyCodeSample
{
    [InheritedExport]
    public interface ICalc
    {
        int Add(int x, int y);
        int Sub(int x, int y);
    }
}

Export属性はクラスにしかつけられないので代わりにInheritedExport属性を使っています。

続いて、rubyによる実装。
RubyCodeSample.ICalc.rb

require 'RubyCodeSample'

class Calc
  include RubyCodeSample::ICalc
  def add(x, y)
    puts('from IronRuby')
    x + y
  end

  def sub(x, y)
    puts('from IronRuby')
    x - y
  end
end

単純なクラスですね。このソースファイルはとりあえずscripts\rubyディレクトリにでも置いておきましょう。

では、MEFを使ってrubyのクラスをインポートして使ってみましょう。

using System;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;

namespace RubyCodeSample
{
    [Export]
    public class Program
    {
        // MEFでインポートされる
        [Import]
        public ICalc Calc { get; set; }

        static void Main()
        {
            // Rubyクラスのエクスポートプロバイダ
            // Rubyで書いたクラスをインポート可能になる
            var rubyEp = new RubyCodeExportProvider(@"scripts\ruby");
            var container = new CompositionContainer(rubyEp);
            var program = new Program();
            container.ComposeParts(program);
            program.Run();
        }

        /// <summary>
        /// テスト。
        /// </summary>
        public void Run()
        {
            // Calcの実装はRuby
            Console.WriteLine(Calc.Add(10, 20));
            Console.WriteLine(Calc.Sub(10, 20));
        }
    }
}

/* 結果
 from IronRuby
 30
 from IronRuby
 -10
 */

rubyで実装していることを意識せずに開発できるのが良いところですね。

ところで、今回はネーミングルールで対応しましたが、カスタム属性を使ってrubyのソースファイルとクラスを指定したり、DirectoryCatalogのように.rbファイルがあるディレクトリを指定してそこにあるファイルをすべて読み込むといった実装もありです。