Mono.Cecil:独自の「コンパイラ」を作成

自転車の発明に夢中になっているプログラマーにとって最も豪華なトピックの1つは、独自の言語、インタープリター、コンパイラーを書くことです。 確かに、他のプログラムを作成または実行できるプログラムは、コード作成者の心にa敬の念を抱かせます。なぜなら、それは複雑で、ボリュームがありますが、とてつもなくエキサイティングだからです。



ほとんどは独自のインタープリターで始まります。これは一般にループ内のコマンドの巨大なスイッチです。 面白い、リラックスした、しかし退屈で非常に遅い。 私はJITにもっと機敏な何かが上手くいきたいと思っています。



この問題の優れた解決策は、ターゲットプラットフォームとして.NETを選択することです。 次回は字句解析を終了しましょう。今日は、実行可能な実行可能ファイルを作成する簡単なプログラムを作成してみましょう。









プログラムには名前とコンソールへの出力Hello、%username%が必要です。



たとえば、実行可能ファイルを作成する方法は多数あります。



最後に選んだオプション。 残念ながら、このタスクでどのようにCecilがReflectionより優れているのかわかりませんが、Cecilの例を見つけたので、正確に分析します。



Mono.Cecilは、バイトの配列としてアセンブリを操作できるライブラリです。 その助けを借りて、独自のアセンブリを作成し、既存のアセンブリを選択して変更することができます。 (通常)使用するのに便利な幅広いクラスを提供します。



会話の件名



ここで、実際には、完成したコード(クラス、フォーム、および実際のジェネレーターメソッドを除く他のすべての説明なし):



using Mono.Cecil; using Mono.Cecil.Cil; public void Compile(string str) { //      ,   :   var name = new AssemblyNameDefinition("SuperGreeterBinary", new Version(1, 0, 0, 0)); var asm = AssemblyDefinition.CreateAssembly(name, "greeter.exe", ModuleKind.Console); //     string  void asm.MainModule.Import(typeof(String)); var void_import = asm.MainModule.Import(typeof(void)); //   Main, , ,  void var method = new MethodDefinition("Main", MethodAttributes.Static | MethodAttributes.Private | MethodAttributes.HideBySig, void_import); //       var ip = method.Body.GetILProcessor(); //  ! ip.Emit(OpCodes.Ldstr, "Hello, "); ip.Emit(OpCodes.Ldstr, str); ip.Emit(OpCodes.Call, asm.MainModule.Import(typeof(String).GetMethod("Concat", new Type[] { typeof(string), typeof(string) }))); ip.Emit(OpCodes.Call, asm.MainModule.Import(typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }))); ip.Emit(OpCodes.Call, asm.MainModule.Import(typeof(Console).GetMethod("ReadLine", new Type[] { }))); ip.Emit(OpCodes.Pop); ip.Emit(OpCodes.Ret); //  ,      :    //      var type = new TypeDefinition("supergreeter", "Program", TypeAttributes.AutoClass | TypeAttributes.Public | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit, asm.MainModule.Import(typeof(object))); //     asm.MainModule.Types.Add(type); //     type.Methods.Add(method); //       asm.EntryPoint = method; //     asm.Write("greeter.exe"); }
      
      







ここで、実際にコードを生成する不気味な中心部についてより慎重に説明します。



そこで何が起こっていますか?



C#で記述された同じプログラムは次のようになります(クラスの説明は省略します)。



 static public void Main() { Console.WriteLine("Hello, " + "username"); Console.ReadLine(); }
      
      







これを行うには、2行を使用します。1行目は定数で、2行目はコンパイル段階で決定され、また定数になります。それらをスタックに配置します。 String.Concatはこれらの行を追加し、結果をスタックの一番上に残します。結果はConsole.WriteLineによって取得され、表示されます。



その後、何かを読む時間がある前にプログラムが閉じないように、Console.ReadLine()が必要です-そして、それは不要な読み取り行を返すので、スタックからそれをスローし、達成感を持って私たちはすでに去りますMainのほぼネイティブな機能。



バイトコードについて



.NET仮想マシン用のプログラムを生成します。メソッドの本体は、明らかにそのコマンドで構成されています。 .NETはスタック仮想マシンであるため、すべての操作はオペランドがスタック上にある状態で実行されます。 それらの完全なリストはウィキペディアで見つけることができますが、私が使用したものについてのみ詳しく説明します。



LDSTRは文字列スタックにプッシュします。 明らかに、パラメータとして文字列が必要です。 実際、「文字列をスタックにロードする」とは、文字列自体がスタックにプッシュされるのではなく、メモリ内のある場所へのポインタのみがプッシュされることを意味します。しかし、ILプログラマーにとっては、これは重要ではありません。 唯一の重要なことは、次のコマンドがそこからそれを使用できることです。



CALLは、名前が示すとおり、メソッドを呼び出します。 これを行うには、このメソッド自体の説明を含むオブジェクトへのリンクを渡す必要があります。この説明は最初にインポートする必要があります。 インポートの場合、型のメソッドを「検索」して、パラメータの型の名前とリストを配列の形式で渡す必要があります。これが、レコードがひどい理由です。 良い方法で、ここでは、「String.Concat(string、string)」という形式の文字列をこの恐怖に変換する何らかの種類のハンドラを記述する必要があります。これを試すことができます。



POPはスタックから一番上のアイテムをポップします。 特別なことは何もありません。 Console.ReadLine()は値を返し、関数はvoidを返すため、必要です。そのため、そこに残すことはできず、クリアする必要があります。



RET-単語returnから、現在の関数を終了します。 それは各関数の最後にある必要があり、1つではないかもしれません-その関数からの出口点の数に依存します。



作業結果





最後に、プログラムをコンパイルして実行し、そこに名前を入力し、重いCompileボタンを押すと、同じフォルダーに、正確に2048バイトの重量のミニチュアgreeter.exeバイナリが格納されます。



起動して、出来上がり!




All Articles