MAG画像ローダ

唐突にPC98時代によく使われていたMAG画像形式ファイルを読み込むプログラムをC#に移植してみました。2,3個の画像でしか試してないのでバグがあるかも。

// 謝辞
// MITH(T.Saito)氏のWAB−S用マルチグラフィックローダ WMLのソースを参考にさせていただきました。

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;

/// <summary>
/// MAG形式画像ローダ。
/// </summary>
public static class MagFile
{
    private static readonly sbyte[] FlagX = {0, -1, -2, -4, 0, -1, 0, -1, -2, 0, -1, -2, 0, -1, -2, 0};
    private static readonly sbyte[] FlagY = {0, 0, 0, 0, -1, -1, -2, -2, -2, -4, -4, -4, -8, -8, -8, 0};

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    private unsafe struct MagFileHeader
    {
        private fixed byte mgHeader [8];
        private fixed byte mgMachine [4];
        private fixed byte mgUser [8];
    }

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    private struct MagHeader
    {
        private readonly byte mgHeader;
        private readonly byte mgMachine;
        private readonly byte mgSystem;
        public readonly byte mgScreenmode;
        public readonly ushort mgStartX;
        public readonly ushort mgStartY;
        public readonly ushort mgEndX;
        public readonly ushort mgEndY;
        public readonly uint mgFlagA_offset;
        public readonly uint mgFlagB_offset;
        public readonly uint mgFlagBSize;
        public readonly uint mgPixelD_offst;
        public readonly uint mgPixelDSize;
    }

    private static unsafe void ReadMagFileHeader(Stream stream, out MagFileHeader magFileHeader)
    {
        fixed (void* p = &magFileHeader)
        {
            var buf = new byte[sizeof (MagFileHeader)];
            stream.Read(buf, 0, buf.Length);
            Marshal.Copy(buf, 0, (IntPtr) p, buf.Length);
        }
    }

    private static unsafe void ReadMagHeader(Stream stream, out MagHeader magHeader)
    {
        fixed (void* p = &magHeader)
        {
            var buf = new byte[sizeof (MagHeader)];
            stream.Read(buf, 0, buf.Length);
            Marshal.Copy(buf, 0, (IntPtr) p, buf.Length);
        }
    }

    private static string ReadComment(Stream stream)
    {
        var bytes = new List<byte>();
        var data = stream.ReadByte();
        while (data != 0x1a)
        {
            bytes.Add((byte) data);
            data = stream.ReadByte();
        }

        return Encoding.GetEncoding(932).GetString(bytes.ToArray());
    }

    private static PixelFormat GetPixelFormat(MagHeader magHeader)
    {
        return (magHeader.mgScreenmode & 0x80) == 0x80
                   ? PixelFormat.Format8bppIndexed
                   : PixelFormat.Format4bppIndexed;
    }

    private static Bitmap CreateBitmap(MagHeader magHeader)
    {
        return new Bitmap(magHeader.mgEndX + 1, magHeader.mgEndY + 1, GetPixelFormat(magHeader));
    }

    private static IEnumerable<Color> ReadPalette(Stream stream, MagHeader magHeader)
    {
        var n = (magHeader.mgScreenmode & 0x80) == 0x80 ? 256 : 16;
        var palette = new byte[n*3];
        stream.Read(palette, 0, palette.Length);
        for (var i = 0; i < n; ++i)
        {
            var g = palette[i*3 + 0];
            var r = palette[i*3 + 1];
            var b = palette[i*3 + 2];
            yield return Color.FromArgb(0xff, r, g, b);
        }
    }

    private static void SetPalette(Bitmap bitmap, IEnumerable<Color> colors)
    {
        var i = 0;
        var colorPalette = bitmap.Palette;
        foreach (var color in colors)
            colorPalette.Entries[i++] = color;

        bitmap.Palette = colorPalette;
    }

    private static int GetStartX(MagHeader magHeader)
    {
        var n = (magHeader.mgScreenmode & 0x80) == 0x80 ? 256 : 16;
        int startx;
        if (n == 256)
            startx = magHeader.mgStartX/4*4;
        else
            startx = magHeader.mgStartX/8*8;
        return startx;
    }

    private static int GetPixelSizeX(MagHeader magHeader)
    {
        var n = (magHeader.mgScreenmode & 0x80) == 0x80 ? 256 : 16;
        int xsize;
        int startx;
        if (n == 256)
        {
            startx = magHeader.mgStartX/4*4;
            xsize = ((magHeader.mgEndX + 4)/4*4 - startx);
        }
        else
        {
            startx = magHeader.mgStartX/8*8;
            xsize = ((magHeader.mgEndX + 8)/8*8 - startx + 1)/2;
        }

        return xsize;
    }

    private static int GetFlagASize(MagHeader magHeader)
    {
        int xsize = GetPixelSizeX(magHeader);
        var x = xsize/4;
        var ysize = magHeader.mgEndY - magHeader.mgStartY + 1;
        return (x*ysize + 7)/8;
    }

    private static void ReadFlagA(Stream stream, MagHeader magHeader, long startPos, byte[] flagA)
    {
        stream.Seek(startPos + magHeader.mgFlagA_offset, SeekOrigin.Begin);
        stream.Read(flagA, 0, flagA.Length);
    }

    private static void ReadFlagB(Stream stream, ref uint left, ref uint xpoint, long startPos, byte[] flagB)
    {
        stream.Seek(startPos + xpoint, SeekOrigin.Begin);
        if (left > 0x8000)
        {
            stream.Read(flagB, 0, 0x8000);
            xpoint += 0x8000;
            left -= 0x8000;
        }
        else
        {
            stream.Read(flagB, 0, 0x8000);
            left = 0;
        }
    }

    /// <summary>
    /// ピクセル単位の読み込み。
    /// <remarks>MAGフォーマットでは2byte単位をピクセルと呼ぶ。1ピクセルには16色の場合4ドット、256色なら2ドットがパックされている。</remarks>
    /// </summary>
    /// <param name="stream"></param>
    /// <param name="left"></param>
    /// <param name="xpoint"></param>
    /// <param name="startPos"></param>
    /// <param name="pixel"></param>
    private static void ReadMagPixel(Stream stream, ref uint left, ref uint xpoint, long startPos, ushort[] pixel)
    {
        stream.Seek(startPos + xpoint, SeekOrigin.Begin);
        var tmp = new byte[0x8000];
        if (left > 0x8000)
        {
            stream.Read(tmp, 0, 0x8000);
            xpoint += 0x8000;
            left -= 0x8000;
        }
        else
        {
            stream.Read(tmp, 0, 0x8000);
            left = 0;
        }

        unsafe
        {
            fixed (void* p = &pixel[0])
            {
                Marshal.Copy(tmp, 0, (IntPtr) p, tmp.Length);
            }
        }
    }

    /// <summary>
    /// MAG画像のロード。
    /// </summary>
    /// <param name="stream">MAG画像のストリーム。</param>
    /// <returns>ビットマップ。</returns>
    public static Bitmap Load(Stream stream)
    {
        MagFileHeader magFileHeader;
        ReadMagFileHeader(stream, out magFileHeader);

        var comment = ReadComment(stream);

        // コメントを読み飛ばした位置がMAGヘッダの開始位置。
        var startPos = stream.Position;

        // MAGヘッダ読み込み。
        MagHeader magHeader;
        ReadMagHeader(stream, out magHeader);

        // 出力用ビットマップの作成。
        var bitmap = CreateBitmap(magHeader);

        // とりあえずタグにコメントを入れる。
        bitmap.Tag = comment;

        // パレットの設定。
        var colors = ReadPalette(stream, magHeader);
        SetPalette(bitmap, colors);

        // フラグAを読み込む。
        var flagA = new byte[GetFlagASize(magHeader)];
        ReadFlagA(stream, magHeader, startPos, flagA);

        // フラグBを読み込む。
        var flagB = new byte[0x8000];
        var leftFlagB = magHeader.mgFlagBSize;
        var offsetFlagB = magHeader.mgFlagB_offset;
        ReadFlagB(stream, ref leftFlagB, ref offsetFlagB, startPos, flagB);

        // ピクセルを読み込む。
        var pixel = new ushort[0x8000/sizeof (ushort)];
        var leftPixel = magHeader.mgPixelDSize;
        var offsetPixel = magHeader.mgPixelD_offst;
        ReadMagPixel(stream, ref leftPixel, ref offsetPixel, startPos, pixel);

        // フラグA,Bからピクセルデータを復元。
        unsafe
        {
            var pixelSizeX = GetPixelSizeX(magHeader);
            var flagBmap = new byte[pixelSizeX];
            var line = new ushort[pixelSizeX*8];

            fixed (byte* p1 = &flagA[0])
            fixed (ushort* p2 = &line[0])
            fixed (byte* p3 = &flagBmap[0])
            {
                var startx = GetStartX(magHeader);
                var pFlagA = p1;
                var pline = p2;

                var p = 0;
                var w = 0;
                var ibit = 0x80;
                var curFlagA = *pFlagA++;
                var j = 15;
                for (int y = magHeader.mgStartY; y <= magHeader.mgEndY; ++y)
                {
                    var flag = new int[16];

                    ++j;
                    for (var i = 0; i < flag.Length; ++i)
                        flag[i] = FlagX[i]*2 + ((FlagY[i] + j) & 0xf)*pixelSizeX;

                    if (j == 16)
                        j = 0;

                    var pFlagBmap = p3;
                    var ftmp = flag[0];

                    var cline = pline + (ftmp/2);

                    for (var x = 0; x < pixelSizeX;)
                    {
                        if ((curFlagA & ibit) != 0)
                        {
                            if (p >= 0x8000)
                            {
                                ReadFlagB(stream, ref leftFlagB, ref offsetFlagB, startPos, flagB);
                                p = 0;
                            }

                            *pFlagBmap ^= flagB[p++];
                        }

                        ibit >>= 1;
                        if (ibit == 0)
                        {
                            ibit = 0x80;
                            curFlagA = *pFlagA++;
                        }

                        var fr = *pFlagBmap;
                        pFlagBmap++;
                        if ((fr >> 4) == 0)
                        {
                            if (w >= 0x8000)
                            {
                                ReadMagPixel(stream, ref leftPixel, ref offsetPixel, startPos, pixel);
                                w = 0;
                            }

                            line[(ftmp + x)/2] = pixel[w/2];
                            w += 2;
                        }
                        else
                        {
                            line[(ftmp + x)/2] = line[(flag[fr >> 4] + x)/2];
                        }

                        x += 2;
                        if ((fr & 0x0f) == 0)
                        {
                            if (w >= 0x8000)
                            {
                                ReadMagPixel(stream, ref leftPixel, ref offsetPixel, startPos, pixel);
                                w = 0;
                            }
                            line[(ftmp + x)/2] = pixel[w/2];
                            w += 2;
                        }
                        else
                        {
                            line[(ftmp + x)/2] = line[(flag[fr & 0xf] + x)/2];
                        }
                        x += 2;
                    }

                    // 1ライン分をビットマップに書き出す。
                    var b = new byte[pixelSizeX];
                    for (var i = 0; i < pixelSizeX/2; i++)
                    {
                        var c = cline[i];
                        b[i*2] = (byte) (c & 0xff);
                        b[i*2 + 1] = (byte) ((c >> 8) & 0xff);
                    }

                    var dst = bitmap.LockBits(new Rectangle(magHeader.mgStartX, y, bitmap.Width, 1),
                                              ImageLockMode.WriteOnly, bitmap.PixelFormat);
                    Marshal.Copy(b, magHeader.mgStartX - startx, dst.Scan0,
                                 bitmap.PixelFormat == PixelFormat.Format8bppIndexed ? bitmap.Width : bitmap.Width/2);
                    bitmap.UnlockBits(dst);
                }
            }
        }

        return bitmap;
    }

    /// <summary>
    /// MAG画像のロード。
    /// </summary>
    /// <param name="path">MAG画像ファイル名。</param>
    /// <returns>ビットマップ。</returns>
    public static Bitmap Load(string path)
    {
        using (var fl = new FileStream(path, FileMode.Open))
        {
            return Load(fl);
        }
    }
}

/* 使い方
 * 
 * var bmp = MagFile.Load(magfile);
 * 
 */

MEFでオープンジェネリック型をエクスポート

MefContribにはジェネリック用のGenericCatalogクラスがあります。これを使うとオープンジェネリック型をエクスポートすることができます。ただ、ドキュメント不足な上に、ちょっと癖がありそうです。

公開するインタフェースは、こんな感じです。

    // このインタフェースから継承したクラスはすべてエクスポートされる
    [InheritedExport]
    public interface ICalc<T>
    {
        T Add(T x, T y);
        T Div(T x, T y);
    }

色々試した限りでは、実装クラスにExport属性を付けてもうまくいかず、インタフェースにInheritedExport属性を付ける必要がありました。とりあえず、オープンジェネリック型をエクスポートする場合は、インタフェースにInheritedExport属性を付けましょう。

実装は適当に。

    // このエクスポートは発見されない。
    // ネット上のサンプルを見る限り、できそうなんだけど・・・
    // [Export(typeof(ICalc<>))]
    public class CalcImpl<T> : ICalc<T>
    {
        public T Add(T x, T y)
        {
            return (dynamic)x + y;
        }

        public T Div(T x, T y)
        {
            return (dynamic)x / y;
        }
    }

テストなのでdynamicを使ってごまかしておきます。

次にジェネリックインタフェースと実装クラスのマッピングを定義するクラスを用意します。GenericCatalogにこのクラスを渡すことで型を解決できるようになるようです。

    /// <summary>
    /// open genericの場合、実装クラスとのマッピングを指定する必要がある。
    /// </summary>
    [Export(typeof(IGenericContractRegistry))]
    public class MyGenericContractRegistry : GenericContractRegistryBase
    {
        protected override void Initialize()
        {
            Register(typeof(ICalc<>), typeof(CalcImpl<>));
        }
    }

準備ができたので、さっそく使ってみましょう。

    [Export]
    class Program
    {
        [Import]
        public ICalc<int> CalcInt32 { get; set; }
        
        [Import]
        public ICalc<double> CalcDouble { get; set; }

        public void Run()
        {
            int x = 1, y = 2;
            Console.WriteLine("{0} / {1} = {2}", x, y, CalcInt32.Div(x, y));
            Console.WriteLine("{0} / {1} = {2}", x, y, CalcDouble.Div(x, y));
        }

        static void Main(string[] args)
        {
            var catalog = new TypeCatalog(typeof (Program));
            var generic = new GenericCatalog(catalog, new MyGenericContractRegistry());
            var container = new CompositionContainer(generic);
            var program = container.GetExportedValue<Program>();

            program.Run();
        }
    }
/* 結果
1 / 2 = 0
1 / 2 = 0.5
 */

CodePlexのフォーラムやブログにあったサンプルが今のMefContribだと動かないので試行錯誤になりましたが、多分、こんな感じ。(^^;

LinFu.DynamicProxyの使い方が分からない

MefContribにCastle以外にもLinFu.DynamicProxyが含まれていたので弄ってみたのですが、どうにも使い方が分かりません。

    using LinFu.DynamicProxy;
    public class MyInterceptor : IInterceptor
    {
        public object Intercept(InvocationInfo info)
        {
            return info.TargetMethod.Invoke(info.Target, info.Arguments);
        }
    }

おおよそ、Casle.DynamicProxyと同じ使い方だと思うのですが、割り込み対象であるはずのinfo.Targetがプロキシになっているんですよね。で、Invokeするとまた、割り込みメソッドが呼び出され・・・スタックオーバーフロー。LinFuのサンプルを見るとインターセプタのコンストラクタで割り込み対象オブジェクトを渡す例が多く、InvocationInfoだけじゃ情報が足りていない気がします。

    using LinFu.DynamicProxy;
    public class MyInterceptor : IInterceptor, IWithTarget
    {
        public object Intercept(InvocationInfo info)
        {
            // info.Targetの代わりにTargetを使う
            return info.TargetMethod.Invoke(Target, info.Arguments);
        }

        public object Target { get; set; }
    }

こんな風に、IWithTargetインタフェースも実装してやって、MefContribLinFu.DynamicProxyInterceptor.csを

        public object Intercept(object value)
        {
            var interfaces = value.GetType().GetInterfaces();
            var proxyInterface = interfaces.FirstOrDefault();
            var additionalInterfaces = interfaces.Skip(1).ToArray();
            // 割り込み対象をインターセプタに設定する
            if (interceptor is IWithTarget)
                (interceptor as IWithTarget).Target = value;
            
            return Generator.CreateProxy(proxyInterface, this.interceptor, additionalInterfaces);
        }

とすると動きました。

ただ、全くテストされていないとは思えないので、私の使い方が間違っていると思うんですけどね・・・

MEFでAOP

過去に似たようなことをやった気がしますが、今回はMefContribを使った一応、正式っぽい方法で。

using System;
using System.Linq;
using System.Reflection;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Primitives;
using System.ComponentModel.Composition.Hosting;
using MefContrib;
using MefContrib.Hosting.Interception;
using MefContrib.Hosting.Interception.Castle;
using MefContrib.Hosting.Interception.Configuration;
using Castle.DynamicProxy;

namespace MefInterceptorSample
{
	public class MyInterceptor : IInterceptor
	{
		public void Intercept(IInvocation invocation)
		{
			Console.WriteLine("Call {0}.", invocation.Method.Name);
			invocation.Proceed();
			Console.WriteLine("Called {0}.", invocation.Method.Name);
		}
	}
	
	public interface ICalc
	{
		int Add(int x, int y);
		int Sub(int x, int y);
	}
	
	[Export(typeof(ICalc))]
	public class CalcImpl : ICalc
	{
		public int Add (int x, int y)
		{
			Console.WriteLine("In Add.");
			return x + y;
		}
		
		public int Sub (int x, int y)
		{
			Console.WriteLine("In Sub.");
			return x - y;
		}
	}
	
	[Export]
	public class MainClass
	{
		[Import]
		public ICalc Calc {private get;set;}
		
		public void Run()
		{
			Console.WriteLine(Calc.Add(1, 2));
			Console.WriteLine(Calc.Sub(1, 2));
		}
		
		public static void Main (string[] args)
		{
			// 割り込み対象となるクラス用カタログ
			// 割り込めないクラスがカタログに含まれていると例外が発生するのでカタログを分ける(恐らくバグ)
			var targetCatalog = new TypeCatalog(typeof(CalcImpl));
			// Castle DynamicProxyを使って割り込む
			var interceptor = new DynamicProxyInterceptor(new MyInterceptor());
			// 第2引数は割込条件。ここでは無条件に割り込ませるため、常にtrue。
			var criteria = new PredicateInterceptionCriteria(interceptor, _=>true);
			// 割り込み用カタログ。 
			var config = new InterceptionConfiguration().AddInterceptionCriteria(criteria);
			var interceptCatalog = new InterceptingCatalog(targetCatalog, config);
			
			// 割り込み対象外のクラス用カタログ。
			var otherCatalog = new TypeCatalog(typeof(MainClass));
			
			var container = new CompositionContainer(new AggregateCatalog(interceptCatalog, otherCatalog));
			var main = container.GetExportedValue<MainClass>();
			main.Run();
		}
	}
}
/* 結果
Call Add.
In Add.
Called Add.
3
Call Sub.
In Sub.
Called Sub.
-1
*/

InterceptingCatalogを使った割り込みですが、これが最終版ではなく、アイディア募集中みたいなことをどこかのフォーラムで見た気がします。

あと、コメントにも書きましたが、Castle.DynamicProxyを使っている場合、interface継承もしくは、仮想メソッドでないと割り込みできないのですが、割り込み対象が存在しない場合、例外が発生してしまいます。恐らくバグだとは思いますが、取りあえずカタログを分けることで回避可能です。

ネットで見かけたサンプルコードでは、ExportMetadata属性を使って割り込み対象の制御をおこなっていましたが、カスタム属性だとソースコードが必要な上、再コンパイルが必要です。AOP自体多用するものではありませんが、想定される使用状況を考えると、これはイマイチな気がしますね。

JetBrains.Annotations

ReSharperはコード分析してnull参照が発生する可能性があるところを指摘する機能があります。


このように該当箇所に波線が出て便利です。

しかし、メソッドの戻り値がnullの場合は、残念ながらチェックされません。

ただ、この機能はカスタム属性が使われているので、nullチェックをしたいメソッドにカスタム属性を付けることで対応可能です。


上記のようにReSharperのオプション設定から、Code Annotationsを表示させると、カスタム属性のデフォルト実装をクリップボードへコピー出来るので、これを自分のプロジェクトに取り込みます。

これでReSharper側で使っているカスタム属性が利用できるようになりました。では、早速。

CanBeNull属性を付けることでコード分析がnull参照を検出できるようになりましたね。

WebActivatorとPreApplicationStartMethodAttribute

MefContrib.MVC3によって生成された、AppStart_MefContribMVC3.csには、MEFを使用するための初期化コードがありますが、このコードは誰が呼び出しているのでしょうか?

[assembly: WebActivator.PreApplicationStartMethod(typeof(TestMvcApplication.App_Start.AppStart_MefContribMVC3), "Start")]

ファイルを覗いてみると上記のようなカスタム属性が見つかります。このWebActivatorがアセンブリの読み込み時に指定されたメソッドを呼び出してくれるのでした。

ところで、.NET4のFCLをチェックしている人なら、System.Web.PreApplicationStartMethodAttributeと同じじゃないの?と疑問に思うかも知れません。違いを確認するために以下のシナリオを考えてみます。

「MEFと同時に他のフレームワークも使用する」
で、NuGetを使って参照を追加すると、App_Startフォルダは以下のようになったとします。

WebActivatorを使わない場合は、この両方のファイルに
[assembly: PreApplicationStartMethod(typeof(AppStart_MefContribMVC), "Run")]
[assembly: PreApplicationStartMethod(typeof(AppStart_FooFramework), "Run")]
が指定されることになるはずです。

・・・が、これ、ビルドエラーになるんですよね。
PreApplicationStartMethodAttributeは1アセンブリに1つしか付けられないの(AllowMultiple=false)・・・

ぶっちゃけ、設計ミスですよね。(^^; もちろん、作った側もそれは認識していて、
Light up your NuGets with startup code and WebActivator – Angle Bracket Percent

The reason is that we were short sighted when we designed this feature in ASP.NET, and only allowed this attribute to be used once per assembly.

先見の明が無かったと率直に言っています。

っということで、NuGetのプロジェクトではPreApplicationStartMethodAttributeの代わりにWebActivatorが使われているという訳でした。

ASP.NET MVC 3でMEFを使う

MefContrib.MVC3を使うと簡単にASP.NET MVCからMEFを使えるのでご紹介。

前提:NuGetがインストール済みなこと。

まず、ASP.NET MVC3のプロジェクトの用意しましょう。

ASP.NET MVC3を選択。

面倒なのでデフォルト実装ありで。

では、MEFを使えるようにしましょう。NuGetがインストールされていると「Add Library Package Reference...」メニューが選べます。

MefContrib.MVC3を選択してインストールします。

すると、必要なライブラリがダウンロードされ、参照が追加されます。また、ASP.NET MVCからMEFを使うためのおまじないコードもプロジェクトに追加されます。

興味があるなら、AppStart_MefContribMVC3.csを覗いてみてください。

これで、MEFが使えるようになりました。では、実際に使ってみましょう。

とりあえず、「Logics」ディレクトリにインタフェースを、「Logics.Impl」にその実装クラスを置くことにします。もちろん、どこでもいいです。

インタフェースはお馴染みの、これ。

namespace TestMvcApplication.Logics
{
    public interface IGreeting
    {
        string Say(string msg);
    }
}

んで、テキトーな実装。

using System;
using System.ComponentModel.Composition;

namespace TestMvcApplication.Logics.Impl
{
    [Export(typeof(IGreeting))]
    public class GreetingImpl : IGreeting
    {
        public string Say(string msg)
        {
            return String.Format("Say 「{0}」", msg);
        }
    }
}

MEFを使ってIGreetingをエクスポートしています。

では、これをHomeControllerから使ってみましょう。

using System.ComponentModel.Composition;
using System.Web.Mvc;
using TestMvcApplication.Logics;

namespace TestMvcApplication.Controllers
{
    public class HomeController : Controller
    {
        [Import]
        public IGreeting Greeting { get; set; }

        public ActionResult Index()
        {
            ViewBag.Message = Greeting.Say("ASP.NET MVC へようこそ");

            return View();
        }

        public ActionResult About()
        {
            return View();
        }
    }
}

IGreetingをインポートして呼び出すだけ。

MefContrib.MVC3を使うと、そこそこ簡単にMEFが利用できますね。