ようこそ。睡眠不足なプログラマのチラ裏です。

MSILコードジェネレータでアスペクト指向的なラップクラスを作る

アスペクト指向とは

アスペクト指向とは、いわゆる“オブジェクト指向だけでは解決が困難であった問題”を
うまいこと解決しましょうよという試みであり、オブジェクト指向に取って代わるものではなく、
オブジェクト指向を補完するためのものであり、オブジェクト指向では分離することのできない
横断的関心事を分離する視点に立ったパラダイムである。


横断的関心事の例としてよくあげられるのが「ロギング機能」である。
ソフトウェアにとって、ログを取ることは非常に重要ではあるが、その中心となるのはビジネスロジックではない。
ロギングのような副次的な処理は往々にしてコードの至る所に、横断的に「散在」してしまう傾向が強い。
コード上に散在してしまう傾向のある横断的関心事は、ソフトウェアの保守性を妨げる原因となるだろう。
このような問題を解決しようとするための道具がアスペクト指向

MSILコードジェネレータでアスペクト指向的な何か
上記の例にもあったロギング機能について、MSILコードジェネレータで解決しようというのが今回のお題。
以下、必要とされるケースはあまりなさそうだが、メソッドごとに開始/終了ログ(メソッド名を含む)を出力したいような場合の例。

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.InteropServices;

namespace WindowsApplication1
{
    /// <summary>
    /// ClassWrapperで提供する、ラップ対象メソッドの前後処理を提供するインターフェイス
    /// </summary>
    public interface IMethodWrapper
    {
        /// <summary>
        /// ラップ対象メソッドの前処理を実装します。
        /// </summary>
        /// <param name="methodName">呼び出しメソッド名</param>
        void MethodWrappedBefore(string methodName);

        /// <summary>
        /// ラップ対象メソッドの後処理を実装します。
        /// </summary>
        /// <param name="methodName">呼び出しメソッド名</param>
        void MethodWrappedAfter(string methodName);
    }

    /// <summary>
    /// T型に対して、オーバーライド可能な仮想関数をラップしたクラスを提供します。
    /// </summary>
    public static class ClassWrapper
    {
        /// <summary>
        /// ラップクラスの型を保持するコレクション
        /// </summary>
        private static Dictionary<string, Type> dicWrapType = new Dictionary<string, Type>();

        /// <summary>
        /// Tのオーバーライド可能な仮想関数をラップします。
        /// </summary>
        /// <typeparam name="T">
        /// ラップ対象のT型
        /// 制約 : classである必要があります。
        ///      : IMethodWrapperインターフェイスを実装している必要があります。
        ///      : publicなデフォルトコンストラクタを有する必要があります。
        /// </typeparam>
        /// <returns>Tのラップクラス</returns>
        public static T Wrap<T>() where T :  class, IMethodWrapper, new()
        {
            Type t = typeof(T);
            Type wt;

            //ラップクラス名
            string wrapClassName = "Wrapped" + t.Name;

            if (dicWrapType.TryGetValue(wrapClassName, out wt))
            {
                //既に作成済みのラップクラスがあれば、それを返す
                return Activator.CreateInstance(wt) as T;
            }

            //動的アセンブリを定義
            AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("Wrapped" + t.Name + "Assembly"), AssemblyBuilderAccess.Run);

            //動的モジュール定義
            ModuleBuilder md = ab.DefineDynamicModule("Wrapped" + t.Name + "Module");

            //動的ラップクラスのインスタンスを定義
            TypeBuilder tb = md.DefineType("Wrapped" + t.Name, TypeAttributes.Class, t);
            
            //動的ラップクラスのデフォルトコンストラクタを定義 
            tb.DefineDefaultConstructor(MethodAttributes.Public);

            //Tが持つメソッドを検索
            foreach (MethodInfo mi in t.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance))
            {
                //オーバーライド可能な仮想関数のみオーバーライドします。(virtual且つ、finalではないメソッド)
                if (mi.IsVirtual && !mi.IsFinal)
                {
                    //パラメータ型取得
                    ParameterInfo[] pis = mi.GetParameters();
                    Type[] types = new Type[pis.Length];
                    for (int i = 0; i < pis.Length; ++i){
                        types[i] = pis[i].ParameterType;
                    }


                    //動的メソッドの定義(オーバーライドメソッドはシグネチャ同じということで)
                    MethodBuilder mb = tb.DefineMethod(mi.Name, mi.Attributes,mi.CallingConvention, mi.ReturnParameter.ParameterType, types);

                    //MSIL(Microsoft Intermediate Language)命令を生成
                    ILGenerator il = mb.GetILGenerator();

                    //パラメータをスタックに積む(thisパラメータもスタックに積むので+1)
                    for (int i = 0; i < pis.Length + 1; ++i){
                        il.Emit(OpCodes.Ldarg, i);
                    }


                    il.DeclareLocal(typeof(T));
                    //Tをデフォルトコンストラクタでインスタンス化しスタックにプッシュ
                    il.Emit(OpCodes.Newobj, typeof(T).GetConstructor(new Type[0]));
                    //0番目のローカル変数をスタックにプッシュ
                    il.Emit(OpCodes.Stloc_0);
                    //0番目のローカル変数をスタックに読み込む
                    il.Emit(OpCodes.Ldloc_0);
                    //メタデータのリテラル文字への新しいオブジェクト参照をプッシュ
                    il.Emit(OpCodes.Ldstr, mb.Name);                                            

                    //IMethodWrapperで実装された前処理を呼び出す
                    il.Emit(OpCodes.Call, typeof(T).GetMethod("MethodWrappedBefore"));

                    //基底クラスのメソッド呼び出し
                    il.Emit(OpCodes.Call, mi);

                    //0番目のローカル変数をスタックに読み込む
                    il.Emit(OpCodes.Ldloc_0);                                               
                    //メタデータのリテラル文字への新しいオブジェクト参照をプッシュ
                    il.Emit(OpCodes.Ldstr, mb.Name);                                        
                    
                    //IMethodWrapperで実装された後処理を呼び出す
                    il.Emit(OpCodes.Call, typeof(T).GetMethod("MethodWrappedAfter"));

                    //Return
                    il.Emit(OpCodes.Ret);

                    //オーバーライドする
                    tb.DefineMethodOverride(mb, mi);
                }
            }

            //ラップクラスの型を生成
            wt = tb.CreateType();
            
            //Dictionaryにラップクラスを登録
            dicWrapType[wrapClassName] = wt;
            
            //ラップクラスを生成し返す
            return Activator.CreateInstance(wt) as T;
        }
    }
}


実行時に新しい型を作るには、上記ソースコードのようにSystem.Reflection.Emitを使う。
これを使用すると、動的にアセンブリを作成し、動的アセンブリ内に動的モジュールを作り、
さらに動的モジュール内に新しい型を定義することができる。
そして、動的アセンブリに作った新しい型にメンバを追加し、それに対してMSILオペコードを作成することができる。



以下、クライアント側のコード

using System;
using System.Windows.Forms;

namespace WindowsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            WrapTest test = ClassWrapper.Wrap<WrapTest>();
            test.Write("アイウエ\(^o^)/オワタ");
            test.Write2("アイウエ\(^o^)/オワタ");

            WrapTest2 test2 = ClassWrapper.Wrap<WrapTest2>();
            test2.Message("アカサタ/(^o^)\ナンテコッタイ");
            test2.Message2("アカサタ/(^o^)\ナンテコッタイ");
        }
    }

    /// <summary>
    /// テスト用クラス その1
    /// </summary>
    public class WrapTest : IMethodWrapper
    {
        public virtual void Write(string voice)
        {
            Console.WriteLine(voice);
        }

        public virtual void Write2(string voice)
        {
            Console.WriteLine(voice + "2");
        }

        #region IMethodWrapper メンバ

        public void MethodWrappedBefore(string methodName)
        {
            Console.WriteLine("# " + methodName + " メソッド開始");
        }

        public void MethodWrappedAfter(string methodName)
        {
            Console.WriteLine("# " + methodName + " メソッド終了");
        }

        #endregion
    }

    /// <summary>
    /// テスト用クラス その2
    /// </summary>
    public class WrapTest2 : IMethodWrapper
    {
        public virtual void Message(string voice)
        {
            MessageBox.Show(voice);
        }

        public virtual void Message2(string voice)
        {
            MessageBox.Show(voice + "2");
        }

        #region IMethodWrapper メンバ

        public void MethodWrappedBefore(string methodName)
        {
            MessageBox.Show("# " + methodName + " メソッド開始");
        }

        public void MethodWrappedAfter(string methodName)
        {
            MessageBox.Show("# " + methodName + " メソッド終了");
        }

        #endregion
    }
 }


という具合。横断的関心事であるロギング機能(メソッド前後処理)を、インターフェイスとして抽出して分離し、
実行時におのおののクラスに対して織り込んで(ウィーブして)いる。
インターフェイス抽出することで、ログの出力方法を外だししているのがミソかな。
やりようによっては、MSILコードジェネレータを使用して、ダイナミックプロキシ的なものも作れるという感じでしょうか。
ん〜・・・、ちょっと想像しただけで超面倒くさそう(^ω^;)