Making archive IL2C #6-13 … 15

この記事は「AOT技術 Advent Calendar 2017」の5日目です。
というか、今のところこのカテゴリに私しかエントリーしてないので、漏れた日を埋めていこうかな、ぐらいの感じです。

YouTube: Playlist: Making archive IL2C
GitHub: IL2C step-by-step design project

ネタをひねり出すのもアレなので、今まで蓄積したMaking archive IL2Cのダイジェストをやっていこうかと思います。

(イメージをクリックするとYouTubeが開きます)


#6-13

これは、いわゆる型推論のような何かとか、CPUのレジスタリネーミングのような何かでは?みたいな事を考えながら、理論的なことはわからないので実コードとして実装可能なように問題を整理していました。

ここで、スタックスロットをシンボルとして抽象化した時に、単にリネーミングするとC言語に落としたときに型が合わなくてコードが成立しないので、その場合はそれぞれの型に合わせたコードを出力する必要があります。ようやく、問題の核心にたどり着きました。

#6-14

ILConverterの構造に手を入れて、全体的に修正したコードの解説を行いました。前回わかった問題を、どうやって対処するのか、ですが…

ブランチ命令によって分割される「実行パス」を認識して、それぞれのパスをキューに入れ、実行パス分岐後の流入箇所で想定される評価スタックのスロット(ここではインデックスと読んでいる)の型が一致しているかどうかを確認し、一致していればC言語上でもコードが一致するので単にgotoで飛ばし、そうでないなら、新たな型を使うが同じようなコードとして出力し直す、ということをします。

この部分、スタックスロットのコンシューマーが、ブランチ直前と遷移先で、どこまでと規定されるのかを機械的に判断するのが難しく、現在(#50)での実装も少し手抜きになっています(複雑というだけで不可能ではない)。

この大幅に変更したコードで、実際にフロー解析が機能して変換されるのかどうか? 緊張しましたが、正しく期待通りに変換されました。そして、Cコンパイラはこの複雑に見えるコードをちゃんと見抜いています。

#6-15

痛恨の収録ミス… 音声入ってなーい!!! orz ということで、後から映像だけ見ながらオーディオコメンタリーやって結合しました :)

  • 変換中の情報をDecodeContextクラスで保持する
  • スタックとスタックの各スロットに格納されることを想定している型の情報を保持する
  • それぞれのスタックスロットに対して、個別のシンボル名を割り当てる(型の異なるローカル変数を区別可能にする)

という登場人物を図示して、実際のコードに落としていきます。

あと、goto文を成立させるためにラベルをどうやって付けるか、という問題を考えています。今考えると、関数で遅延評価すれば良いよなと思うんですが、C#で書くと(ちょっとしたことではあるんですが)モヤモヤして、結局小さい入れ物を作って対処しました。

Making archive IL2C #6-10 … 12

この記事は「AOT技術 Advent Calendar 2017」の4日目です。
というか、今のところこのカテゴリに私しかエントリーしてないので、漏れた日を埋めていこうかな、ぐらいの感じです。

YouTube: Playlist: Making archive IL2C
GitHub: IL2C step-by-step design project

ネタをひねり出すのもアレなので、今まで蓄積したMaking archive IL2Cのダイジェストをやっていこうかと思います。

(イメージをクリックするとYouTubeが開きます)


#6-10

前回から引き続き、フロー解析(実行パス解析)の検討です。

ここら辺で、評価スタックの同じインデックスに異なる型が使用される可能性について気がついた感じです。ただ、まだadd opcodeについてのバリエーションで同じことが起きる、ぐらいの狭い範囲での気づきです(と言うか、こっちの前にIL全体で発生しうるという事に気が付きたかった :)

#6-11

フローについて本格的に解析が必要になり、今まではILConverterで変換した結果を一方的に変換処理に垂れ流ししていたのですが、実行パス(解析開始位置からブランチ先までの一連のopcodeシーケンス)毎に「フローの束」として順次解析を行う事が出来るように、地味ですが重要なリファクタリングを行っていきます。

#6-12

引き続き、フロー解析のコア部分を検討しながら実装しています。評価スタックの同一インデックスが異なる型で使われる場合のCソースコード上の表現方法について、ホワイトボードにあるように異なるローカル変数として宣言して使い分ける、という方針で実装することになりました。

Making archive IL2C #6-7 … 9

この記事は「AOT技術 Advent Calendar 2017」の3日目です。
というか、今のところこのカテゴリに私しかエントリーしてないので、漏れた日を埋めていこうかな、ぐらいの感じです。

YouTube: Playlist: Making archive IL2C
GitHub: IL2C step-by-step design project

ネタをひねり出すのもアレなので、今まで蓄積したMaking archive IL2Cのダイジェストをやっていこうかと思います。

(イメージをクリックするとYouTubeが開きます)


#6-7

Hello world的な最初のトライがInt32の計算だったので、まずはInt64を扱うことで、どのような差異が生まれるのかを確認しました。

計算自体はInt32とほぼ同じ(C言語に変換しても結果的に “+” オペレータになる)なのですが、リテラルの数値に”LL”を付けたり、関数引数の型に応じてローカル変数の型を変える必要があり、変換用コンテキストとして保持している情報にパラメータ群も保持するように改造しました。

このあたりではまだ評価スタックは、直前にPushされた値に対応する式を直接文字列で格納していたりして、ある意味のどかな感じです。

#6-8

迷走の始まりです :)

前回までで評価スタックに式を文字列で格納するのは問題がありそうだ、ということは分かったのですが、具体的に何が問題になりそうなのかはまだグレーだったので、簡単なILから順に考えて問題の焦点を考察しました。

デコーダー(とILConverterの前後)で、ブランチ命令の分析が必要ではないかという事が臭ってきます。何故なら、評価スタックにPushする値(の型)は自由であり型の制約がないため、フロー解析(実行パス解析)を行わないと、どの型に対応するのかが分からないからです。

#6-9

前回から引き続き、フロー解析のアルゴリズムの検討です。

この頃はまだ、文字列で表現された式でどうやってうまくやるかという事に執着していたような気がします。ひたすら細かいパターンを詳細に検討します。

Making archive IL2C #6-4 … 6

この記事は「AOT技術 Advent Calendar 2017」の二日目です。
というか、今のところこのカテゴリに私しかエントリーしてないので、漏れた日を埋めていこうかな、ぐらいの感じです。

YouTube: Playlist: Making archive IL2C
GitHub: IL2C step-by-step design project

ネタをひねり出すのもアレなので、今まで蓄積したMaking archive IL2Cのダイジェストをやっていこうかと思います。

(イメージをクリックするとYouTubeが開きます)


#6-4

ILConverterという抽象クラスを導入して、OpCodeに対応する変換の実装をクラスで表現できるようにしました。また、ILConverterを動的に収集して、ILConverterを実装した具象クラスを定義するだけで、OpCodeのデコーダーへの紐付けを動的に処理できるようにしました。

現在の実装(#6-48)でもそうですが、どうにかして「OpCodeと対応する変換処理」と「オペランドの扱い」を分離したいと言うバックグラウンドが垣間見えます。理由は、分離したほうがテストがしやすいのではないかとか、そういう事を考えていたと思います。今は…?

そう言えば、まだ録画機材とかで色々トラブルとか試行錯誤とかしていた頃で、(YouTubeに上げて正常公開されたにもかかわらず)ちょっと変な気がします。H264のストリームが壊れてるのかもしれません。今見ても、Full HD視聴はYouTubeから跳ねられることがあります… 生ファイルは持っているので再エンコードして入れ替えたいのですが、今のYouTubeの仕様では後から動画の差し替えが出来ないんですよね。

#6-5

前半では、「Human resource machine」という、Androidのゲームを紹介しています。このゲームは、ILやバイトコードのような低レベル言語がどのように動作するのかを、パズルのような遊び感覚で知ることが出来るものです。開発をやらない人によっては、このゲームの由来がそういう所から来ている事すら気が付かない人もいると思います。面白いのでおすすめです。

後半は、ILConverterの実装が増えてくることを考えて、ILConverterの変換結果をテストできるように、ユニットテストを実装する方法と実際の実装をやってみました。テストにはNUnitを使っています。

ユニットテストはしばらくこのインフラを使っていきますが、現在の実装においては機能していません。途中での大幅に構造に手を入れた事があり、今後もまだ構造に変更が加わらないかどうかがまだ見えていないため、この修正は保留しています。

#6-6

Hello world的な変換が出来て、テストコードも書けたので、今後の大まかな方針をまとめました。その上で、Int32を扱ったので、Int64にしたらどうなるのかを検証しなから対応しました。題材として丁度良かったので、徐々にTDDでやるようにしていきました。

C言語の整数リテラルの変則的な扱いについて驚いたりしてましたね :)

そして、いよいよVC++のプロジェクトを作り、そこに変換されたCソースコードを突っ込んで、正しく動作する事を確認しました。ここで、生成された機械語コードが「想定通り」全部静的解決されて、計算が直値に置き換わり、しかも関数呼び出しもインライン化されて、たった1命令に短縮されたことを確認しました。

Debugビルドの結果:

Releaseビルドの結果:

Making archive IL2C #6-1 … 3

この記事は「AOT技術 Advent Calendar 2017」の一日目です。
というか、今のところこのカテゴリに私しかエントリーしてないので、漏れた日を埋めていこうかな、ぐらいの感じです。

YouTube: Playlist: Making archive IL2C
GitHub: IL2C step-by-step design project

ネタをひねり出すのもアレなので、今まで蓄積したMaking archive IL2Cのダイジェストをやっていこうかと思います。

(イメージをクリックするとYouTubeが開きます)


#6-1

IL2Cを始めたいきさつと、どのようにプロジェクトを進めるのかが前半、後半はILSpyを使用して、Hello world的なコードがどのようなILにコンパイルされるのかを解説しています。一応、メタプログラミング初心者とか、IL初心者とかを想定して、割りと細かいところまで解説しています。

#6-2

前回ILSpyで確認したILを、リフレクションを使用してILの情報をプログラマブルに取得するためには、どのような事をすれば良いのかを解説しています。ライブ配信ではありませんが、目の前で一から書いてデバッグして見せることで、感触をつかみやすくしました。この回で、ILのバイトコードデコーダーの雛形を作りました。

#6-3

画像の通り、極初期の、main関数っぽい何か(全然C言語コードではありませんが)を、デコーダーを使って出力する所までを実装しました。これがIL2Cの内部骨格となって、あとはOpCodeをどう変換するかという、要するにスタート地点に立ったことになります。

初歩的なILの組:

ldc.i4.1
stloc.0

int a = 1;

のようなコードに変換されて欲しい、というOpCodeとC言語の対応付けを確認しました。

このまとめを書いたことで思い出したのですが、既に#6-3の時点で評価スタックの型の追跡が必要なのではないかという事に気がついていましたね。

IL2C短信: milestone3

IL2Cの短信です。milestone3となる、ValueTypeのサポートを(限定的ですが)行いました。

YouTube: Playlist: Making archive IL2C
GitHub: IL2C step-by-step design project


IL2Cをmicro:bitで動かす (#6-24・#6-25)

  • #6-24・#6-25の時点のIL2Cを使用して、micro:bitArduino UNOのCコンパイラを使って実際に極小環境で動かしました。

Making Archive IL2C at .NET Conf 2017 Tokyo (#6-28)

namespace il2c_test_target
{
    public class Hoge1
    {
        public static int Add1(int a, bool isTwo)
        {
            return a + (isTwo ? 2 : 1);
        }
    }
}
int32_t il2c_test_target_Hoge1_Add1(int32_t a, bool isTwo)
{
    int32_t local0;

    int32_t __stack0_int32_t;
    int32_t __stack1_int32_t;

    __stack0_int32_t = a;
    __stack1_int32_t = isTwo ? 1 : 0;
    if (__stack1_int32_t != 0) goto L_0000;
    __stack1_int32_t = 1;
    goto L_0001;
L_0000:
    __stack1_int32_t = 2;
L_0001:
    __stack0_int32_t = __stack0_int32_t + __stack1_int32_t;
    local0 = __stack0_int32_t;
    goto L_0002;
L_0002:
    __stack0_int32_t = local0;
    return __stack0_int32_t;
}

milestone3 (#6-37)

  • byte, sbyte, short, ushort, boolのサポート。
  • ValueType(構造体)のフィールドとメソッドをサポート。スタティックとインスタンスメンバの両方共に変換出来ます。
    スタティックメンバはCソースコード上、グローバル変数とグローバル関数として変換し、インスタンスフィールドは構造体、インスタンスメソッドはグローバル関数として変換。
  • 名前空間と型名についてのマングリングの導入。
  • 値を返さないメソッド(void)のret OpCodeのサポート
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace il2c_test_target
{
    public struct Hoge3
    {
        public static int Value1 = 123;
        public int Value2;

        public int GetValue2(int a, int b)
        {
            return this.Value2 + a + b;
        }
    }

    public class Hoge4
    {
        public static int Test4()
        {
            var hoge3 = new Hoge3();
            hoge3.Value2 = 456;

            return hoge3.Value2;
        }

        public static int Test5()
        {
            var hoge3 = new Hoge3();
            hoge3.Value2 = 789;

            var result = hoge3.GetValue2(123, 456);
            return result;
        }
    }
}
#ifndef __MODULE_il2c_test_target__
#define __MODULE_il2c_test_target__

#include <stdint.h>
#include <string.h>
#include <stdbool.h>

typedef struct il2c_test_target_Hoge3
{
    int32_t Value2;
} il2c_test_target_Hoge3;

extern int32_t il2c_test_target_Hoge3_Value1;

extern int32_t il2c_test_target_Hoge3_GetValue2(il2c_test_target_Hoge3* __this, int32_t a, int32_t b);
extern int32_t il2c_test_target_Hoge4_Test4(void);
extern int32_t il2c_test_target_Hoge4_Test5(void);

#endif
#include "il2c_test_target.h"

int32_t il2c_test_target_Hoge3_Value1 = 123;

int32_t il2c_test_target_Hoge3_GetValue2(il2c_test_target_Hoge3* __this, int32_t a, int32_t b)
{
    int32_t local0;

    il2c_test_target_Hoge3* __stack0_il2c_test_target_Hoge3_reference;
    int32_t __stack0_int32_t;
    int32_t __stack1_int32_t;

    __stack0_il2c_test_target_Hoge3_reference = __this;
    __stack0_int32_t = __stack0_il2c_test_target_Hoge3_reference->Value2;
    __stack1_int32_t = a;
    __stack0_int32_t = __stack0_int32_t + __stack1_int32_t;
    __stack1_int32_t = b;
    __stack0_int32_t = __stack0_int32_t + __stack1_int32_t;
    local0 = __stack0_int32_t;
    goto L_0000;
L_0000:
    __stack0_int32_t = local0;
    return __stack0_int32_t;
}

int32_t il2c_test_target_Hoge4_Test4(void)
{
    il2c_test_target_Hoge3 local0;
    int32_t local1;

    il2c_test_target_Hoge3* __stack0_il2c_test_target_Hoge3_reference;
    il2c_test_target_Hoge3 __stack0_il2c_test_target_Hoge3;
    int32_t __stack0_int32_t;
    int32_t __stack1_int32_t;

    __stack0_il2c_test_target_Hoge3_reference = &local0;
    memset(__stack0_il2c_test_target_Hoge3_reference, 0x00, sizeof(il2c_test_target_Hoge3));
    __stack0_il2c_test_target_Hoge3_reference = &local0;
    __stack1_int32_t = 456;
    __stack0_il2c_test_target_Hoge3_reference->Value2 = __stack1_int32_t;
    __stack0_il2c_test_target_Hoge3 = local0;
    __stack0_int32_t = __stack0_il2c_test_target_Hoge3.Value2;
    local1 = __stack0_int32_t;
    goto L_0000;
L_0000:
    __stack0_int32_t = local1;
    return __stack0_int32_t;
}

int32_t il2c_test_target_Hoge4_Test5(void)
{
    il2c_test_target_Hoge3 local0;
    int32_t local1;
    int32_t local2;

    il2c_test_target_Hoge3* __stack0_il2c_test_target_Hoge3_reference;
    int32_t __stack0_int32_t;
    int32_t __stack1_int32_t;
    int32_t __stack2_int32_t;

    __stack0_il2c_test_target_Hoge3_reference = &local0;
    memset(__stack0_il2c_test_target_Hoge3_reference, 0x00, sizeof(il2c_test_target_Hoge3));
    __stack0_il2c_test_target_Hoge3_reference = &local0;
    __stack1_int32_t = 789;
    __stack0_il2c_test_target_Hoge3_reference->Value2 = __stack1_int32_t;
    __stack0_il2c_test_target_Hoge3_reference = &local0;
    __stack1_int32_t = 123;
    __stack2_int32_t = 456;
    __stack0_int32_t = il2c_test_target_Hoge3_GetValue2(__stack0_il2c_test_target_Hoge3_reference, __stack1_int32_t, __stack2_int32_t);
    local1 = __stack0_int32_t;
    __stack0_int32_t = local1;
    local2 = __stack0_int32_t;
    goto L_0000;
L_0000:
    __stack0_int32_t = local2;
    return __stack0_int32_t;
}

IL2Cプロジェクト – Extensive Xamarin

技術書典3で配布予定の”Extensive Xamarin”に、IL2Cを寄稿させて頂きました。
是非、皆さん入手して読んでください :)

範囲はおおよそ.NET Confの発表分です。ビデオシリーズではすべて網羅していますが、その中でも印象深かったトピックについて、書籍化で解説しやすい部分を抜粋してあります。
締切がギリギリでしたが掲載を快諾していただいたXamaritansのメンバーの方々に感謝します。

何故Xamarin本に寄稿したのかですが、#6-38で喋っています:

IL2C短信: milestone2

IL2Cの短信です。milestoneを決めてやっているわけではないのですが、一区切りついたのでmilestone2ということにしておこう。

YouTube: Playlist: Making archive IL2C
GitHub: IL2C step-by-step design project


milestone1 (#6-4)

Hello world的なシンプルコードの変換が出来る:

public static int main()
{
  var a = 1;
  var b = 2;
  var c = a + b;
  return c;
}
int main(void)
{
  int local0;
  int local1;
  int local2;
  int local3;

  local0 = 1;
  local1 = 2;
  local2 = local1 + local0;
  local3 = local2;
  return local3;
}

milestone2 (#6-16)

  • プリミティブ型のプラットフォーム依存性を回避したいので、stdint.hを使うようにする。
  • 64ビット整数の足し算のサポート(今後の方向性検証のため)。
  • スタックの高度な再利用を含むコードの変換が出来る。
    これが一番大変だった… ILの実行パス解析に型推論が必要(知識ないのでもっと直接的な手法で実装した)とか。今の方法にも積み残しがあるけど。
  • 無条件・条件ジャンプに対応。
    上が出来たので、必然的にこれも出来るようになった。
public static long main()
{
  var a = 1L;
  var b = 2L;
  var c = a + b;
  return c;
}
#include <stdint.h>

int64_t main(void)
{
  int64_t local0;
  int64_t local1;
  int64_t local2;
  int64_t local3;

  int32_t __stack0_int32_t;
  int64_t __stack0_int64_t;
  int64_t __stack1_int64_t;

  __stack0_int32_t = 1;
  __stack0_int64_t = (int64_t)__stack0_int32_t;
  local0 = __stack0_int64_t;
  __stack0_int32_t = 2;
  __stack0_int64_t = (int64_t)__stack0_int32_t;
  local1 = __stack0_int64_t;
  __stack0_int64_t = local0;
  __stack1_int64_t = local1;
  __stack0_int64_t = __stack0_int64_t + __stack1_int64_t;
  local2 = __stack0_int64_t;
  __stack0_int64_t = local2;
  local3 = __stack0_int64_t;
  goto L_000b;
L_000b:
  __stack0_int64_t = local3;
  return __stack0_int64_t;
}

この冗長なコードは、VC++のReleaseビルドで2命令に短縮されます:

ビデオネタ: IL2C

IL2Cと言うのは、Unityで言うところのIL2CPPのC言語版です。これをフルスクラッチで作ってみるというビデオシリーズです。
つまり、C#(というか、それをコンパイルしたILを含むDLLやEXE)を入力に、C言語のソースコードを生成するツール、というわけです。

YouTube: Playlist: Making archive IL2C
GitHub: IL2C step-by-step design project

現在第6回までやったところですが、以下のようなC#のコード:

public static int main()
{
  var a = 1;
  var b = 2;
  var c = a + b;
  return c;
}

が、

#include <stdint.h>
int32_t main(void)
{
  int32_t local0;
  int32_t local1;
  int32_t local2;
  int32_t local3;

  local0 = 1;
  local1 = 2;
  local2 = local1 + local0;
  local3 = local2;
  return local3;
}

というC言語のソースコードに変換できる所まで出来ました。また、これの64ビット(long, int64_t)も出来たところです。

開発の方法の検討・設計・実装・テストまで含めて、全体を一からフルスクラッチでやってるところを、ほぼカット無しで収録しています。メタプログラミングに興味のある方は視聴してみてください。

この調子で続けると、40回~50回ぐらいで完成かな… 先は長そうだ :)

あと、途中でHuman Resource Machineというゲームをちょっとだけ紹介しています。IL(というかアセンブリ言語)をゲーム感覚で学べるアプリでお薦めです。

真・ILのキホン – 第六回 Center CLR 勉強会

WP_20160319_11_10_36_Pro_LIILについての勉強会を、第六回 Center CLR 勉強会でやってきました!

前回色々進行がダメだったので仕切り直しと言う事で、最初に少し解説を加えたりしてみました。


導入

il1扱っているネタが極めて低レベルと言う事もあり、前回同様出題内容を各自で解いてみるという形式で進行しました。
その前に導入として:

  • 「System.Reflection.Emit」名前空間を覚えておくこと。
    System.Reflection.Emit.OpCodesクラスがあり、ここにILのオプコード(OpCode)が定義されているよ。
  • OpCodeとは
    CLRが理解できる中間言語(バイトコード)
  • OpCodeにはいくつか種類がある事。
  • スタック操作・スタックマシンとは:
    CLRがバイトコード由来で動作するための基礎となる構造で、JVMのような類似技術もある。リアルなCPUにはあまり採用されていない事。

を説明しました。


実践して覚える

今回も、前回と同様GitHubのテンプレートとなるコードを元に課題にチャレンジします。

果たして…今回はPart5まで到達出来ました!! Part5では、インスタンスメソッドを扱い、その後のPartでクラス内のフィールドへのアクセス等を扱う予定でしたが、時間足らず。スライドは上げておくので、残りのPartを「忘れないうちに」取り組んでみる事をお勧めします。

# 多分しばらくはILネタのセッションは無いですww


Intermediate Languageを覚えることの意義

ILの基本と言う事で2回も長時間セッションを行ったわけですが、ILを覚えたところで一体何の役に立つのか?と言う疑問を持つ方もいるかも知れません。ILをやっていて重要だなと思うポイントを2点挙げておきます。

  • 参照型と値型の明確な区別
    ChalkTalk CLR – 動的コード生成技術(式木・IL等)でもやりましたが、CLRには大別して「参照型」と「値型」という2つの型の相違があります。これらはCLR内でのメモリの扱われ方が違う上、IL上でも異なる扱われ方をされます。特に参照型の値と、値型の値と、値型が参照される場合、など、IL上でも正しく区別が必要です。これらの区別がC#やVB.netでも正しく出来ていない・苦手という人は多いと思いますが、ILでどのように扱うのかがしっかり理解できれば(しかもこれは単純な法則でもあるので、抽象的な概念などは不要で実は覚えやすい)、C#などでコードを書く場合でも、自信をもって書けるはずです。また、これが分かると、ボクシング(Boxing)・アンボクシング(Unboxing)のコストと、これらがいつ生じるのか、と言う事が正しく理解できます。暗に気が付かないうちにこれらを発生させてしまい、パフォーマンスの低下を招くという事を回避できるようになるはずです。
  • アセンブリメタデータの構造観
    普段はコンパイラが自動的によしなにやってくれる、アセンブリメタデータの構造についての直観的な理解が得られます。なぜC#の言語構造はこうなっているのか、の(すべてではありませんが)ILから見た姿が分かります。記述した型がどこでどのように使われるのか、あるいはメソッドの定義はどこからやってくるのか、ILとどのように関連付けされるのかが分かります。これが分かると、どんな場合にアセンブリを分割すべきなのか、あるいはリフレクションを使用して解決すべき課題なのかそうではないのか、と言ったことを判断できます。こういった判断は、最終的にシステム全体の構造にも影響を与える可能性があるので、システム設計を行う上では無視できない要素の一つだと思います。

学問で言えば本当に基礎的な部分に相当するので、これを習得するメリットが見えにくいのは事実ですが、覚えておいて損はないと思います。大体、ILなんて「単純」なので、難しそうで実は簡単なんですよ!(きわめて抽象度の高い概念を覚える事に比べたら、どんな人でも覚えれる可能性があります)。

Let’s IL!!

それではまた。

ILのキホン – 第五回Center CLR年末会

WP_20151226_14_13_25_Pro_LI「ILのキホン」と言う題目で、Center CLR年末会で登壇してきました。

Center CLRも第五回で、毎回参加の方と新しく参加の方で程よく盛り上がってうれしい感じです。ChalkTalkもそうですが、遠方からわざわざ参加して頂いた方もありがとうございます。来年も継続して行きたいと思います。

さて、今回のネタは結構前から決めていたものの、年末の忙しさの為に十分な時間が取れず、直前に方針を決めてバタバタしてしまいました。猛省…

と言う事だけではないんですが、ちょっとセッションの進行を変えてみました。


Intermediate Languageのキホン

ILの事については、第一回のChalkTalk CLRで「ChalkTalk CLR – 動的コード生成技術(式木・IL等)」と題してディスカッションを行ったのですが、今回は本会でその展開をしようと思ったのです。

briefingしかし、ただILを説明したのでは、実際にILを使う具体的なシチュエーションでも提起できない限り、右から左へ流れていってしまうだけだと考えて、ハンズオンのような形式で進行してみました。

Emitが可能なコードを一から書いていると、なかなか本題にたどり着けないため、GitHubに元ネタとなるコードを用意しておき、Emitコードから書き始めれられるようにしました。

/// <summary>
/// コードをILで動的に生成するヘルパークラスです。
/// </summary>
internal sealed class Emitter : IDisposable
{
	private readonly AssemblyBuilder assemblyBuilder_;
	private readonly ModuleBuilder moduleBuilder_;

	/// <summary>
	/// コンストラクタです。
	/// </summary>
	/// <param name="name">アセンブリ名</param>
	public Emitter(string name)
	{
		var assemblyName = new AssemblyName(name);
#if NET45
		assemblyBuilder_ = AssemblyBuilder.DefineDynamicAssembly(
			assemblyName,
			AssemblyBuilderAccess.RunAndSave);
#else
		assemblyBuilder_ = AssemblyBuilder.DefineDynamicAssembly(
			assemblyName,
			AssemblyBuilderAccess.Run);
#endif
		moduleBuilder_ = assemblyBuilder_.DefineDynamicModule(name + ".dll");
	}

	/// <summary>
	/// Disposeメソッドです。
	/// </summary>
	public void Dispose()
	{
#if NET45
		// デバッグ用に出力
		assemblyBuilder_.Save(moduleBuilder_.ScopeName);
#endif
	}

	/// <summary>
	/// 引数と戻り値を指定可能なメソッドを定義します。
	/// </summary>
	/// <typeparam name="TArgument">引数の型</typeparam>
	/// <typeparam name="TReturn">戻り値の型</typeparam>
	/// <param name="typeName">クラス名</param>
	/// <param name="methodName">メソッド名</param>
	/// <param name="emitter">Emitを実行するデリゲート</param>
	/// <returns>デリゲート</returns>
	public Func<TArgument, TReturn> EmitMethod<TArgument, TReturn>(
		string typeName,
		string methodName,
		Action<ILGenerator> emitter)
	{
		// クラス定義
		var typeAttribute = TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Abstract;
		var typeBuilder = moduleBuilder_.DefineType(
			typeName,
			typeAttribute,
			typeof(object));

		// メソッド定義
		var methodAttribute = MethodAttributes.Public | MethodAttributes.Static;
		var methodBuilder = typeBuilder.DefineMethod(
			methodName,
			methodAttribute,
			typeof(TReturn),
			new [] { typeof(TArgument) });

		// ILGenerator
		var ilGenerator = methodBuilder.GetILGenerator();

		emitter(ilGenerator);

		// 定義を完了して型を得る
		var typeInfo = typeBuilder.CreateTypeInfo();
		var type = typeInfo.AsType();

		// メソッドを取得する
		var bindingFlags = BindingFlags.Public | BindingFlags.Static;
		var method = type.GetMethod(methodName, bindingFlags);

		// デリゲートを生成する
		return (Func<TArgument, TReturn>)method.CreateDelegate(
			typeof(Func<TArgument, TReturn>));
	}
}

public static class Program
{
	public static void Main(string[] args)
	{
		// 動的アセンブリを生成
		using (var emitter = new Emitter("TestAssembly"))
		{
			// メソッドを動的に生成
			var func = emitter.EmitMethod<int, string>(
				"TestNamespace.TestType",
				"TestMethod",
				ilGenerator =>
				{
					ilGenerator.Emit(OpCodes.Ldstr, "Hello IL coder!");
					ilGenerator.Emit(OpCodes.Ret);
				});

			// 実行
			var result = func(123);

			// 結果
			Console.WriteLine(result);
		}
	}
}

Emitterクラスは、System.Reflection.Emitの煩雑なコードを隠ぺいするために定義しています。課題の多くはMainメソッド内のラムダブロックのEmitメソッドをいじります。

# なお、このコードはnetcore5向けでも実行可能にしてあります。

課題はPart1~Part10まで用意し、はじめに少しだけ「スタックマシン」についての解説を行いました。


Part 1: 引数の値をToStringして返すようにする

part1GitHubのコードを軽く説明して、Emit本体のコードを課題に従って書き換えてもらいました。

テンプレートのコードは、”Hello IL coder!”と言う文字列を返すだけのものですが、引数で与えられたintの値を文字列に変換して返すのが課題です。

最初なので、こんなものだろうと思っていたのですが… 実はこれ、大変な罠が…

やるべきことは、スライド通り大きく3つあります。

引数の値はどうやって入手するか

ヒントに書いておいたのですが、「OpCodesクラス」を見ると、IL命令の一覧が確認できます。

そして、スタックに値を積む(プッシュ)するのは、「Ld~」で始まる命令であることも説明しました。OpCodesの一覧を見ていると、「Ldarg_0」命令が見つかります。これを使うと、引数の最初の値をスタックに積みます。「Ldarg」や「Ldarg_S」命令でもOKです。その場合は、Emitメソッドの引数にインデックス(0ですが)を指定します。

Int32.ToStringのMethodInfoはどうやって入手するか / staticメソッドの呼び出し

メソッドを呼び出すには、「Call」命令を使います。その時、Emitする引数に、呼び出すメソッドの「メソッド情報(MethodInfo)」が必要です。これには、リフレクションを使います。

// Int32のTypeクラスのインスタンスを得る
Type int32Type = typeof(System.Int32);

// Int32.ToStringメソッドのメソッド情報を得る
MethodInfo toStringMethod = int32Type.GetMethod("ToString", Type.EmptyTypes);

ToStringのオーバーロードは複数あるので、正しいメソッド情報を選択するために、Type.EmptyTypesフィールドを使って、「引数0」のオーバーロードを選択させています(このフィールドを使わなくても、0個の配列でもOKです)。

typeofを使うことに対して、一部の方がモヤモヤしていた(チートっぽい)ようです。厳密に0からInt32のTypeを取得したことにならないのではないかとの事で、確かにその通り。typeofを使うと、C#コンパイラがコンパイル時にInt32を解決しようとします。実行時に0から取得する事も不可能ではないのですが、その話をするとまた話がずれていくので、今は諦めてもらう事に。

# Call命令を使うかどうかの更なる議論がありますが、それは後のPartで…


「動かない!」

複数のチームから「動かない!」とか、「NullReferenceExceptionがスローされる!」とか騒ぎが…

ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Call, toStringMethod);
ilGenerator.Emit(OpCodes.Ret);

どう見ても間違っていないように見える… そこで、「ILSpy」を使って、C#で書いた等価コードを逆アセンブルした所、引数をスタックに積む際に、「Ldarga」命令を使っていました。

やられた orz

Ldarga命令は、対象の値そのものではなく、その値を示す参照(ポインタ)をスタックに積みます。これは、対象の値が「値型(ValueType)」であり、Callでそのメソッドに遷移した先ではthis参照として扱われなければならないため、Ldarg命令ではなく、Ldarga命令を使う必要があったのです。

Int32の場合は値型と言っても、なぜ参照化しなければならないのか、ピンと来ないかもしれません。以下に構造体(構造体も値型)を使った例を示します:

// 構造体
public struct Hoge
{
	public int A;
	public int B;
	public int C;

	// 計算し、結果をCに代入する
	public void Compute()
	{
		// 計算結果を代入するには、this(自分自身への参照)が必要
		this.C = this.A + this.B;
	}
}

// メソッドを呼び出して計算させる
Hoge hoge = new Hoge();
hoge.A = 1;
hoge.B = 2;
hoge.C = 123;
hoge.Compute();

// hoge.Cは、123か3か?
Console.WriteLine(hoge.C);

Computeメソッドはインスタンスメソッドなので、内部でthis参照が使えます。this参照を使った場合、そのインスタンスは、呼び出し元のhogeとなるはずです。Computeメソッドから呼び出し元が保持するインスタンスにアクセスさせるには、文字通り「参照」が必要なのです。従って、ここではLdarga命令を使って、インスタンスへの参照を渡す必要があります。勿論、対象が参照型(クラス)であれば、Ldarg命令で問題ありません。

# NullReferenceExceptionがスローされたのは、本当に偶然のようで、真の原因は分かりません。


全然足りない!!

そんなわけで、このPart1を30分ぐらいでやるつもりだったのが、これだけで時間使い切ってしまいました。値型と参照型の違いは、実際にはPart6ぐらいでやってもらう予定だったのが、こんな所でやる事になったのが敗因…

時間を延長して続けるかどうかという話もあったのですが、他セッションもあったため、次回に持ち越しとなりました。次回に同じ題目で再チャレンジします。ごめんなさい。スライドはその時まで正式公開はしません。次回も同様に参加する場合は、Part1の内容の復習をしておいてくだしあ。


詰め込みすぎは良くない

年末は… ダメですね。この記事も31日に書いてるし (;´Д`)
でも楽しかったです。来年もよろしくお願いします!