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の原則を実装する方法を詳細に説明しようとしました。

Source: https://habr.com/ru/post/J108947/


All Articles