Dynamic Proxies in C#

Proxy Pattern是Design Patterns中常用的一種設計模式,目的大致是隔離使用端及目標物件,降低使用端及目標物件的相依性,進而提升應用程式的可變性,也就是延展性。常見的Proxy應用大致如下圖。

 

/黃忠成

 

 

Proxy Pattern

  Proxy Pattern是Design Patterns中常用的一種設計模式,目的大致是隔離使用端及目標物件,降低使用端及目標物件的相依性,進而提升應用程式的可變性,也就是延展性。常見的Proxy應用大致如下圖。

 

圖1

 

 

 

原本應用程式是直接使用Target Object,在使用Proxy Pattern後,應用程式變成透過Proxy Object來操作Target Object。直覺上看來似乎是多了一層,但就軟體架構角度上來看,這一層的出現,可以讓應用程式與Target Object的相依性降低,使得Target Object不再是定死不變的,在完整的架構裡,大致會變成下圖。

圖2

 

 

 

在更大的系統架構中,甚至會出現Proxy 的Proxy這種設計。

圖3 

 

當然,Proxy Pattern在帶來延展性的同時也會增加系統的複雜度及效能的降低,因此判斷哪個部份該套上Proxy Pattern,哪個部份不用,本身就是個架構設計上的重要考量。

 

Source Code Level Proxy and Runtime Level Proxy

 

   關於Proxy Pattern的文件已經很多,此處就不再贅述了,回到主題。通常,Proxy Pattern指的是Source Code Level Proxy,意指在撰寫程式時就已經存在的設計,這些Proxy是

系統原始碼的一部分,要程式設計師花時間去設計及撰寫的。

   但Proxy 其實不單只有這種,還有一種常見的Proxy名為Runtime Level Proxy,這種Proxy不存在於系統設計之初,也不是系統原始碼的一部分,其是執行時期由Framework自動產生的Proxy。

   Runtime Level Proxy常見於幾個應用,例如.NET Remoting,客戶端發出要求取得遠端物件時,其實取得的便是一個Runtime Level Proxy,此時該Proxy的作用就是將客戶端對於遠端物件的

呼叫動作轉化為訊息後傳遞至遠端伺服器,而遠端伺服器則是將訊息還原為對於目標物件的呼叫動作來執行。

圖4

下面是一個Remoting的例子。

ShareIntf.cs(ShareDLL)


using System;
using System.Collections.Generic;
using System.Text;

namespace ShareDLL
{
    public interface IMyService
    {
        string SayHello(string name);
    }
}

Program.cs(Server)


using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;

namespace Server
{
    class Program
    {
        static void Main(string[] args)
        {
            TcpChannel chnl = new TcpChannel(9012);
            ChannelServices.RegisterChannel(chnl,false);
            RemotingConfiguration.RegisterWellKnownServiceType(typeof(MyServiceImpl), "myService.rem", WellKnownObjectMode.Singleton);
            Console.WriteLine("Server Is Running");
            Console.ReadLine();
            Console.ReadLine();
        }
    }

    public class MyServiceImpl : MarshalByRefObject,ShareDLL.IMyService
    {
        #region IMyService Members

        public string SayHello(string name)
        {
            return string.Format("Hello {0}", name);
        }

        #endregion
    }    
}


Program.cs(Client)


using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;

namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            TcpChannel chnl = new TcpChannel();
            ChannelServices.RegisterChannel(chnl, false);
            ShareDLL.IMyService service = (ShareDLL.IMyService)Activator.GetObject(typeof(ShareDLL.IMyService), "tcp://localhost:9012/myService.rem");
            Console.Write(service.SayHello("code6421"));
            Console.ReadLine();
        }
    }
}

如程式碼所列,Client端的Program.cs呼叫Activator.GetObject,此時取得的物件便是由Framework所動態產生的Proxy物件,作用就是將接下來的SayHello函式呼叫轉化為可透過網路傳遞的訊息,然後送往遠端伺服器,也就是Server來執行對MyServiceImpl.SayHello的呼叫,之後將回傳值轉化為訊息後回傳,此時該Proxy物件就負責將訊息轉化為回傳值。

Runtime Level Proxy成功的將呼叫轉化訊息,訊息轉化為回傳值動作封裝起來,使Client端於撰寫這類程式時更加方便且直覺。

 

Mock Object in Unit Testing

   常使用Unit Testing的朋友,對於Mock Object一定不陌生,這是TDD模式最重要的一部分,在此就不贅述了,直接來看一個NUnit Mock Object的應用例子。


using NUnit.Mocks;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DemoMock
{

    public interface IPerson
    {
        string Name { get; set; }
        int Age { get; set; }
        string Show();
    }

    class Program
    {
        static void Main(string[] args)
        {
            DynamicMock mock = new DynamicMock(typeof(IPerson));
            mock.SetReturnValue("Get_Name", "code6421");
            mock.SetReturnValue("Get_Age", "18");
            mock.SetReturnValue("Show", "code6421, 18");
            IPerson p = (IPerson)mock.MockInstance;
            Console.WriteLine("name :" + p.Name);
            Console.WriteLine(p.Show());
            Console.ReadLine();
        }
    }
}


如範例所示,我們在沒有實作IPerson的情況下撰寫程式,此時透過DynamicMock.MockInstance轉型過來的物件就是一個Runtime Level Proxy,而其函式呼叫,屬性讀寫的回應動作都是由使用端,也就是我們所控制,

當然,這段程式無法呈現出TDD與Mock Object的真實應用,我們改寫一下。這個範例使用早期NUnit的寫法,現在其實DynamicMock已經用Nsubstitute改寫了,不過為了簡化文章,此處還是採用舊的寫法。

Program.cs


using NUnit.Mocks;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DemoMock
{

    public interface IPerson
    {
        string Name { get; set; }
        int Age { get; set; }
        string Show();
    }

    public class Program
    {
        public static bool CheckAge(IPerson person)
        {
            return person.Age > 18;
        }

        static void Main(string[] args)
        {
            Console.ReadLine();
        }
    }
}

UnitTest.cs


using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NUnit.Mocks;
using DemoMock;

namespace UnitTestProject2
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void TestMethod1()
        {
            DynamicMock mock = new DynamicMock(typeof(IPerson));
            mock.SetReturnValue("Get_Name", "code6421");
            mock.SetReturnValue("Get_Age", "18");
            mock.SetReturnValue("Show", "code6421, 18");
            IPerson p = (IPerson)mock.MockInstance;
            Assert.IsTrue(Program.CheckAge(p));
        }
    }
}

仔細看一下UnitTest.cs,你會發現到我們在沒有Person物件的情況下對使用到Person物件的函式做單元測試,這便是TDD的精神,而此時DynamicMock所扮演的角色就是動態產生出一個實作IPerson介面的物件,而此物件就是Runtime Level Proxy,但與Remoting的作用有些不同,對於Mock物件的呼叫都會被轉往特定函式,而此特定函式則依據使用者,也就是Unit Test的設定而動作。

OK,聽起來有點玄,我們自己來做一個DynamicMock就可以完全弄懂Runtime Level Proxy的意義了。

MyMock.cs


using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;
using System.Text;

namespace DemoMock
{
    public class MyMock:RealProxy
    {
        private Dictionary<string,object> _methodResults = new Dictionary<string,object>();

        public MyMock(Type mockType):base(mockType)
        {
        }

        public void SetMethodResult(string methodName, object result)
        {
            if (_methodResults.ContainsKey(methodName))
                _methodResults[methodName] = result;
            else
                _methodResults.Add(methodName, result);
        }

        public override System.Runtime.Remoting.Messaging.IMessage Invoke(System.Runtime.Remoting.Messaging.IMessage msg)
        {
            string methodName = msg.Properties["__MethodName"] as string;
            if (_methodResults.ContainsKey(methodName))
            {
                ReturnMessage rm = new ReturnMessage(_methodResults[methodName], null, 0, null, (IMethodCallMessage)msg);
                return rm;
            }
            return null;
        }
    }
}

RealProxy是由.NET Framework所提供的一個Proxy類別,使用端建立RealProxy物件時必須指定將要Proxy的Target Interface,當使用端呼叫RealProxy的GetTransparentProxy函式時,其回傳的就是一個實作Target Interface介面的物件,當使用端操作此物件時,所有的函式呼叫,屬性存取都會被轉化為訊息傳到Invoke函式,此時這裡依據SetMethodResult的定義來做出回應動作,注意,回應動作也必須封裝為訊息後回傳。

透過RealProxy來實作Runtime Level Proxy算是相當簡單且快速,但其有個缺點,那就是RealProxy只在Full .NET Framework存在,Silverlight、Windows Phone中都不支援。

 

 

In Real World – Deep into IL Code and Emit

 

  RealProxy的缺點在於並非所有.NET Framework平台都支援,除此之外,它在效率上來說也是相當亮眼的,因為其底層是整合於CLR中,就技術上而言,其效能緊追於一般呼叫之後,唯一值得思慮的是訊息的轉換過程一定得花上一點時間,但這點誰來做都一樣。

  那如果不使用RealProxy呢?還有什麼方法可以達到同樣效果?目前已知的技術還有兩個,一個是CodeDOM,也就是直接於程式中以Code Expression物件群產生出程式樹,然後轉譯為C#、VB.NET程式碼編譯後執行,但此技巧與RealProxy一樣,並非所有.NET Framework平台都支援。

  二是使用.NET Framework內建的Emit機制,直接產生IL Code編譯後執行,此技巧也是目前在.NET Framework平台中相容性最高的,.NET Framework、Silverlight都支援(Windows Phone、Windows Store App、XBOX目前都未支援)。

  讓我們先看看以Emit寫成的MyMock範例好了。

MyMock.cs


using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;

namespace SimpleImpl
{
    public class BaseProxy
    {
        public Dictionary<string,object> _methodResults = null;


        public void SetMethodResults(Dictionary<string,object> methodResults)
        {
            _methodResults = methodResults;
        }

        public object GetMethodResult(string methodName)
        {
            if(_methodResults.ContainsKey(methodName))
                return _methodResults[methodName];
            return null;
        }

    }

    public class MyMock
    {        
        private Type _tpType = null;
        private Type _proxyType = null;
        private Dictionary<string,object> _methodResults = new Dictionary<string,object>();

        public MyMock(Type proxyType)
        {
            _proxyType = proxyType;
        }

        public object GetReturnValue(string methodName)
        {
            return _methodResults[methodName];
        }

        public void SetMethodResult(string methodName, object result)
        {
            if (_methodResults.ContainsKey(methodName))
                _methodResults[methodName] = result;
            else
                _methodResults.Add(methodName, result);
        }

        public object GetTransparentProxy()
        {
            if (_tpType == null)
            {
                TypeBuilder tb = GetTypeBuilder();
                ConstructorBuilder constructor = tb.DefineConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | 
                        MethodAttributes.RTSpecialName, CallingConventions.Standard, new Type[] {typeof(Dictionary<string,object>)});
                
                ILGenerator il = constructor.GetILGenerator();
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Stfld, typeof(BaseProxy).GetField("_methodResults",BindingFlags.Instance | BindingFlags.Public));
                il.Emit(OpCodes.Ret);

                List omitMethods = new List();
                foreach (var field in _proxyType.GetProperties())
                {
                    CreateProperty(tb, field.Name, field.PropertyType);
                    omitMethods.Add("get_" + field.Name);
                    omitMethods.Add("set_" + field.Name);
                }
                foreach (var mtd in _proxyType.GetMethods())
                {
                    if(omitMethods.IndexOf(mtd.Name) == -1)
                        CreateMethod(tb, mtd.Name, mtd.GetParameters().Select(a => a.ParameterType).ToArray(), mtd.ReturnType);
                }
                _tpType = tb.CreateType();
            }

            ConstructorInfo ci = _tpType.GetConstructor(new Type[]{typeof(Dictionary<string,object>)});
            return ci.Invoke(new object[]{_methodResults});
        }

        private TypeBuilder GetTypeBuilder()
        {
            var typeSignature = "MyDynamicType_" + Guid.NewGuid().ToString().Replace("{","").Replace('-','$');
            var an = new AssemblyName(typeSignature);
            AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run);
            ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
            TypeBuilder tb = moduleBuilder.DefineType(typeSignature
                                , TypeAttributes.Public |
                                TypeAttributes.Class |
                                TypeAttributes.AutoClass |
                                TypeAttributes.AnsiClass |
                                TypeAttributes.BeforeFieldInit |
                                TypeAttributes.AutoLayout
                                ,typeof(BaseProxy),new Type[]{_proxyType});
            return tb;
        }

        private void CreateProperty(TypeBuilder tb, string propertyName, Type propertyType)
        {
            FieldBuilder fieldBuilder = tb.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);

            PropertyBuilder propertyBuilder = tb.DefineProperty(propertyName, PropertyAttributes.SpecialName , propertyType, null);
            MethodBuilder getPropMthdBldr = tb.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName |
                                                             MethodAttributes.HideBySig | MethodAttributes.Final | MethodAttributes.NewSlot | 
                                                             MethodAttributes.Virtual, propertyType, Type.EmptyTypes);
            ILGenerator getIl = getPropMthdBldr.GetILGenerator();

            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Ldfld, fieldBuilder);
            getIl.Emit(OpCodes.Ret);

            MethodBuilder setPropMthdBldr =
                tb.DefineMethod("set_" + propertyName,
                  MethodAttributes.Public | MethodAttributes.SpecialName |
                  MethodAttributes.HideBySig | MethodAttributes.Final | MethodAttributes.NewSlot | MethodAttributes.Virtual,
                  null, new[] { propertyType });

            ILGenerator setIl = setPropMthdBldr.GetILGenerator();
            Label modifyProperty = setIl.DefineLabel();
            Label exitSet = setIl.DefineLabel();

            setIl.MarkLabel(modifyProperty);
            setIl.Emit(OpCodes.Ldarg_0);
            setIl.Emit(OpCodes.Ldarg_1);
            setIl.Emit(OpCodes.Stfld, fieldBuilder);

            setIl.Emit(OpCodes.Nop);
            setIl.MarkLabel(exitSet);
            setIl.Emit(OpCodes.Ret);

            propertyBuilder.SetGetMethod(getPropMthdBldr);
            propertyBuilder.SetSetMethod(setPropMthdBldr);
        }

        private void CreateMethod(TypeBuilder tb, string methodName, Type[] argsType, Type returnType)
        {            
            MethodBuilder mthdBldr = tb.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | 
                                                     MethodAttributes.Virtual, returnType, argsType);
            ILGenerator getIl = mthdBldr.GetILGenerator();
            if (returnType != typeof(void))
            {
                getIl.Emit(OpCodes.Ldarg_0);
                getIl.Emit(OpCodes.Ldstr, methodName);
                getIl.Emit(OpCodes.Call, typeof(BaseProxy).GetMethod("GetMethodResult", BindingFlags.Public | BindingFlags.Instance));
            }
            
            getIl.Emit(OpCodes.Ret);
        }
    }    
}

Program.cs


using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;

namespace SimpleImpl
{
    public interface IPerson
    {
        string Name { get; set; }
        int Age { get; set; }
        string Show();
    }


    class Program
    {
        static void Main(string[] args)
        {
            MyMock mock = new MyMock(typeof(IPerson));            
            mock.SetMethodResult("Show", "code6421, 18");
            IPerson p = (IPerson)mock.GetTransparentProxy();
            p.Name = "code6421";
            Console.WriteLine("name :" + p.Name);
            Console.WriteLine(p.Show());
            Console.ReadLine();
        }
    }
}

由GetTransparentProxy看起,此處一開始就建立了一個TypeBuilder,Emit流程大概如下圖。

圖5

TypeBuilder物件可以於執行時期中動態建立出一個型別,MyMock以此建立出與要求相符的類別,也就是建立一個類別並動態實作呼叫端要求的介面。


private TypeBuilder GetTypeBuilder()
{
            var typeSignature = "MyDynamicType_" + Guid.NewGuid().ToString().Replace("{","").Replace('-','$');
            var an = new AssemblyName(typeSignature);
            AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run);
            ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
            TypeBuilder tb = moduleBuilder.DefineType(typeSignature
                                , TypeAttributes.Public |
                                TypeAttributes.Class |
                                TypeAttributes.AutoClass |
                                TypeAttributes.AnsiClass |
                                TypeAttributes.BeforeFieldInit |
                                TypeAttributes.AutoLayout
                                ,typeof(BaseProxy),new Type[]{_proxyType});
            return tb;
}

當Assembly產生後,緊接著就是產生Module,這裡傳入的參數就不需要考慮重複性的問題了。在Module產生後就是產生Type,此時必要的參數就是型別名稱、修飾子如Public、AutoLayout,還有基礎類別(此處為BaseProxy)及實作的介面。在.NET Assembly的結構中,Type必須存在於Module內並嵌入於Assembly之中,因此必須先建立一個Assembly,傳入的typeSignature便是Assembly名稱,此處要注意Assembly名稱重複的問題,以動態的Assembly來說GUID是最好的選擇,不過要提醒讀者,在大型的Framework應用中通常會考慮到快取及重用的議題,因此此處最佳的設計模式是將產生的Assembly與Type配對,當下次要求的是重覆的Interface時就不重新產生而直接重用。

   MyMock的目的是產生出一個類別並實作特定介面,當使用者呼叫此類別的Method時進行攔截動作,用傳統的Emit手法的話,要做的工作其實很多,因此這裡選擇使用一個基礎類別來提供一些共通函式,好處是我們不需要以IL來寫這些函式。


public class BaseProxy
{
        public Dictionary<string,object> _methodResults = null;

        public void SetMethodResults(Dictionary<string,object> methodResults)
        {
            _methodResults = methodResults;
        }

        public object GetMethodResult(string methodName)
        {
            if(_methodResults.ContainsKey(methodName))
                return _methodResults[methodName];
            return null;
        }
}

在建立好Type後,接著就必須產生出該Type的成員函式,也就是使用端所要求的介面實作。以MyMock來說,所有的呼叫都必須被攔截,然後依據呼叫端的設定來決定函式的回傳值,所以共用的函式有兩個,一個是SetMethodResults,用來設定函式回傳值,一個是GetMethodResults,用來取得特定函式的回傳值,前者是由使用端呼叫,後者是由Emit產生的IL呼叫。如果不使用BaseProxy這種手法,就必須以IL產生出_methodResults欄位,接著產生出SetMethodResults供MyMock呼叫,並產生出函式由_methodResults欄位取值後傳回,這麼大量的IL碼其實很難寫。


        public object GetReturnValue(string methodName)
        {
            return _methodResults[methodName];
        }

        public void SetMethodResult(string methodName, object result)
        {
            if (_methodResults.ContainsKey(methodName))
                _methodResults[methodName] = result;
            else
                _methodResults.Add(methodName, result);
        }

        public object GetTransparentProxy()
        {
            if (_tpType == null)
            {
                TypeBuilder tb = GetTypeBuilder();
                ConstructorBuilder constructor = tb.DefineConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | 
                        MethodAttributes.RTSpecialName, CallingConventions.Standard, new Type[] {typeof(Dictionary<string,object>)});
                
                ILGenerator il = constructor.GetILGenerator();
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Stfld, typeof(BaseProxy).GetField("_methodResults",BindingFlags.Instance | BindingFlags.Public));
                il.Emit(OpCodes.Ret);

                List omitMethods = new List();
                foreach (var field in _proxyType.GetProperties())
                {
                    CreateProperty(tb, field.Name, field.PropertyType);
                    omitMethods.Add("get_" + field.Name);
                    omitMethods.Add("set_" + field.Name);
                }
                foreach (var mtd in _proxyType.GetMethods())
                {
                    if(omitMethods.IndexOf(mtd.Name) == -1)
                        CreateMethod(tb, mtd.Name, mtd.GetParameters().Select(a => a.ParameterType).ToArray(), mtd.ReturnType);
                }
                _tpType = tb.CreateType();
            }

            ConstructorInfo ci = _tpType.GetConstructor(new Type[]{typeof(Dictionary<string,object>)});
            return ci.Invoke(new object[]{_methodResults});
        }

當使用端使用MyMock物件時,是直接呼叫其SetMethodResults來設定函式的回傳值,因此在動態的Proxy建立時,必須要把MyMock中的_methodsResults也一併帶過去,這裡做法有兩種,一是寫在BaseProxy裡,這種很簡單,對讀者們應該不難,另一種是直接產生IL Code,也就是下面這樣。


                ConstructorBuilder constructor = tb.DefineConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | 
                        MethodAttributes.RTSpecialName, CallingConventions.Standard, new Type[] {typeof(Dictionary<string,object>)});
                
                ILGenerator il = constructor.GetILGenerator();
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Stfld, typeof(BaseProxy).GetField("_methodResults",BindingFlags.Instance | BindingFlags.Public));
                il.Emit(OpCodes.Ret);

好了,最難的部分來了,首先ConstructorBuilder是MethodBuilder的另一種形態,用來產生出建構子,在定義好參數與修飾子後就是IL Code了,Ldargs_0指的是載入函式的第一個參數,以此類推到Ldargs_3(第四個參數),這是IL內建的快速存取指令,當需要存取第四個參數之後時就得使用Ldargs_S。注意!除了靜態函式外,所有的類別成員函式的第一個參數都是物件本身(this),所以在此建構子雖然有一個參數,但從IL來看是載入第一個(this),及第二個(_methodResults)。

    Stfld可以將指定的值放入特定的Field(欄位)中,注意,你沒看到所謂指定的值是吧?在IL的世界中,stFld的上一個指令就是在指定這個值,也就是說將ldargs_1(第二個參數)放入_methodResults(stfld)中,最後Emit Ret也就是回傳的IL碼,注意!前面是Stfld,也就是這個函式是沒有回傳值的。

那怎麼呼叫建構子來產生物件呢?很簡單,就是Reflection。


            ConstructorInfo ci = _tpType.GetConstructor(new Type[]{typeof(Dictionary<string,object>)});
            return ci.Invoke(new object[]{_methodResults});

IL不是比較快?是的,但可以仔細想想,在這裡是無法使用IL來呼叫這個建構子的,因為那必須再產生出另一個Assembly、Type、Method後呼叫,然後再產生出另一個Assembly、Type、Method後呼叫前者,結論還是一樣,一定會有一個Reflection存在於外面。

有了建構子的產生概念,現在可以來談談Property及Method的產生了。


        private void CreateProperty(TypeBuilder tb, string propertyName, Type propertyType)
        {
            FieldBuilder fieldBuilder = tb.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);

            PropertyBuilder propertyBuilder = tb.DefineProperty(propertyName, PropertyAttributes.SpecialName , propertyType, null);
            MethodBuilder getPropMthdBldr = tb.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName |
                                                             MethodAttributes.HideBySig | MethodAttributes.Final | MethodAttributes.NewSlot | 
                                                             MethodAttributes.Virtual, propertyType, Type.EmptyTypes);
            ILGenerator getIl = getPropMthdBldr.GetILGenerator();

            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Ldfld, fieldBuilder);
            getIl.Emit(OpCodes.Ret);

            MethodBuilder setPropMthdBldr =
                tb.DefineMethod("set_" + propertyName,
                  MethodAttributes.Public | MethodAttributes.SpecialName |
                  MethodAttributes.HideBySig | MethodAttributes.Final | MethodAttributes.NewSlot | MethodAttributes.Virtual,
                  null, new[] { propertyType });

            ILGenerator setIl = setPropMthdBldr.GetILGenerator();

            setIl.Emit(OpCodes.Ldarg_0);
            setIl.Emit(OpCodes.Ldarg_1);
            setIl.Emit(OpCodes.Stfld, fieldBuilder);

            setIl.Emit(OpCodes.Nop);
            setIl.Emit(OpCodes.Ret);

            propertyBuilder.SetGetMethod(getPropMthdBldr);
            propertyBuilder.SetSetMethod(setPropMthdBldr);
        }

什麼是Property?在.NET Framework中,Property的構造是一個成員變數加上set_<PropertyName>及get_<PropertyName>兩個函式,這兩個函式必須為Public,於此處有點比較特別的是,MyMock是產生出一個類別來實做特定介面,所以此處產生的Property必然是介面所定義的,當在IL實作介面函式必須遵守一個規則,函式必須被標記為virtual。  剩下的就是將參數取入變數這種簡單的動作了,接著看Method的產生。


        private void CreateMethod(TypeBuilder tb, string methodName, Type[] argsType, Type returnType)
        {            
            MethodBuilder mthdBldr = tb.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | 
                                                     MethodAttributes.Virtual, returnType, argsType);
            ILGenerator getIl = mthdBldr.GetILGenerator();
            if (returnType != typeof(void))
            {
                getIl.Emit(OpCodes.Ldarg_0);
                getIl.Emit(OpCodes.Ldstr, methodName);
                getIl.Emit(OpCodes.Call, typeof(BaseProxy).GetMethod("GetMethodResult", BindingFlags.Public | BindingFlags.Instance));
            }
            
            getIl.Emit(OpCodes.Ret);
        }

IL的Call指令可以讓我們呼叫特定函式,其必須傳入一個MethodBuilder或是MethodInfo,注意!此處沒有指定參數是吧?與產生建構子時相同,前面的指令便是在準備參數,通常用低階語言的說法來說就是 :將參數推入堆疊然後呼叫函式。Ldargs_0將this推入,Ldstr指令則將一個字串推入堆疊中,注意!不同形態的參數必須使用不同的IL指令。這裡為何要推入ldargs_0呢?很簡單,因為接下來要呼叫GetMethodResult,而這是一個成員函式,要呼叫成員函式前得先把該成員函式所屬物件實體推入堆疊(還記得成員函式的第一個參數都是this嗎?)。

好了,大概就是如此,下面是此範例的執行畫面。

圖6

 

Final – Build Dynamic Proxy and Implement Method Interceptor

  以Mock目的來說,前面的範例已經能充分表達其原理了,接下來是更深入的應用,在AOP、DI的Framework如Spring中,允許使用端掛載一個類別至動態產生的類別上,當使用端呼叫該類別中的函式時都會事先呼叫這個類別所定義的特定函式,此種應用與RealProxy相同,就是攔截函式呼叫,然後將參數等狀態轉給另一個函式。


    public class ConsoleLoggingAroundAdvice : IMethodInterceptor
    {
        public object Invoke(IMethodInvocation invocation)
        {
            Console.Out.WriteLine("Advice executing; calling the advised method..."); 1
            object returnValue = invocation.Proceed(); 2 3
            Console.Out.WriteLine("Advice executed; advised method returned " + returnValue); 4
            return returnValue; 5
        }
    }
…………………
    ProxyFactory factory = new ProxyFactory(new ServiceCommand());
    factory.AddAdvice(new ConsoleLoggingAroundAdvice());
    ICommand command = (ICommand) factory.GetProxy();
    command.Execute("This is the argument");

以前面所提到的知識要做同樣的東西並不難,見下例。

Proxies.cs


using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;

namespace SimpleProxy
{
    public interface ICallInterceptor
    {
        void PreCall(string methodName, Dictionary<string,object> args);
        void PostCall(string methodName, Dictionary<string,object> args);
    }

    public class BaseProxy
    {
        public ICallInterceptor _interceptor = null;
        public object _instance = null;
    }


    public class SimpleProxy
    {
        private Type _tpType = null;
        private Type _proxyType = null;

        public SimpleProxy(Type proxyType)
        {
            _proxyType = proxyType;
        }


        public object GetTransparentProxy(ICallInterceptor interceptor, object instance)
        {
            if (_tpType == null)
            {
                TypeBuilder tb = GetTypeBuilder();
                ConstructorBuilder constructor = tb.DefineConstructor(MethodAttributes.Public | MethodAttributes.SpecialName |
                        MethodAttributes.RTSpecialName, CallingConventions.Standard, new Type[] { typeof(ICallInterceptor), typeof(object) });

                ILGenerator il = constructor.GetILGenerator();
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Stfld, typeof(BaseProxy).GetField("_interceptor", BindingFlags.Instance | BindingFlags.Public));
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldarg_2);
                il.Emit(OpCodes.Stfld, typeof(BaseProxy).GetField("_instance", BindingFlags.Instance | BindingFlags.Public));
                il.Emit(OpCodes.Ret);

                List omitMethods = new List();
                foreach (var field in _proxyType.GetProperties())
                {
                    CreateProperty(tb, field.Name, field.PropertyType);
                    omitMethods.Add("get_" + field.Name);
                    omitMethods.Add("set_" + field.Name);
                }
                foreach (var mtd in _proxyType.GetMethods())
                {
                    if (omitMethods.IndexOf(mtd.Name) == -1)
                        CreateMethod(tb, mtd.Name, mtd.GetParameters().Select(a => a.ParameterType).ToArray(), mtd.ReturnType);
                }
                _tpType = tb.CreateType();
            }

            ConstructorInfo ci = _tpType.GetConstructor(new Type[] { typeof(ICallInterceptor),typeof(object) });
            return ci.Invoke(new object[] { interceptor, instance });
        }

        private TypeBuilder GetTypeBuilder()
        {
            var typeSignature = "MyDynamicType_" + Guid.NewGuid().ToString().Replace("{", "").Replace('-', '$');
            var an = new AssemblyName(typeSignature);
            AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run);
            ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
            TypeBuilder tb = moduleBuilder.DefineType(typeSignature
                                , TypeAttributes.Public |
                                TypeAttributes.Class |
                                TypeAttributes.AutoClass |
                                TypeAttributes.AnsiClass |
                                TypeAttributes.BeforeFieldInit |
                                TypeAttributes.AutoLayout
                                , typeof(BaseProxy), new Type[] { _proxyType });
            return tb;
        }

        private void CreateProperty(TypeBuilder tb, string propertyName, Type propertyType)
        {
            FieldBuilder fieldBuilder = tb.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);

            PropertyBuilder propertyBuilder = tb.DefineProperty(propertyName, PropertyAttributes.SpecialName, propertyType, null);
            MethodBuilder getPropMthdBldr = tb.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName |
                                                             MethodAttributes.HideBySig | MethodAttributes.Final | MethodAttributes.NewSlot |
                                                             MethodAttributes.Virtual, propertyType, Type.EmptyTypes);
            ILGenerator getIl = getPropMthdBldr.GetILGenerator();

            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Ldfld, fieldBuilder);
            getIl.Emit(OpCodes.Ret);

            MethodBuilder setPropMthdBldr =
                tb.DefineMethod("set_" + propertyName,
                  MethodAttributes.Public | MethodAttributes.SpecialName |
                  MethodAttributes.HideBySig | MethodAttributes.Final | MethodAttributes.NewSlot | MethodAttributes.Virtual,
                  null, new[] { propertyType });

            ILGenerator setIl = setPropMthdBldr.GetILGenerator();
            
            setIl.Emit(OpCodes.Ldarg_0);
            setIl.Emit(OpCodes.Ldarg_1);
            setIl.Emit(OpCodes.Stfld, fieldBuilder);

            setIl.Emit(OpCodes.Nop);
            setIl.Emit(OpCodes.Ret);

            propertyBuilder.SetGetMethod(getPropMthdBldr);
            propertyBuilder.SetSetMethod(setPropMthdBldr);
        }

        private void CreateMethod(TypeBuilder tb, string methodName, Type[] argsType, Type returnType)
        {
            MethodBuilder mthdBldr = tb.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig |
                                                     MethodAttributes.Virtual, returnType, argsType);
            ILGenerator getIl = mthdBldr.GetILGenerator();            
            ConstructorInfo ci = typeof(Dictionary<string,object>).GetConstructor(new Type[0]);

            LocalBuilder lb1 = getIl.DeclareLocal(typeof(Dictionary<string,object>));
            LocalBuilder lb2 = null;
            if (returnType != typeof(void))
                lb2 = getIl.DeclareLocal(returnType);
            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Newobj, ci);
            getIl.Emit(OpCodes.Stloc, lb1);            
            int index = 1;
            foreach (var param in _proxyType.GetMethod(methodName).GetParameters())
            {
                getIl.Emit(OpCodes.Ldloc, lb1);
                getIl.Emit(OpCodes.Ldstr, param.Name);
                if (index < 4)
                {
                    switch (index)
                    {
                        case 1:
                            getIl.Emit(OpCodes.Ldarg_1);
                            break;
                        case 2:
                            getIl.Emit(OpCodes.Ldarg_2);
                            break;
                        case 3:
                            getIl.Emit(OpCodes.Ldarg_3);
                            break;
                    }
                }
                else
                    getIl.Emit(OpCodes.Ldarg_S, index);
                if (param.ParameterType.IsPrimitive || param.ParameterType.IsValueType)
                    getIl.Emit(OpCodes.Box, param.ParameterType);
                getIl.Emit(OpCodes.Callvirt, typeof(Dictionary<string,object>).GetMethod("Add"));
                index++;
            }
            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Ldfld, typeof(BaseProxy).GetField("_interceptor"));
            getIl.Emit(OpCodes.Ldstr, methodName);
            getIl.Emit(OpCodes.Ldloc, lb1);
            getIl.Emit(OpCodes.Call, typeof(ICallInterceptor).GetMethod("PreCall"));

            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Ldfld, typeof(BaseProxy).GetField("_instance"));
            index = 1;
            foreach (var param in _proxyType.GetMethod(methodName).GetParameters())
            {
                if (index < 4)
                {
                    switch (index)
                    {
                        case 1:
                            getIl.Emit(OpCodes.Ldarg_1);
                            break;
                        case 2:
                            getIl.Emit(OpCodes.Ldarg_2);
                            break;
                        case 3:
                            getIl.Emit(OpCodes.Ldarg_3);
                            break;
                    }
                }
                else
                    getIl.Emit(OpCodes.Ldarg_S, index);
                //below call instructoon don't need box , because caller(user code) will box first.
                //if (param.ParameterType.IsPrimitive|| param.ParameterType.IsValueType)
                //    getIl.Emit(OpCodes.Box, param.ParameterType);
                index++;
            }
            getIl.Emit(OpCodes.Call, _proxyType.GetMethod(methodName));

            if (lb2 != null)
            {                
                getIl.Emit(OpCodes.Stloc, lb2);
            }

            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Ldfld, typeof(BaseProxy).GetField("_interceptor"));
            getIl.Emit(OpCodes.Ldstr, methodName);
            getIl.Emit(OpCodes.Ldloc, lb1);
            getIl.Emit(OpCodes.Call, typeof(ICallInterceptor).GetMethod("PostCall"));
            if (lb2 != null)
            {
                getIl.Emit(OpCodes.Ldloc, lb2);
            }
            getIl.Emit(OpCodes.Ret);
        }
    }
}

Program.cs


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SimpleProxy
{
    public class MyInterceptor : ICallInterceptor
    {
        public void PreCall(string methodName, Dictionary<string,object> args)
        {
            Console.WriteLine("precall:" + methodName);
            foreach (var item in args.Values)
                Console.WriteLine(item.ToString());
            Console.WriteLine("-------------------------------");
        }

        public void PostCall(string methodName, Dictionary<string,object> args)
        {
            Console.WriteLine("postcall:" + methodName);
            foreach (var item in args.Values)
                Console.WriteLine(item.ToString());
            Console.WriteLine("-------------------------------");
        }
    }

    public interface IMyIntf
    {
        void Show(string name);
        int Sum(int x, int y);
    }

    public class MyObj:IMyIntf
    {

        public void Show(string name)
        {
            Console.WriteLine(name);            
        }


        public int Sum(int x, int y)
        {
            return x + y;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            SimpleProxy proxy = new SimpleProxy(typeof(IMyIntf));
            IMyIntf obj = proxy.GetTransparentProxy(new MyInterceptor(), new MyObj()) as IMyIntf;
            obj.Show("TEST");
            Console.WriteLine("Sum: " + obj.Sum(15, 20));
            Console.ReadLine();
        }
    }
}

先看ICallInterceptor,要掛載函式成函式呼叫攔截者的物件都必須實作此介面,當目標物件的函式被呼叫前後都會把參數打包傳入。


    public interface ICallInterceptor
    {
        void PreCall(string methodName, Dictionary<string,object> args);
        void PostCall(string methodName, Dictionary<string,object> args);
    }

目的不同,所以BaseProxy定義也有不同。


    public class BaseProxy
    {
        public ICallInterceptor _interceptor = null;
        public object _instance = null;
    }

實際應用時可將_interceptor定義為List,允許掛載多個Interceptor,另外,此處的_instance是指向要掛載攔截者的物件,也就是我們產生出來的那個動態類別所生成的物件。


        public object GetTransparentProxy(ICallInterceptor interceptor, object instance)
        {
            if (_tpType == null)
            {
                TypeBuilder tb = GetTypeBuilder();
                ConstructorBuilder constructor = tb.DefineConstructor(MethodAttributes.Public | MethodAttributes.SpecialName |
                        MethodAttributes.RTSpecialName, CallingConventions.Standard, new Type[] { typeof(ICallInterceptor), typeof(object) });

                ILGenerator il = constructor.GetILGenerator();
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Stfld, typeof(BaseProxy).GetField("_interceptor", BindingFlags.Instance | BindingFlags.Public));
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldarg_2);
                il.Emit(OpCodes.Stfld, typeof(BaseProxy).GetField("_instance", BindingFlags.Instance | BindingFlags.Public));
                il.Emit(OpCodes.Ret);

                List omitMethods = new List();
                foreach (var field in _proxyType.GetProperties())
                {
                    CreateProperty(tb, field.Name, field.PropertyType);
                    omitMethods.Add("get_" + field.Name);
                    omitMethods.Add("set_" + field.Name);
                }
                foreach (var mtd in _proxyType.GetMethods())
                {
                    if (omitMethods.IndexOf(mtd.Name) == -1)
                        CreateMethod(tb, mtd.Name, mtd.GetParameters().Select(a => a.ParameterType).ToArray(), mtd.ReturnType);
                }
                _tpType = tb.CreateType();
            }

            ConstructorInfo ci = _tpType.GetConstructor(new Type[] { typeof(ICallInterceptor),typeof(object) });
            return ci.Invoke(new object[] { interceptor, instance });
        }

GetTransparentProxy部份的建構子產生部份也變成接受兩個參數:Interceptor與要Proxy的物件,這裡看到了較複雜的IL碼,但基本上應該還在可理解範圍內。


        private void CreateMethod(TypeBuilder tb, string methodName, Type[] argsType, Type returnType)
        {
            MethodBuilder mthdBldr = tb.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig |
                                                     MethodAttributes.Virtual, returnType, argsType);
            ILGenerator getIl = mthdBldr.GetILGenerator();            
            ConstructorInfo ci = typeof(Dictionary<string,object>).GetConstructor(new Type[0]);

            LocalBuilder lb1 = getIl.DeclareLocal(typeof(Dictionary<string,object>));
            LocalBuilder lb2 = null;
            if (returnType != typeof(void))
                lb2 = getIl.DeclareLocal(returnType);
            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Newobj, ci);
            getIl.Emit(OpCodes.Stloc, lb1);            
            int index = 1;
            foreach (var param in _proxyType.GetMethod(methodName).GetParameters())
            {
                getIl.Emit(OpCodes.Ldloc, lb1);
                getIl.Emit(OpCodes.Ldstr, param.Name);
                if (index < 4)
                {
                    switch (index)
                    {
                        case 1:
                            getIl.Emit(OpCodes.Ldarg_1);
                            break;
                        case 2:
                            getIl.Emit(OpCodes.Ldarg_2);
                            break;
                        case 3:
                            getIl.Emit(OpCodes.Ldarg_3);
                            break;
                    }
                }
                else
                    getIl.Emit(OpCodes.Ldarg_S, index);
                if (param.ParameterType.IsPrimitive || param.ParameterType.IsValueType)
                    getIl.Emit(OpCodes.Box, param.ParameterType);
                getIl.Emit(OpCodes.Callvirt, typeof(Dictionary<string,object>).GetMethod("Add"));
                index++;
            }
            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Ldfld, typeof(BaseProxy).GetField("_interceptor"));
            getIl.Emit(OpCodes.Ldstr, methodName);
            getIl.Emit(OpCodes.Ldloc, lb1);
            getIl.Emit(OpCodes.Call, typeof(ICallInterceptor).GetMethod("PreCall"));

            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Ldfld, typeof(BaseProxy).GetField("_instance"));
            index = 1;
            foreach (var param in _proxyType.GetMethod(methodName).GetParameters())
            {
                if (index < 4)
                {
                    switch (index)
                    {
                        case 1:
                            getIl.Emit(OpCodes.Ldarg_1);
                            break;
                        case 2:
                            getIl.Emit(OpCodes.Ldarg_2);
                            break;
                        case 3:
                            getIl.Emit(OpCodes.Ldarg_3);
                            break;
                    }
                }
                else
                    getIl.Emit(OpCodes.Ldarg_S, index);
                //below call instructoon don't need box , because caller(user code) will box first.
                //if (param.ParameterType.IsPrimitive|| param.ParameterType.IsValueType)
                //    getIl.Emit(OpCodes.Box, param.ParameterType);
                index++;
            }
            getIl.Emit(OpCodes.Call, _proxyType.GetMethod(methodName));

            if (lb2 != null)
            {                
                getIl.Emit(OpCodes.Stloc, lb2);
            }

            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Ldfld, typeof(BaseProxy).GetField("_interceptor"));
            getIl.Emit(OpCodes.Ldstr, methodName);
            getIl.Emit(OpCodes.Ldloc, lb1);
            getIl.Emit(OpCodes.Call, typeof(ICallInterceptor).GetMethod("PostCall"));
            if (lb2 != null)
            {
                getIl.Emit(OpCodes.Ldloc, lb2);
            }
            getIl.Emit(OpCodes.Ret);
        }

CreateMethod是此範例最複雜的部份,因為在這裡我們必須用IL指令碼完成參數的打包並放入一個Dictionary<string,object>區域變數,攔截者的呼叫等動作,由於參數部份是不定的,這裡用Reflection來取得參數數量及型態。


            LocalBuilder lb1 = getIl.DeclareLocal(typeof(Dictionary<string,object>));
            LocalBuilder lb2 = null;
            if (returnType != typeof(void))
                lb2 = getIl.DeclareLocal(returnType);
            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Newobj, ci);
            getIl.Emit(OpCodes.Stloc, lb1);            
            int index = 1;
            foreach (var param in _proxyType.GetMethod(methodName).GetParameters())
            {
                getIl.Emit(OpCodes.Ldloc, lb1);
                getIl.Emit(OpCodes.Ldstr, param.Name);
                if (index < 4)
                {
                    switch (index)
                    {
                        case 1:
                            getIl.Emit(OpCodes.Ldarg_1);
                            break;
                        case 2:
                            getIl.Emit(OpCodes.Ldarg_2);
                            break;
                        case 3:
                            getIl.Emit(OpCodes.Ldarg_3);
                            break;
                    }
                }
                else
                    getIl.Emit(OpCodes.Ldarg_S, index);
                if (param.ParameterType.IsPrimitive || param.ParameterType.IsValueType)
                    getIl.Emit(OpCodes.Box, param.ParameterType);
                getIl.Emit(OpCodes.Callvirt, typeof(Dictionary<string,object>).GetMethod("Add"));
                index++;
            }

LocalBuilder用來宣告區域變數,也就是函式中的區域變數,NewObj則用來生成區域變數實體,注意,第二個參數是建構子,如果建構子需要參數傳遞,前面就得用stfld等指令來推參數進堆疊。緊接著是將參數放入Dictionary<string,object>區域變數中,注意!這個動作是函式呼叫,也就是呼叫Add函式,在這之前必須使用ldloc來將該區域變數,也就是要呼叫Add函式的物件推入堆疊中。


foreach (var param in _proxyType.GetMethod(methodName).GetParameters())
            {
                getIl.Emit(OpCodes.Ldloc, lb1);
                getIl.Emit(OpCodes.Ldstr, param.Name);
                if (index < 4)
                {
                    switch (index)
                    {
                        case 1:
                            getIl.Emit(OpCodes.Ldarg_1);
                            break;
                        case 2:
                            getIl.Emit(OpCodes.Ldarg_2);
                            break;
                        case 3:
                            getIl.Emit(OpCodes.Ldarg_3);
                            break;
                    }
                }
                else
                    getIl.Emit(OpCodes.Ldarg_S, index);
                if (param.ParameterType.IsPrimitive || param.ParameterType.IsValueType)
                    getIl.Emit(OpCodes.Box, param.ParameterType);
                getIl.Emit(OpCodes.Callvirt, typeof(Dictionary<string,object>).GetMethod("Add"));
                index++;
            }

這裡讀者們可以看到對於1~4個參數是用ldarg_,而5之後的參數是用ldarg_s,這在推參數堆疊時要特別注意。另外,當參數是值型別或是內建型別(int,short…)時,要推入Dictionary<string,object>前必須進行box。


                if (param.ParameterType.IsPrimitive || param.ParameterType.IsValueType)
                    getIl.Emit(OpCodes.Box, param.ParameterType);
                getIl.Emit(OpCodes.Callvirt, typeof(Dictionary<string,object>).GetMethod("Add"));

接下來是呼叫Interceptor。


            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Ldfld, typeof(BaseProxy).GetField("_interceptor"));
            getIl.Emit(OpCodes.Ldstr, methodName);
            getIl.Emit(OpCodes.Ldloc, lb1);
            getIl.Emit(OpCodes.Call, typeof(ICallInterceptor).GetMethod("PreCall"));

因為interceptor是儲存於BaseProxy的_interceptor變數內,所以要呼叫他之前得先把這個變數推入堆疊,然後接著推參數,這裡第一個參數是函式名稱,因為是字串所以用ldstr,接著是Dictionary<string,object>這個區域變數,因為是區域變數,所以使用ldloc。
最後是呼叫PostCall及轉接回傳值。


            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Ldfld, typeof(BaseProxy).GetField("_instance"));
            index = 1;
            foreach (var param in _proxyType.GetMethod(methodName).GetParameters())
            {
                if (index < 4)
                {
                    switch (index)
                    {
                        case 1:
                            getIl.Emit(OpCodes.Ldarg_1);
                            break;
                        case 2:
                            getIl.Emit(OpCodes.Ldarg_2);
                            break;
                        case 3:
                            getIl.Emit(OpCodes.Ldarg_3);
                            break;
                    }
                }
                else
                    getIl.Emit(OpCodes.Ldarg_S, index);
                //below call instructoon don't need box , because caller(user code) will box first.
                //if (param.ParameterType.IsPrimitive|| param.ParameterType.IsValueType)
                //    getIl.Emit(OpCodes.Box, param.ParameterType);
                index++;
            }
            getIl.Emit(OpCodes.Call, _proxyType.GetMethod(methodName));
	
            if (lb2 != null)
            {                
                getIl.Emit(OpCodes.Stloc, lb2);
            }

            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Ldfld, typeof(BaseProxy).GetField("_interceptor"));
            getIl.Emit(OpCodes.Ldstr, methodName);
            getIl.Emit(OpCodes.Ldloc, lb1);
            getIl.Emit(OpCodes.Call, typeof(ICallInterceptor).GetMethod("PostCall"));
            if (lb2 != null)
            {
                getIl.Emit(OpCodes.Ldloc, lb2);
            }
            getIl.Emit(OpCodes.Ret);

這裡應該不用再解釋為何第一個指令是ldarg_0了吧?因為存取成員變數與函式都一樣,要先把所屬物件實體推入堆疊。注意,當呼叫完欲Proxy物件函式後,其回傳值要從堆疊中取出並放入預先準備的lb2這個區域變數內。


            getIl.Emit(OpCodes.Call, _proxyType.GetMethod(methodName));
	
            if (lb2 != null)
            {                
                getIl.Emit(OpCodes.Stloc, lb2);
            }

最後在Ret前將lb2推入堆疊。


            if (lb2 != null)
            {
                getIl.Emit(OpCodes.Ldloc, lb2);
            }
            getIl.Emit(OpCodes.Ret);

圖7是執行結果。

 

後記

  用Emit來產生IL其實並不難,難的部分在於如何維持堆疊的完整性,例如在呼叫Intereceptor那個例子中會看到不停的下ldarg_0,這是因為指定某個成員變數時必須先將該成員變數所屬的物件實體推入堆疊中,掌握這點後,IL就是流水帳而已。