.Netで生成

.Netコードを生成するには、いくつかの方法があります。



この記事では、Reflection Emitを使用したコード生成手法について説明します。



生成方法についてもう少し



1つ目は、.Net仮想マシン用のCILコード(MSILまたは単にILとも呼ばれる)の直接生成です。 この場合、生成されたコードはCILで記述されます。これは、外観上はステロイドのアセンブラーに似ています。 出力では、動的なクラスとメソッド、または「裸の」動的メソッドを持つ動的アセンブリ(ディスクに保存する可能性がある)を取得します。 次に、裁量で生成された商品を使用します。

2つ目は、高レベル言語(たとえば、C#またはVB)でのソースコードの生成と、その後のCILでのソースのコンパイルです。 出力は、対応するコンパイラによって作成されたアセンブリです。

3番目は、式ツリービューからの生成です。 Expressionメソッドを使用して一部のASDを記述し、同じExpressionが記述されたメソッドを生成します。 内部的に、Expressionはその表現をすぐにCILコードに変換し、説明されたASD(AST)の有用な検証を生成します。 ただし、Expression Treeの機能には制限があります。型とアセンブリを生成して、ディスクに保存することはできません。



CILを生成します



なぜ高レベル言語ではなくCILであるのですか? 高レベル言語での生成では、この言語でソースを作成し、CILにコンパイルするため、CILでの生成はより効率的です。 さらに、上位言語での生成には、外部プロセス(コンパイラー)の関与が必要です。 そしてまだ-これは.NETプログラマーのアセンブラーに似た何かをいじるまれな機会です。 ただし、高レベル言語での生成には利点があります。CILを扱う必要がなく、使い慣れた言語でコードを生成します。 さらに、このようなコード生成のソースは、常にログに保存またはダンプし、目で検証するか、IDEに挿入してデバッグすることもできます。



それはどのように見えますか



Reflection Emitを使用してコードを生成するには、アセンブラーの最小限の理解が必要です。 CILアセンブラにはレジスタ、オフセット、またはoffsetなアドレス指定はありません。 何がありますか? 計算のスタックがあり、すべての操作はそれでのみ動作し、レジスタはありません。 同時に、コンピューティングスタックには理由があるため、ローカル変数とメソッド引数は含まれません。CILの場合、これらは別個の概念です。 まだ操作があります。 それらは、通常のアセンブラ(さまざまな種類の遷移、数学演算、メソッド呼び出しなど)または特別なCLR(Box / Unbox、Newobj、Isinstなど)の2種類です。 ただし、分離は純粋に形式的なものです。



十分な言葉、生成を始めましょう



100回聞くよりも1回見る方がよく、さらにはリースする方が良いです。 言葉が十分にあるのは私です。例を見てみましょう。

タスクをこれにしましょう:いくつかのエンティティのトランスフォーマーを他のものに生成します。 つまり 基本的には同じですが、プロパティ名が異なるクラスがあります。 例:

public class TestSrc { public int SomeID { get; set; } } public class TestTarg { public double SomeOtherID { get; set; } }
      
      





TestSrcをTestTargに変換する必要があります。 コンバーターを次のようにします。

 class Mapper<TIn, TOut> { protected delegate TOut MapMethod(TIn src); public TOut Map(TIn source) {...} private MapMethod GenerateMapMethod(IDictionary<string, string> mapping) {...} }
      
      





Mapメソッドは、GenerateMapMethodを呼び出して最初の呼び出しで変換メソッドを生成し、後続の呼び出しで既に生成されたメソッドを使用します。 GenerateMapMethodの入力に渡すマッピングは、エンティティ内のフィールドの対応です(キーはTIn型のプロパティの名前、値はTOut型のプロパティの名前です)。



動的アセンブリ


まず、選択を行う必要があります。生成されたコードはどこに配置されますか? 動的アセンブリまたは動的メソッドの2つのオプションがあります。 それと、もう1つが「オンザフライ」で作成されます。

動的アセンブリは完全なソリューションであり、任意のメソッドセットを使用して実際のクラスと構造を生成できます。 動的アセンブリのもう1つの利点は、生成したものを後で使用したり分析したりできるように保存する機能です。 これは難しいケースです。

したがって、クラスと静的メソッドを使用してアセンブリを作成します。

 protected MapMethod GenerateMapMethod(IDictionary<string, string> mapping) { var dynGeneratorHostAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly( new AssemblyName("Test.Gen, Version=1.0.0.1"), AssemblyBuilderAccess.RunAndSave); var dynModule = dynGeneratorHostAssembly.DefineDynamicModule( "Test.Gen.Mod", "generated.dll"); var dynType = dynModule.DefineType( "Test.MapperOne", TypeAttributes.Abstract | TypeAttributes.Sealed | TypeAttributes.Public); var dynMethod = dynType.DefineMethod( "callme", MethodAttributes.Static, typeof(TOut), new Type[] { typeof(TIn) }); var prm = dynMethod.DefineParameter(1, ParameterAttributes.None, "source"); GenerateMapMethodBody(dynMethod.GetILGenerator(), prm, mapping); var finalType = dynType.CreateType(); dynGeneratorHostAssembly.Save("generatedasm.dll"); var realMethodInfo = finalType.GetMethod(dynMethod.Name); var methodToken = dynMethod.GetToken().Token; var methodInfo = dynModule.ResolveMethod(methodToken); return (MapMethod)Delegate.CreateDelegate( typeof(MapMethod), (MethodInfo)methodInfo); }
      
      





このコードは何をしますか? はい、一般的に、書かれているのはそれが行うことです-現在のドメインで動的アセンブリを定義し(別のドメインを作成できます)、アセンブリの使用方法を示します:実行のみ、保存のみ、またはすべて(AssemblyBuilderAccess列挙によって決定されます)。 AssemblyBuilderAccess.RunAndSaveを指定した場合に大きなオーバーヘッドが発生するかどうかは不明であり、アセンブリを保存する必要はありません。 .Net 4では、ページ化された動的アセンブリ(AssemblyBuilderAccess.RunAndCollect)を作成する機能が登場しました。 アセンブリをアンロードするには、このアセンブリの型インスタンスと型自体を参照しないでください。詳細はこちらを参照してください。

次に、アセンブリ内のモジュールを決定します。 アセンブリはモジュールで構成されていることを覚えています。多くの場合、1つのアセンブリは1つのモジュールですが、マルチモジュールアセンブリもあります。 モジュールは物理ファイルに対応するため、モジュールを定義するときに、そのファイル名を指定します。

モジュールでは、型を決定します-クラスまたは構造体にすることができます。 単純なDefineType呼び出し( "Test.MapperOne")は、Test名前空間にプライベートMapperOneクラスを作成します。 生成されたクラスとメソッドを名前で参照する必要がないかもしれないという事実にもかかわらず、きちんとした名前と名前空間を与える方が良いでしょう。なぜなら、それらは最初にトレースのスタックに現れ、次に分析するとリフレクターを使用して生成された構造は、より明確で快適になります。 「やめて!」と、熱心な読者が言うでしょう。 結局のところ、取得するクラスはプライベートであり、別のアセンブリであっても、それを使用できますか? 実際、できます。 ただし、すべてを厳密に正確にしたい場合は、次のように記述します。

 var dynType = dynModule.DefineType("Test.MapperOne", TypeAttributes.Abstract | TypeAttributes.Sealed | TypeAttributes.Public);
      
      





そして最後に、型で、返される値の型とメソッドへの入力引数の型を指定することでメソッドを定義します。

次に、生成されたメソッドに意味を入力する必要があります。 このプロセスについては後で詳しく検討するため、GenerateMapMethodBody(dynMethod.GetILGenerator()、prm、マッピング)の呼び出しをスキップして、さらに詳しく見ていきます。

型のすべてのメソッドが生成された後、dynType.CreateType()メソッドを呼び出して型を作成する必要があります。 その後、その型を使用した動的な操作はできなくなります。 しかし、これでタイプの使用準備が整いました。 CreateTypeを呼び出す前は、CLRは型とそのメソッドについて何も知りません。 DefineDynamicAssembly呼び出しの直後にドメインに表示されるアセンブリ、およびDefineDynamicModule呼び出しの直後にアセンブリに表示されるモジュールとは異なります。

興味深い点の1つは、DefineTypeメソッドを使用して動的な型を定義すると、TypeBuilderが返されたことです。 TypeBuilderを見ると、TypeBuilderから継承されますが、TypeがTypeBuilderの場合、すべてのTypeメソッドにアクセスできるわけではありません。 あなたがそれについて考えるなら、これは論理的です、なぜならそのようなタイプはまだないからです。 一部のTypeプロパティは、常にNotSupportedExceptionを返すようにオーバーライドされます。 一部のメソッドは、CreateTypeを呼び出す前に例外をスローしてから、対応するRuntimeTypeへの呼び出しのリダイレクトを開始します。 同様の状況は、MethodInfoから継承されるMethodBuilderクラスの場合です。 MethodBuilderは、すべてのプロパティとメソッドも実装していません。 別の状況は、たとえば、Delegate.CreateDelegateが2番目の引数としてMethodInfoをとるという事実によって複雑になりますが、そこにMethodBuilderを渡そうとすると、(CreateTypeを呼び出した後でも)例外が発生します。 だから注意してください。



動的な方法


ただし、アセンブリは必要なく、独自の型は必要ないと仮定し、非常に小さなメソッドを生成したいだけです。 その後、より適切な「経済」提案-動的な方法になります。 前のセクションの大量のコードの代わりに、次のように記述します。

 protected MapMethod GenerateMapMethod2(IDictionary<string, string> mapping) { var dynMethod = new DynamicMethod("callme", typeof(TOut), new Type[] { typeof(TIn) }); var prm = dynMethod.DefineParameter(1, ParameterAttributes.None, "source"); GenerateMapMethodBody(dynMethod.GetILGenerator(), prm, mapping); return (MapMethod)dynMethod.CreateDelegate(typeof(MapMethod)); }
      
      





彼らはメソッドを作成し、それを意味で満たし、デリゲートを返しました。 できた ドキュメントには、このようなメソッドには動的アセンブリ、モジュール、および型は必要ないと書かれていますが、リフレクションまたはProcessExplorerを使用すると、動的アセンブリが作成されていることがわかります(すべての動的ドメインメソッドに1つ)。 そして、その中にマニフェストモジュールさえありますが、私たちのメソッドを(リフレクションによって)見つけることができませんでした。 それにもかかわらず、彼は働いています。 動的メソッドとその生成に割り当てられたすべてのメモリは、誰も参照しなくなった後に解放できます。 したがって、この方法は少し速くて経済的です。 この場合、メソッドには匿名ホスト(匿名ホスト)を使用しますが、既存のモジュールまたはクラスにメソッドを「固定」するオプションがあります。 これには、モジュールまたは型を受け入れる特別なコンストラクターがあり、それに動的メソッドを追加します。 モジュールの場合、タイプはモジュールに対してグローバルになり、内部タイプを含むすべてのタイプのモジュールにアクセスできます。 クラスの場合、クラスのすべての内部フィールドにアクセスできます。 しかし、「匿名」ホスティングを使用している場合でも、動的メソッドから内部クラスやこれらのクラスの内部フィールドにアクセスする機会があります。 これを行うには、skipVisibilityパラメーターを指定したコンストラクターを使用し、このパラメーターをtrueに設定します(このパラメーターは、JIT検証がスキップされ、CAS検証と混同しないことを示します)。 ところで、「匿名」ホスティングを使用する機能は.Net 3.0でのみ登場しました。



メソッド本体


そして今、私たちは最も興味深い部分に来ます-どのようにコードを生成しますか? この例では、コードはGenerateMapMethodBodyメソッド(dynMethod.GetILGenerator()、prm、マッピング)を生成します。 ILGeneratorをこのメソッドに渡します。パラメーターとマッピングは、あるクラスのフィールドと別のクラスのフィールドの対応です。 ILGeneratorクラスを使用すると、生成されたメソッドの本体にCILコマンドを埋め込むことができます。 彼はこれをEmitメソッドで行います。 ILGeneratorでは、DefineLabelメソッドを使用して遷移のラベルを作成し(条件構造を整理するため)、DefineLocalメソッドを使用してローカル変数を宣言し、例外のブロックを作成することもできます。 後者の場合、BeginCatchBlock、BeginExceptFilterBlockなどの形式のメソッドセット全体が使用されます。 CILのほとんどのコマンドは、計算スタック(評価スタック、以下では簡潔にするためにスタックのみ)で動作します。 CLRは、いずれにしても、スタックの境界を越えないようにします。 スタックがオーバーフローした場合、StackOverflowExceptionが発生します。空のスタックから値を取得しようとした場合、またはメソッドによってそこに置かれなかった値を取得しようとした場合(つまり、メソッドはスタックの「その」部分のみを認識します)、InvalidProgramExceptionが発生します。 メソッドに渡される引数はスタック上にありません。 それらを使用するには、OpCodes.Ldargチームを使用する必要があります。 したがって、メソッドの開始時には、スタックは空のようになります。 メソッドの実行後は空になります。そうでない場合は、InvalidProgramExceptionが再び発生します。 そして、これはCILコード生成のマイナス面の1つです。ここで高級言語をコンパイルする段階でキャッチできるエラーは、実行時にのみ表示されます。たとえば、変数の入力や初期化に関連するエラーです。



ILコードを生成するための便利な手法は、高レベル言語で作成し、生成したいサンプルをコンパイルし、コンパイルしてから(コンパイル前にReleaseに切り替え、最適なコードからサンプルを取ることを忘れないでください)、CILで目的のコードのテンプレートがどのように見えるかを確認することです。 このようなテンプレートコードを反射板で見ると便利です。 さらに、特別なReflectionEmitLanguageプラグインもあります。 このプラグインは、リフレクターで表示されるメソッドまたはタイプのコードではなく、表示されるコードを生成するコードを表示します。 リフレクターがない場合は、.Net SDKのIL Disassembler(ildasm.exe)を使用してテンプレートを表示できます。 メソッドの構成要素である正直なCILが表示されます。 次に、テンプレートをニーズに合わせて調整します。これで完了です。 同じメソッドを使用して、たとえば、シールクラスまたは内部仮想メソッドを作成するために、メソッドまたはそのクラスに追加する必要がある修飾子を見つけることができます。



どこかでクラス間のプロパティの対応を知っているとすると、テンプレートは次のようになります。

  public static TestTarg GenerateTemplate(TestSrc src) { var result = new TestTarg(); result.SomeOtherID = (double)src.SomeID; return result; }
      
      





コードをコンパイルし、IL Disassemblerでそれを見て、以下を確認します。

 .method private hidebysig static class ConsoleApplication1.TestTarg GenerateTemplate(class ConsoleApplication1.TestSrc src) cil managed { // Code size 21 (0x15) -  ,   .maxstack 2 // -  CLR   //     .locals init ([0] class ConsoleApplication1.TestTarg result) //     (  +  ) //           IL_0000: newobj instance void ConsoleApplication1.TestTarg::.ctor() //        0 IL_0005: stloc.0 //        0 IL_0006: ldloc.0 //        0 IL_0007: ldarg.0 //  (  ),             .     . IL_0008: callvirt instance int32 ConsoleApplication1.TestSrc::get_SomeID() //  int  double IL_000d: conv.r8 //   ,            ( int) IL_000e: callvirt instance void ConsoleApplication1.TestTarg::set_SomeOtherID(float64) //    IL_0013: ldloc.0 //    IL_0014: ret } // end of method Program::GenerateTemplate
      
      





このテンプレートコードを見て、各操作をILGenerator.Emit()への呼び出しの形式で転送します。次に例を示します。

 //IL_0000: newobj instance void ConsoleApplication1.TestTarg::.ctor() //  var tTarg = typeof(TOut); var targetCtor = tTarg.GetConstructor(new Type[0]); gen.Emit(OpCodes.Newobj, targetCtor); ... //IL_0006: ldloc.0 gen.Emit(OpCodes.Ldloc, locResult); ... //IL_0008: callvirt instance int32 ConsoleApplication1.TestSrc::get_SomeID() var methodSrc = tSrc.GetProperty(methodMap.Key).GetGetMethod(); gen.Emit(OpCodes.Callvirt, methodSrc); ... //IL_0014: ret gen.Emit(OpCodes.Ret);
      
      





Reflection.Emitプラグインでリフレクターを使用すると、すべてがさらに簡単になり、必要なILGenerator.Emit()の呼び出しが表示されます。

これがテンプレート用のプラグインです。
 public MethodBuilder BuildMethodGenerateTemplate(TypeBuilder type) { // Declaring method builder // Method attributes System.Reflection.MethodAttributes methodAttributes = System.Reflection.MethodAttributes.Private | System.Reflection.MethodAttributes.HideBySig | System.Reflection.MethodAttributes.Static; MethodBuilder method = type.DefineMethod("GenerateTemplate", methodAttributes); // Preparing Reflection instances MethodInfo method1 = typeof(TestSrc).GetMethod(/*  */); MethodInfo method2 = typeof(TestTarg).GetMethod(/**/); // Setting return type method.SetReturnType(typeof(TestTarg)); // Adding parameters method.SetParameters( typeof(TestSrc) ); // Parameter src ParameterBuilder src = method.DefineParameter(1, ParameterAttributes.None, "src"); ILGenerator gen = method.GetILGenerator(); // Preparing locals LocalBuilder result = gen.DeclareLocal(typeof(TestTarg)); // Writing body gen.Emit(OpCodes.Ldloca_S,0); gen.Emit(OpCodes.Initobj,TestTarg); gen.Emit(OpCodes.Ldloca_S,0); gen.Emit(OpCodes.Ldarg_0); gen.Emit(OpCodes.Callvirt,method1); gen.Emit(OpCodes.Conv_R8); gen.Emit(OpCodes.Call,method2); gen.Emit(OpCodes.Ldloc_0); gen.Emit(OpCodes.Ret); // finished return method; }
      
      





各操作のヘルプはmsdnで表示でき、OpCodesクラスにはすべての操作の定義が含まれています。 「額」ではなく、いくつかのコマンドを転送することをお勧めします。 たとえば、stloc.0などのコマンドは、混乱しないように、次のように記述しない方が良いでしょう。

 gen.DeclareLocal(yourType); gen.Emit(OpCodes.Ldloc_0);
      
      





など:

 var locResult = gen.DeclareLocal(yourType); gen.Emit(OpCodes.Ldloc, localVar);
      
      





メソッドのパラメーターでも同じことができます。

C#で同じように見える一部の構造体は、CILで異なることに注意してください。 例:

 var c = new RefType(); // reference  var a = new RefType[0]; //  var s = new ValType(); // value  // reference  var targetCtor = typeof(RefType).GetConstructor(new Type[0]); gen.Emit(OpCodes.Newobj, targetCtor); //  gen.Emit(OpCodes.Newarr, typeof(RefType)); // value  LocalBuilder loc = gen.DeclareLocal(typeof(ValType)); gen.Emit(OpCodes.Ldloca, loc); gen.Emit(OpCodes.Initobj, typeof(ValType));
      
      





refパラメーターにも注意を喚起したいと思います。 なぜなら refパラメーターの場合、パラメーターには値が含まれませんが、リンクが含まれる場合、CILレベルで異なる方法で作業する必要があります。 これは一種の間接アドレス指定です。 これが参照型のrefパラメーターである場合、パラメーターにはリンクへのリンクが含まれ、単純なldargコマンドはオブジェクトへのリンクではなく、リンクへのリンクをスタックに配置します(2つの松の木で迷子になる大きなチャンス)。 スタック上のオブジェクト参照を取得するには、さらにldind.refを呼び出す必要があります。

これが値型のrefパラメーター(ただし構造体ではない)である場合、パラメーターには値への参照が含まれます。 また、スタックに値を設定するには、ldargまたはstargコマンドではなく、ldindまたはstindを使用する必要があります。

構造を使用した同様の状況(まったく逆)。 構造タイプのパラメーターまたは変数がある場合、それにアクセスするには、最初に構造のアドレスをスタックに配置する必要があります。 これにはldargaコマンドがあります。



おわりに



なぞなぞ:正規表現とCIL生成の共通点は何ですか? 回答:非常に難しいリバースエンジニアリング。 したがって、生成しているコードがわかりやすいように、生成コードについてコメントするのを怠らないでください。 さて、または、たとえば、メソッドの生成では、コメントは通常よりも80%多いはずです。 通常、それらをまったく書かない場合は、始めましょう。



CIL生成のトピックで議論できる質問はおそらく他にもたくさんありますが、この記事はすでに話題になっているように思えます。 みなさん、幸運を祈ります。すぐに会いましょう。



便利なリンク:



UPD:

コメントの賢明な人々は、それがあなたが生成する困難なタスクであなたを助けることができることを示唆します:



私は彼らと仕事をしておらず、彼らについて何も言えません。



PS

すべてのプログラマは、コンパイラ、コードジェネレータ、およびストアをPehPaEで作成する必要があります。

(トピックではなく、別のオプションですが、私はそれが好きでした。すべてのプログラマーは、Linuxカーネルを構築し、データベースをテラバイトに拡張し、フローティングバグを入れる必要があります)。



All Articles