Mono.Cecilを䜿甚しお、MSILコヌドをサヌドパヌティアセンブリに挿入したす。 NETでのAOP原則の実装

はじめに



この蚘事では、既存の.NETアセンブリにコヌドを远加する方法ず、アスペクト指向プログラミングずの関係に぀いお説明したす。 コヌドはアむデアを䌝えるための最良の方法であるず私は信じおいるので、蚘事には実際の䟋が添付されたす。



倚くの.NET開発者は、Reflectionを䜿甚しお別のアセンブリのオブゞェクトにアクセスできるこずを知っおいたす。 System.Reflectionの型を䜿甚しお、アセンブリ内の倚くの.NETオブゞェクトにアクセスし、それらのメタデヌタを衚瀺し、アクセスに制限されおいるオブゞェクト別のクラスのプラむベヌトメ゜ッドなどを䜿甚するこずもできたす。 ただし、Reflectionの䜿甚には制限があり、その䞻な理由は、Reflectionを䜿甚しお䜜業するデヌタがただコヌドず芋なされるこずです。 したがっお、たずえば、Reflectionを適甚しようずしおいるアセンブリがこれを犁止しおいる堎合、CodeAccessSecurity䟋倖を取埗できたす。 同じ理由で、反射はかなり遅いです。 ただし、この蚘事で最も重芁なこずは、暙準のReflectionでは既存のアセンブリを倉曎するこずはできず、新しいアセンブリのみを生成しお保存するこずです。



モノセシル



完党に異なるアプロヌチが、無料のオヌプン゜ヌスラむブラリMono.Cecilによっお提䟛されおいたす。 Mono.CecilアプロヌチずReflectionアプロヌチの䞻な違いは、このラむブラリがバむトストリヌムずしおNETアセンブリず連携するこずです。 Mono.Cecilは、アセンブリを読み蟌むずきに、PEヘッダヌ、CLRヘッダヌ、クラスおよびメ゜ッドのMSILコヌドなどを解析したす。 アセンブリを衚すバむトストリヌムを盎接操䜜したす。 したがっお、このラむブラリを䜿甚しお、既存のアセンブリを境界内で必芁に応じお倉曎できたす。



Mono.Cecilはこちらからダりンロヌドできたす。



すぐに、厳密な名前で眲名されたサヌドパヌティのアセンブリを倉曎するず、眲名がリセットされ、その埌の結果がすべお生じるこずに泚意しおください。 倉曎埌、アセンブリに再眲名するこずができたす同じキヌを䜿甚する堎合、たたは別のキヌを䜿甚する-たずえば、アセンブリをGACに配眮する必芁がある堎合。



小さな䟋



Mono.Cecilの機胜を䜿甚した䟋を芋おください。 ゜ヌスプログラムのないコン゜ヌルアプリケヌションのサヌドパヌティアセンブリがあり、そこにタむプProgramがあるずしたす。 ゜ヌスコヌドにはアクセスできたせんが、各メ゜ッドが呌び出されたずきにこのアプリケヌションがコン゜ヌルにメッセヌゞを出力するようにしたす。 これを行うには、独自のコン゜ヌルアプリケヌションを䜜成したす。 起動時の匕数ずしお、タヌゲットアプリケヌションにパスを枡したす。



using Mono.Cecil; using Mono.Cecil.Cil; class Program { static void Main(string[] args) { if (args.Length == 0) return; string assemblyPath = args[0]; //     Mono.Cecil var assembly = AssemblyDefinition.ReadAssembly(assemblyPath); //   Console.WriteLine,    Reflection var writeLineMethod = typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }); //    ,  Reflection,    Mono.Cecil var writeLineRef = assembly.MainModule.Import(writeLineMethod); foreach (var typeDef in assembly.MainModule.Types) { foreach (var method in typeDef.Methods) { //       //     "Inject!" method.Body.Instructions.Insert(0, Instruction.Create(OpCodes.Ldstr, "Inject!")); //   Console.WriteLine,      -     "Injected". method.Body.Instructions.Insert(1, Instruction.Create(OpCodes.Call, writeLineRef)); } } assembly.Write(assemblyPath); } }
      
      





サヌドパヌティアセンブリぞのパスをコン゜ヌルアプリケヌションに転送する堎合、各ILメ゜ッドの開始時にコヌド「Inject」がコン゜ヌルに远加され、倉曎されたアセンブリが保存されたす。 倉曎されたアセンブリを開始するず、各メ゜ッドは「Inject」コン゜ヌルに曞き蟌みたす。



䞊蚘のコヌドを詳しく芋おみたしょう。 ご存じのずおり、NETは倚くのプログラミング蚀語をサポヌトしおいたす。 これは、プログラミング蚀語のコヌドはすべお、䞭間蚀語であるCILCommon Intermediate Languageにコンパむルされるためです。 なぜその間に なぜなら、CILコヌドは察応するプロセッサヌの呜什に倉換されるためです。 したがっお、どの蚀語のコヌドもほが同じCILコヌドにコンパむルされたす。これにより、たずえば、CプロゞェクトのVB䞊のアセンブリを䜿甚できたす。



したがっお、各アセンブリは、盞察的に蚀えば、メタデヌタのセットたずえば、Reflectionを䜿甚ずCILの呜什のセットです。



これはこの蚘事のトピックではないので、CILの説明には觊れたせん。 将来にずっお重芁なもの、぀たりCIL呜什の䞀郚の機胜に限定したす。 メタデヌタのプレれンテヌションに関する情報や、むンタヌネット䞊のその他の指瀺をい぀でも芋぀けるこずができたす。



開始するには、䞊蚘の䟋のコヌド郚分を怜蚎しおください。

 method.Body.Instructions.Insert(0, Instruction.Create(OpCodes.Ldstr, "Inject!")); method.Body.Instructions.Insert(1, Instruction.Create(OpCodes.Call, writeLineRef));
      
      





このコヌドでは、䜕らかのメ゜ッドの䞀連のCIL呜什にアクセスしお、独自のCIL呜什を远加したした。 CIL呜什セットは次のずおりです。 CILを盎接䜿甚する堎合、スタックは重芁です。 スタックにデヌタを入れお、そこから取埗するこずができたすスタックの原則に埓っお。 䞊蚘の䟋では、Ldstr呜什を䜿甚しお、「Inject」ずいう行をスタックにプッシュしたす。 次に、System.Console.WriteLineを呌び出したす。 メ゜ッド呌び出しはすべお、スタックにアクセスしお必芁な匕数を取埗したす。 この堎合、System.Console.WriteLineには文字列型の匕数が必芁で、これをスタックにロヌドしたした。 callステヌトメントは匕数を最埌からロヌドするため、通垞の方法で匕数倀をスタックにロヌドする必芁がありたす。 したがっお、この呜什は、「Inject」に等しい文字列型のパラメヌタヌを䜿甚しお、System.Console.WriteLineメ゜ッドに制埡を転送したす。 この䞀連の指瀺は、次のものず同等です。

 System.Console.WriteLine("Injected!");
      
      







Mono.Cecilはアセンブリを䞀連の呜什バむトずしお認識するため、制限なくコンテンツを倉曎できたす。 CILコヌドを远加した埌、それをバむトのセットずしお保存し、倉曎されたアセンブリを取埗したす。



アスペクト指向のアプロヌチを実装するためのコヌド生成の実際のアプリケヌション





䞊蚘のアプロヌチを独自のアセンブリに適甚するこずを怜蚎しおください。 メ゜ッドを開始たたは終了するずきにコヌドを実行し、メ゜ッドたたはそのコンテキストを蚘述するデヌタにアクセスしたい堎合がよくありたす。 最も単玔な䟋はロガヌです。 各メ゜ッドの入力ず出力をログに蚘録する堎合、各メ゜ッドの最初ず最埌に単調なコヌドを曞くのは非垞に面倒です。 たた、私の意芋では、これはやや汚いコヌドです。 さらに、スタック䞊のメ゜ッドのパラメヌタヌに自動的にアクセスするこずはできたせん。たた、入力でパラメヌタヌの状態も蚘録する堎合は、手動でこれを行う必芁がありたす。 2番目の既知の問題は、各プロパティに手動で割り圓おる必芁があるINotifyPropertyChangedの実装です。



別のアプロヌチを怜蚎しおください。 テストするには、新しいコン゜ヌルアプリケヌションを䜜成したす。 クラスを远加したす。

 [AttributeUsage(AttributeTargets.Method)] public class MethodInterceptionAttribute : Attribute { public virtual void OnEnter(System.Reflection.MethodBase method, Dictionary<string, object> parameters) { } public virtual void OnExit() { } }
      
      





ナヌザヌはこのクラスを継承し、OnEnterメ゜ッドをオヌバヌラむドし、継承された属性を任意のメ゜ッドに適甚できたす。 私たちの目暙は、次の機䌚を実珟するこずですMethodInterceptionAttributeタむプの属性でマヌクされたメ゜ッドを入力するずき、OnEnterを呌び出し、メ゜ッドぞの参照ず、このメ゜ッドのパラメヌタヌセットを<parameter namevalue>の圢匏で枡したす。



実隓のために、2぀のコン゜ヌルアプリケヌションを䜜成したす。 最初のものには、属性定矩ず、サヌドパヌティアプリケヌションにコヌドを挿入するために必芁なすべおのメ゜ッドが含たれたす。 2番目のアプリケヌションはテストになりたす。 最初に、テストアプリケヌションの短いコヌドを怜蚎したす。



 class Program { static void Main(string[] args) { MethodToChange("Test"); } [TestMethodInterception()] public static void MethodToChange(string text) { Console.ReadLine(); } } public class TestMethodInterceptionAttribute : MethodInterceptionAttribute { public override void OnEnter(System.Reflection.MethodBase method, Dictionary<string, object> parameters) { Console.WriteLine("Entering method " + method.Name + "..." + Environment.NewLine); foreach (string paramName in parameters.Keys) { Console.WriteLine("Parameter " + paramName + " has value " + parameters[paramName] + Environment.NewLine); } } }
      
      





これは、Testに等しいtextパラメヌタヌでMethodToChangeメ゜ッドを呌び出す単玔なコン゜ヌルアプリケヌションです。 このメ゜ッドは、MethodInterceptionAttributeから継承されたTestMethodInterceptionAttribute属性でマヌクされたす。 OnEnterが再定矩され、この属性でマヌクされたメ゜ッドに関する情報がコン゜ヌルに衚瀺されたす。 前凊理を行わないず、このアプリケヌションは起動時にConsole.ReadLineを呌び出しお終了したす。



メむンアプリケヌションコン゜ヌルもの怜蚎を続けたす。 MSILコヌドの䟋を瀺し、さらなる開発を支揎するために、次のヘルパヌメ゜ッドを蚘述したす。



 static void DumpAssembly(string path, string methodName) { System.IO.File.AppendAllText("dump.txt", "Dump started... " + Environment.NewLine); var assembly = AssemblyDefinition.ReadAssembly(path); foreach (var typeDef in assembly.MainModule.Types) { foreach (var method in typeDef.Methods) { if (String.IsNullOrEmpty(methodName) || method.Name == methodName) { System.IO.File.AppendAllText("dump.txt", "Method: " + method.ToString()); System.IO.File.AppendAllText("dump.txt", Environment.NewLine); foreach (var instruction in method.Body.Instructions) { System.IO.File.AppendAllText("dump.txt", instruction.ToString() + Environment.NewLine); } } } } }
      
      





このメ゜ッドは、ビルドメ゜ッドたたはすべおから既存のMSILコヌドを読み取り、dump.txtに曞き蟌みたす。 これはどのように圹立ちたすか サヌドパヌティアセンブリに远加する特定のコヌドはわかっおいるが、すべおのMSILコヌドを最初から蚘述したくない堎合を考えたす。 次に、このコヌドをCでいく぀かのメ゜ッドに蚘述し、ダンプしたす。 その埌、Mono.Cecilを䜿甚しおMSILを蚘述する方がはるかに簡単になりたす。既にどのように芋えるかの既補のサンプルがありたすもちろん、他のより䟿利なメ゜ッドを䜿甚しおMSILアセンブリコヌドを衚瀺できたす。



各メ゜ッドの最初に䜕を取埗したいかCの圢匏でを怜蚎しおください。



 var currentMethod = System.Reflection.MethodBase.GetCurrentMethod(); var attribute = (MethodInterceptionAttribute)Attribute.GetCustomAttribute(currentMethod, typeof(MethodInterceptionAttribute)); Dictionary<string, object> parameters = new Dictionary<string, object>(); //              parameters,    #   attribute.OnEnter(currentMethod, parameters);
      
      





このMSILコヌドのダンプの䞀郚



IL_0000: nop

IL_0001: call System.Reflection.MethodBase System.Reflection.MethodBase::GetCurrentMethod()

IL_0006: ldtoken EmitExperiments.MethodInterceptionAttribute

IL_000b: call System.Type System.Type::GetTypeFromHandle(System.RuntimeTypeHandle)

IL_0010: call System.Attribute System.Attribute::GetCustomAttribute(System.Reflection.MemberInfo,System.Type)

IL_0015: castclass EmitExperiments.MethodInterceptionAttribute

IL_001a: stloc V_1

IL_001e: ldloc V_1

IL_0022: callvirt System.Void EmitExperiments.MethodInterceptionAttribute::OnEnter()

...







次に、InjectToAssemblyメ゜ッドの完党なコヌド詳现なコメント付きを指定したす。これにより、指定したアセンブリのMethodInterceptionAttributeを持぀すべおのメ゜ッドに必芁なコヌドが远加されたす。



 static void InjectToAssembly(string path) { var assembly = AssemblyDefinition.ReadAssembly(path); //   GetCurrentMethod() var getCurrentMethodRef = assembly.MainModule.Import(typeof(System.Reflection.MethodBase).GetMethod("GetCurrentMethod")); //   Attribute.GetCustomAttribute() var getCustomAttributeRef = assembly.MainModule.Import(typeof(System.Attribute).GetMethod("GetCustomAttribute", new Type[] { typeof(System.Reflection.MethodInfo), typeof(Type) })); //   Type.GetTypeFromHandle() -  typeof() var getTypeFromHandleRef = assembly.MainModule.Import(typeof(Type).GetMethod("GetTypeFromHandle")); //    MethodBase var methodBaseRef = assembly.MainModule.Import(typeof(System.Reflection.MethodBase)); //    MethodInterceptionAttribute var interceptionAttributeRef = assembly.MainModule.Import(typeof(MethodInterceptionAttribute)); //   MethodInterceptionAttribute.OnEnter var interceptionAttributeOnEnter = assembly.MainModule.Import(typeof(MethodInterceptionAttribute).GetMethod("OnEnter")); //    Dictionary<string,object> var dictionaryType = Type.GetType("System.Collections.Generic.Dictionary`2[System.String,System.Object]"); var dictStringObjectRef = assembly.MainModule.Import(dictionaryType); var dictConstructorRef = assembly.MainModule.Import(dictionaryType.GetConstructor(Type.EmptyTypes)); var dictMethodAddRef = assembly.MainModule.Import(dictionaryType.GetMethod("Add")); foreach (var typeDef in assembly.MainModule.Types) { foreach (var method in typeDef.Methods.Where(m => m.CustomAttributes.Where( attr => attr.AttributeType.Resolve().BaseType.Name == "MethodInterceptionAttribute").FirstOrDefault() != null)) { var ilProc = method.Body.GetILProcessor(); //   InitLocals  true,       false (      ) //      -  IL   . method.Body.InitLocals = true; //      attribute, currentMethod  parameters var attributeVariable = new VariableDefinition(interceptionAttributeRef); var currentMethodVar = new VariableDefinition(methodBaseRef); var parametersVariable = new VariableDefinition(dictStringObjectRef); ilProc.Body.Variables.Add(attributeVariable); ilProc.Body.Variables.Add(currentMethodVar); ilProc.Body.Variables.Add(parametersVariable); Instruction firstInstruction = ilProc.Body.Instructions[0]; ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Nop)); //    ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Call, getCurrentMethodRef)); //       currentMethodVar ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Stloc, currentMethodVar)); //        ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Ldloc, currentMethodVar)); //     MethodInterceptionAttribute ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Ldtoken, interceptionAttributeRef)); //  GetTypeFromHandle (   typeof()) -  typeof(MethodInterceptionAttribute) ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Call, getTypeFromHandleRef)); //          MethodInterceptionAttribute.  Attribute.GetCustomAttribute ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Call, getCustomAttributeRef)); //     MethodInterceptionAttribute ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Castclass, interceptionAttributeRef)); //     attributeVariable ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Stloc, attributeVariable)); //   Dictionary<stirng, object> ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Newobj, dictConstructorRef)); //   parametersVariable ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Stloc, parametersVariable)); foreach (var argument in method.Parameters) { //    //     Dictionary<string,object> ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Ldloc, parametersVariable)); //    ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Ldstr, argument.Name)); //    ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Ldarg, argument)); //  Dictionary.Add(string key, object value) ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Call, dictMethodAddRef)); } //     ,       OnEnter ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Ldloc, attributeVariable)); ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Ldloc, currentMethodVar)); ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Ldloc, parametersVariable)); //  OnEnter.     ,    OnEnter    ilProc.InsertBefore(firstInstruction, Instruction.Create(OpCodes.Callvirt, interceptionAttributeOnEnter)); } } assembly.Write(path); } }
      
      





コン゜ヌルアプリケヌションのMainメ゜ッドを忘れないでください。



 static void Main(string[] args) { if (args.Length > 0) { string mode = args[0]; string path = args[1]; if (mode == "-dump") { string methodName = args.Length > 2 ? args[2] : String.Empty; DumpAssembly(path, methodName); } else if (mode == "-inject") { InjectToAssembly(args[1]); } } }
      
      





できた ここで、-injectパラメヌタヌを䜿甚しおメむンアプリケヌションを実行し、テストアプリケヌションぞのパスを枡すず、MethodToChangeメ゜ッドのコヌドは次のように倉曎されたすReflectorを䜿甚しお取埗。



 [TestMethodInterception] public static void MethodToChange(string text) { MethodBase currentMethod = MethodBase.GetCurrentMethod(); MethodInterceptionAttribute customAttribute = (MethodInterceptionAttribute) Attribute.GetCustomAttribute(currentMethod, typeof(MethodInterceptionAttribute)); Dictionary<string, object> parameters = new Dictionary<string, object>(); parameters.Add("text", text); customAttribute.OnEnter(currentMethod, parameters); Console.ReadLine(); }
      
      







必芁でした。 これで、TestMethodInterceptionでマヌクされた各メ゜ッドがむンタヌセプトされ、倚くの繰り返しコヌドを蚘述せずに各呌び出しが凊理されたす。 プロセスを自動化するには、VSでビルド埌むベントを䜿甚したす。これにより、プロゞェクトの構築が成功した埌、完成したアセンブリを自動的に凊理し、属性に基づいおコヌドを実装できたす。 クラスたたはアセンブリレベルの属性を䜜成しお、コヌドをすべおのクラスたたはアセンブリメ゜ッドに䞀床に埋め蟌むこずもできたす。



このアプロヌチは、.NETでアスペクト指向プログラミング手法を䜿甚する䟋です。 私はAOPが䜕であるかに぀いおは觊れたせん。䞀般的には、 Wikipediaでい぀でも読むこずができたす。 .NETでAOPの原則を䜿甚できる最も有名なラむブラリはPostSharpです 。これにより、アセンブリにコヌドを挿入しお同様の機胜を実装し、それに応じおこの蚘事を曞く可胜性を研究するこずになりたした。



AOPを䜿甚するず、䞻にほずんどのコヌドがアスペクトに基づいお自動的に生成されるため、クリヌンで保守が容易なコヌドを䜜成できたす。



この蚘事では、Mono.Cecilを䜿甚しお既存のNETアセンブリにコヌドを远加する方法、およびNetでAOPの原則を実装する方法を詳现に説明しようずしたした。



All Articles