VB.NETは解析されやすいのか?(その1)

はじめに

C#VBなどの.NET言語はリバースエンジニアリングに弱いと言われています。 ちょっとした用事でソースコードを紛失したライブラリを解析してその事を実感したので今回はその事です。

イントロ

それではまず、VB*1コンパイルの過程をおさらいしましょう。

  1. VBでコードを書く
  2. VBコンパイラVBのコードからCIL(Common Intermediate Language)に変換される
  3. コンパイル結果がファイルに格納される(ここまでがコンパイル時)
  4. プログラムがメモリにロードされる
  5. メソッド単位で初めて呼び出されるタイミングでCPU命令に変換される

という流れになっています。

また、自己反映計算《†リフレクション†》に対応するため3の時点で生成されるファイルにはクラス名はメソッド名などの情報が埋め込まれています。 そのため、メソッド名をそのまま読み取ることが可能であり、読みやすいコードを書いている場合はクラス名やメソッド名から役割や機能を類推する事も出来ます。

このブログは取り合えずやってみようぜの精神なので、とりあえずテキトーなアプリケーションを解析してみましょう。

ここで想定するアプリケーションは以下の条件を満たすものとします。

  • すべてマネージで構成されている
  • 難読化されていない

機能をP/Invokeに委譲してたりみんな大好き異形言語C++/CLIで実装されてたりとネイティブが含まれていると難易度が一気に跳ね上がります。 また、難読化されていると文字列が暗号化されて格納されたり、メソッドのオーバーロードをこれでもかと言わんばかりに突っ込まれたり、識別子を紛らわしい文字に置き換えらえたり、フローを書き換えて逆コンパイラを殺しにかかったりとこちらも鬼畜な感じになるのでやるなら気合を入れてとりかかってくだしあ。

世の中にはC#向け(一部VBにも対応)している逆コンパイラがあるので、弊社の知っている範囲ですが一応紹介します。今回は使いませんが

  • ILSpy
    • この手のツールとしては珍しくVBへの逆コンパイルに対応している。
    • オープンソースとして開発されておりタダで使える。弊社も割とお世話になってる。
  • dotPeek
    • ReSharperで有名なJetBrains社の逆コンパイラC#のみ)。というよりReSharperのツールチェーンの一部。
    • dotPeekだけならタダで使える。
    • Visual Studioっぽい外見でかっこいい。けどやたら重いので起動を待っている間にやる気が消滅することもある。
    • C#プロジェクトへのエクスポート機能があったりシンボルサーバとして動かしたりすることも出来る。
    • 見た目がかっこいい。
  • .NET Refactor
    • RedGate社の逆コンパイラ
    • 有償。
    • 使ったことがないので正直よくわからない。

解析

とりあえずテキトーなアプリケーションとしてSuperUsefulApplication.exeというコンソールアプリケーションを用意しました。

パスワードを入力してください
>password
パスワードが違います

まず、起動するとパスワードを求められます。初めにこれを突破してみましょう。

アセンブルしておきましょう。

>ildasm SuperUsefulApplication.exe /utf8 /out=SuperUsefulApplication.il

すると以下のファイルが生成されるはずです。

SuperUsefulApplication.il
SuperUsefulApplication.res
SuperUsefulApplication.Resources.resources

ここで興味があるのはSuperUsefulApplication.ilだけですので、他のはとりあえず気にしない方向で。

上記の動作例を見るとパスワードを入力させ判定するコードの周辺に「パスワードを入力してください」という文字があるはずです。 .NET Frameworkの内部コードはUTF-16 LEですので、

D1 30 B9 30 EF 30 FC 30 C9 30 92 30 65 51 9B 52 57 30 66 30 4F 30 60 30 55 30 44 30

というバイトアレイがリテラルとして近くにあればいいな~という願望を抱きながら検索します。

    .entrypoint
    .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 ) 
    // コード サイズ       188 (0xbc)
    .maxstack  4
    .locals init (class [System.Core]System.Security.Cryptography.SHA1Cng V_0,
             class SuperUsefulApplication.SuperUsefulClass`1<string> V_1,
             string V_2)
    IL_0000:  newobj     instance void [System.Core]System.Security.Cryptography.SHA1Cng::.ctor()
    IL_0005:  stloc.0
    // パスワードを入力してください
    IL_0006:  ldstr      bytearray (D1 30 B9 30 EF 30 FC 30 C9 30 92 30 65 51 9B 52
                                    57 30 66 30 4F 30 60 30 55 30 44 30 )
    IL_000b:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_0010:  ldstr      ">"
    IL_0015:  call       void [mscorlib]System.Console::Write(string)
    IL_001a:  ldstr      "Z454hURH5p7ckn+/aqZMFTQ7J/o="
    IL_001f:  ldloc.0
    IL_0020:  call       class [mscorlib]System.Text.Encoding [mscorlib]System.Text.Encoding::get_UTF8()
    IL_0025:  call       string [mscorlib]System.Console::ReadLine()
    IL_002a:  callvirt   instance uint8[] [mscorlib]System.Text.Encoding::GetBytes(string)
    IL_002f:  callvirt   instance uint8[] [mscorlib]System.Security.Cryptography.HashAlgorithm::ComputeHash(uint8[])
    IL_0034:  call       string [mscorlib]System.Convert::ToBase64String(uint8[])
    IL_0039:  ldc.i4.0
    IL_003a:  call       int32 [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.Operators::CompareString(string,
                                                                                                                      string,
                                                                                                                      bool)
    IL_003f:  brfalse.s  IL_0051

    IL_0041:  ldstr      bytearray (D1 30 B9 30 EF 30 FC 30 C9 30 4C 30 55 90 44 30   // .0.0.0.0.0L0U.D0
                                    7E 30 59 30 )                                     // ~0Y0
    IL_0046:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_004b:  ldc.i4.0
    IL_004c:  call       void [mscorlib]System.Environment::Exit(int32)
    IL_0051:  ldstr      bytearray (88 30 46 30 53 30 5D 30 )                         // .0F0S0]0
    IL_0056:  call       void [mscorlib]System.Console::WriteLine(string)

ありました。今回はツイてます。

ILコードの詳細は皆さんの脳内評価スタックを信じて詳しくは解説しませんが、以下の流れでパスワードを検証していることが分かります。

  1. パスワードを入力させる
  2. パスワードをUTF-8としてバイト列に変換する
  3. 上のバイト列のSHA1ハッシュを計算する
  4. ハッシュをBase64に変換して文字列と比較する

パスワードがプログラム中に直接埋め込まれていた場合、この時点でパスワードが判明します。まぁ、普通はそんなことはあり得ませんが。 SHA1を突破するのはあきらめてこの検証ロジック自体を迂回する事とします。

IL_004cまでが検証ロジックのようですので、プログラムの先頭で直接IL_0051にジャンプすれば何とかなりそうです。

ということでほい。

    .entrypoint
    .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )
    // コード サイズ       188 (0xbc)
    .maxstack  4
    .locals init (class [System.Core]System.Security.Cryptography.SHA1Cng V_0,
             class SuperUsefulApplication.SuperUsefulClass`1<string> V_1,
             string V_2)
              br.s       IL_0051 // ここを追加
    IL_0000:  newobj     instance void [System.Core]System.Security.Cryptography.SHA1Cng::.ctor()
    IL_0005:  stloc.0

アセンブルして確認してみましょう。

>ilasm SuperUsefulApplication.il /exe /debug=impl /resource=SuperUsefulApplication.res /output=SuperUsefulApplication2.exe

一応peverifyで生成されたプログラムを確認しておきましょう。

>peverify SuperUsefulApplication2.exe

Microsoft (R) .NET Framework PE Verifier バージョン  4.0.30319.0
Copyright (c) Microsoft Corporation. All rights reserved.

含まれるすべてのクラスおよびメソッドSuperUsefulApplication2.exe 確認しました。

問題なさそうですね。それでは起動してみましょう。

ようこそ
>

期待通りに検証ロジックを迂回できています。

終わりに

今回は割と長くなってしまったので、いったんここで区切っておきます。 気が向いたらこの続きについて書こうと思います。

github.com

余談ですが、サンプルプログラムで使用しているCryptography API: Next GenerationはWindows Vista以降でしか動きません。 XP? なにそれ? おいしいの?

つづく

*1:C#も同じですが、VB向けの記事ですので・・・

VB13以前でもNull条件演算子を使いたい

はじめに

ちょっと前にVB14の新機能についての話をしたじゃないですか。 その中にNull条件演算子なんてものがあったと思います。

弊社はEntity FrameworkみたいなライブラリでFirstOrDefaultみたいなクエリを割合ぽいぽい投げ返ってきた値がNothingかそうでないかで処理を分岐させることが多いのですが、

Dim hoge = db.Hoges.Where(Function(it) it.UniqueKey = "HOGE").FirstOrDefault()
Dim hogeName As String

If Not hoge Is Nothing Then
    hogeName = hoge.Name
Else
    hogeName = Nothing
End If

みたいなコードをほぼ毎回のように書かなくてはならず非常にげんなりします。 まぁ、設計がクソなのも一理ありますが。

そこでNull条件演算子を使いたいのですが、現在使用している環境では使えないという最高にクソな感じに仕上がっており*1日夜冗長でクソなコードを生成している次第でございます。うおォン 俺はまるでクソコードジェネレータだ

あああああ

かくなる上はメタプログラミングを用いてごり押しで解決したいところなのですが、さすがに構文に介入することはできません。 コンパイル時に介入してASTを弄れれば話は別かもしれませんが。

Roslyn? なにそれ? おいしいの?

というわけで拡張メソッドでそれっぽく仕上げてお茶を濁すほかありません。

というわけでほい。

Module NullableExtension

    <Extension>
    Public Function N(Of TModel, TResult As Class)(value As TModel, expr As Func(Of TModel, TResult)) As TResult
        If value Is Nothing Then
            Return Nothing
        Else
            Return expr(value)
        End If
    End Function

    <Extension>
    Public Function Ns(Of TModel, TResult As Structure)(value As TModel, expr As Func(Of TModel, TResult)) As Nullable(Of TResult)
        If value Is Nothing Then
            Return Nothing
        Else
            Return expr(value)
        End If
    End Function

    <Extension>
    Public Function Ns(Of TModel, TResult As Structure)(value As TModel, expr As Func(Of TModel, Nullable(Of TResult))) As Nullable(Of TResult)
        If value Is Nothing Then
            Return Nothing
        Else
            Return expr(value)
        End If
    End Function

End Module

とくに解説することも無いのですが、

N(Of TModel, TResult As Class)(value As TModel, expr As Func(Of TModel, TResult))

N(Of TModel, TResult As Structure)(value As TModel, expr As Func(Of TModel, TResult))

オーバーロード不可なのはなんとなくわかりますが、

Ns(Of TModel, TResult As Structure)(value As TModel, expr As Func(Of TModel, TResult))

Ns(Of TModel, TResult As Structure)(value As TModel, expr As Func(Of TModel, Nullable(Of TResult)))

オーバーロードできるんですね。驚きです。

んで、こんな感じに使います。

Dim a = New Hoge() With {.Id = 1, .Name = "Hoge", .ParentId = 0}
Dim b = New Hoge() With {.Id = 1, .Name = "Hoge", .ParentId = Nothing}
Dim c As Hoge

Console.WriteLine(a.Ns(Function(it) it.Id))
Console.WriteLine(a.N(Function(it) it.Name))
Console.WriteLine(a.Ns(Function(it) it.ParentId))
        
Console.WriteLine(b.Ns(Function(it) it.Id))
Console.WriteLine(b.N(Function(it) it.Name))
Console.WriteLine(b.Ns(Function(it) it.ParentId))

Console.WriteLine(c.Ns(Function(it) it.Id))
Console.WriteLine(c.N(Function(it) it.Name))
Console.WriteLine(c.Ns(Function(it) it.ParentId))

おわりに

なんというか、拡張メソッドの乱用ですよね。

Optionalみたいなアプローチではなくnullだろうがなんだろうがゴリ押しで値を取得するみたいなアプローチ、私は結構好きです。 ただ、実際に他人のコードでこんなのを見たら『うわぁ・・・』は必至ですね。 取り合えず設計をもう少しどうにかしろよ的な。

余談ですが、VBのラムダってFunction(it) it...みたいに冗長なのが嫌いなんですよね。 同じ意味を持つC#のそれと比較してもit => it.NameFunction(it) it.Nameの差の大きいこと。

まぁ、そもそもC#/VBののラムダの内部に可変ステートを抱えられる仕様はちょっとどうかな~と思います。 シングルスレッドならともかく、マルチスレッドでうっかり可変ステートを外部に出してしまうとスレッドセーフの要件の一つである『不変なステートは常にスレッドセーフ』が途端に崩壊してしまうのでマルチスレッドで使うときはハラハラします。

とにかく、Decimalの内部構造を取り出して桁数を解析する以来のバッドノウハウの再来ということで。

github.com

おわり

*1:そもそもYield Returnが使えない。複数行ラムダはぎりぎり使える。あっ(察し)

C#でもSigilでデリゲートをダイナミックに生成したい

はじめに

飽きもせずに.NET黒魔術シリーズです。

SigilといってもEPUBの方のSigilではないです。そっちの情報を求めていた人はまわれ右してお帰りください。

やる気が起きなかった時にネットサーフィンしていたところ、こんな記事を見つけました。

www.infoq.com

github.com

このライブラリはILGeneratorをラップし、ILの誤りを可能な限り早く検出しわかりやすいエラーメッセージを提供してくれます。 というよりもILGeneratorは実行時でないと誤りがあるかわからないのと、誤りがあります程度のメッセージしか提供してくれずかなりつらぽよポイントは高いです。

今回はSigilを用いて動的にToStringをするデリゲートを作成してみましょう。好きですね。ToStringを動的に生成するの。

どうでもいいのですが、作者の笑顔がいいですね。

お手本

とりあえず、ベースとなるILをコンパイラに生成してもらいましょう。

class Target
{
    public int Id { set; get; }
    public virtual string Name { set; get; }
    public DateTime Hoge { set; get; }
}

こんなクラスがあって、

class Program
{
    static void Main(string[] args)
    {
        var target = new Target()
        {
            Id = 1,
            Name = "あああああ",
            Hoge = DateTime.Now
        };

        Console.WriteLine(ToString(target));
    }

    static string ToString(object value)
    {
        var target = (Target)value;
        var builder = new StringBuilder();

        builder
            .Append(nameof(Target) + ":{")
            .Append(nameof(Target.Id) + "=")
            .Append(target.Id)
            .Append(",")
            .Append(nameof(Target.Name) + "=")
            .Append(target.Name)
            .Append(",")
            .Append(nameof(Target.Hoge) + "=")
            .Append(target.Hoge)
            .Append("}");

        return builder.ToString();
    }
}

こんな感じにToStringするとしましょう。

アセンブリをビルドし、ildasmで逆アセンブルすると以下のコードを生成すればいいことがわかります。

  .maxstack  2
  .locals init ([0] class Samplejunk.Target target,
           [1] class [mscorlib]System.Text.StringBuilder builder,
           [2] string V_2)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  castclass  Samplejunk.Target
  IL_0007:  stloc.0
  IL_0008:  newobj     instance void [mscorlib]System.Text.StringBuilder::.ctor()
  IL_000d:  stloc.1
  IL_000e:  ldloc.1
  IL_000f:  ldstr      "Target:{"
  IL_0014:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
  IL_0019:  ldstr      "Id="
  IL_001e:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
  IL_0023:  ldloc.0
  IL_0024:  callvirt   instance int32 Samplejunk.Target::get_Id()
  IL_0029:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(int32)
  IL_002e:  ldstr      ","
  IL_0033:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
  IL_0038:  ldstr      "Name="
  IL_003d:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
  IL_0042:  ldloc.0
  IL_0043:  callvirt   instance string Samplejunk.Target::get_Name()
  IL_0048:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
  IL_004d:  ldstr      ","
  IL_0052:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
  IL_0057:  ldstr      "Hoge="
  IL_005c:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
  IL_0061:  ldloc.0
  IL_0062:  callvirt   instance valuetype [mscorlib]System.DateTime Samplejunk.Target::get_Hoge()
  IL_0067:  box        [mscorlib]System.DateTime
  IL_006c:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(object)
  IL_0071:  ldstr      "}"
  IL_0076:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
  IL_007b:  pop
  IL_007c:  ldloc.1
  IL_007d:  callvirt   instance string [mscorlib]System.Object::ToString()
  IL_0082:  stloc.2
  IL_0083:  br.s       IL_0085
  IL_0085:  ldloc.2
  IL_0086:  ret

共通中間言語

C#とILのコードを比較しながら見てみましょう。

共通中間言語(Common Intermediate Language、略してCIL)はスタックベースの言語であり、オペランドをスタックへプッシュし命令を実行し計算結果をスタックから受け取ります。

ここではスタック遷移を以下の書式で表現します。

  • スタックは左から右に伸びていく。つまり古い値が左で新しい値が右である。
  • 左側のスタックが命令の実行前のスタックを表し、右側のスタックが命令の実行後のスタックを表す。

試しにやたら目につくcallvirtを見てみましょう。

形式

callvirt <メソッド>

スタック遷移

..., <オブジェクト>, <引数1>, ..., <引数N> → ..., <返り値>(返り値のないメソッドの場合は無い)

callvirtはメソッドを呼び出します。 また、virtとついているあたり、コンパイル時のクラスに基づいたメソッド呼び出しではなく、実際のクラスに基づき呼び出すメソッドを決定します。

まぁ、要するにオーバーライドされている場合はオーバーライドしている派生先のメソッドが呼び出されるってことです。

callvirtで呼び出す場合は最初に呼び出すオブジェクトをスタックへプッシュし、次に順に引数をスタックへプッシュしていきます。 呼び出し先のメソッドから制御が返されると、返り値がある場合はスタックの先頭に返り値が置かれます。

と、いうわけで先ほどのコードを先頭から見ていきましょう。

var target = (Target)value;
// IL_0001:  ldarg.0
// IL_0002:  castclass  Samplejunk.Target
// IL_0007:  stloc.0
  • ldarg.0は0番目の引数をスタックへプッシュ
    • (空) → object
  • castclassでスタックの先頭のオブジェクトをキャストしスタックにプッシュ
    • object → Target
  • stloc.0でスタックの先頭のオブジェクトを0番目のローカル変数に代入
    • Target → (空)
var builder = new StringBuilder();
// IL_0008:  newobj     instance void [mscorlib]System.Text.StringBuilder::.ctor()
// IL_000d:  stloc.1
  • newobjStringBuilderインスタンスを生成
    • (空) → StringBuilder
  • stloc.1でスタックの先頭のオブジェクトを1番目のローカル変数に代入
    • StringBuilder → (空)
builder
    .Append(nameof(Target) + ":{")
// IL_000e:  ldloc.1
// IL_000f:  ldstr      "Target:{"
// IL_0014:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
  • ldloc.1で1番目のローカル変数をスタックにプッシュ
    • (空) → StringBuilder
  • ldstrで文字列定数"Target:{"をスタックにプッシュ
    • StringBuilder → StringBuilder, string
  • callvirtStringBuilder::Append(string)を呼び出す StringBuilderをスタックにプッシュする
    • StringBuilder, string → StringBuilder

あとは似たようなコードなので割愛しますが、上のコードから以下のことが読み取れます。

  • StringBuilder::Appendの適切なオーバーロードがある場合はそのメソッドを呼び出す
  • 参照型で適切なオーバーロードがない場合はStringBuilder::Append(object)を呼び出す
  • 値型で適切なオーバーロードがない場合はboxでボックス化を施したのちにStringBuilder::Append(object)を呼び出す

つまりC#で書く場合はコンパイラがやってくれているオーバーロードの解決を自分でやらないといけないってことです。

Sigil

前半はSigilというよりはILそのものについてのお話しでしたが、ここからはSigilでどのように生成しましょうかというお話になります。

その前に、String::Appendオーバーロードを解決するヘルパメソッドを作っておきましょう。

static bool IsSupportedType(Type type)
{
    var appends = typeof(StringBuilder)
        .GetMethods(Public | Instance)
        .Where(it => it.Name == "Append")
        .Where(it => it.GetParameters().Length == 1)
        .ToDictionary(it => it.GetParameters()[0].ParameterType);

   return appends.ContainsKey(type);
}

static MethodInfo GetAppend(Type type)
{
    var appends = typeof(StringBuilder)
        .GetMethods(Public | Instance)
        .Where(it => it.Name == "Append")
        .Where(it => it.GetParameters().Length == 1)
        .ToDictionary(it => it.GetParameters()[0].ParameterType);

    if (appends.ContainsKey(type))
        return appends[type];
    else
        return appends[typeof(object)];
}

IsSupportedTypeTypeを引数に取り、その型の引数をとるオーバーロードがあればtrueを返し、なければfalseを返します。

同様にGetAppendTypeを引数に取り、その型の引数をとるオーバーロードがあればそのMethodInfoを返し、なければobjectMethodInfoを返します。

static Func<object, string> BuildDelegate(Type targetType)
{
    var targetProperties = targetType.GetMembers(Public | Instance)
        .Where(it => it is PropertyInfo)
        .Select(it => (PropertyInfo)it)
        .Where(it => it.CanRead && it.GetIndexParameters().Length == 0);

    var e = Emit<Func<object, string>>.NewDynamicMethod();

    using (var target = e.DeclareLocal(targetType))
    using (var builder = e.DeclareLocal<StringBuilder>())
    {
        // target = (Target)value;
        e.LoadArgument(0);
        e.CastClass(targetType);
        e.StoreLocal(target);

        // builder = new StringBuilder();
        e.NewObject<StringBuilder>();
        e.StoreLocal(builder);

        // builder.Append(nameof(Target) + ":{")
        e.LoadLocal(builder);
        e.LoadConstant(targetType.Name + "{");
        e.CallVirtual(GetAppend(typeof(string)));

        var isNotFirst = false;
        foreach (var it in targetProperties)
        {
            if (isNotFirst)
            {
                // builder.Append(",")
                e.LoadConstant(",");
                e.CallVirtual(GetAppend(typeof(string)));
            }
            isNotFirst = true;

            // builder.Append(nameof(Target.Property) + "=")
            e.LoadConstant(it.Name + "=");
            e.CallVirtual(GetAppend(typeof(string)));

            // builder.Append(target.Property)
            e.LoadLocal(target);
            e.CallVirtual(it.GetGetMethod());
            if (it.PropertyType.IsValueType && !IsSupportedType(it.PropertyType))
                e.Box(it.PropertyType);
            e.CallVirtual(GetAppend(it.PropertyType));
        }

        // builder.Append("}")
        e.LoadConstant("}");
        e.CallVirtual(GetAppend(typeof(string)));

        // return builder.ToString()
        var toString = typeof(object).GetMethod("ToString");
        e.CallVirtual(toString);
        e.Return();
    }

    Console.WriteLine($".maxstack {e.MaxStackSize}");
    Console.WriteLine(e.Instructions());

    return e.CreateDelegate();
}
static void Main(string[] args)
{
    var target = new Target()
    {
        Id = 1,
        Name = "あああああ",
        Hoge = DateTime.Now
    };

    var d = BuildDelegate(typeof(Target));
    Console.WriteLine();
    Console.WriteLine(d(target));
}
using (var target = e.DeclareLocal(targetType))
using (var builder = e.DeclareLocal<StringBuilder>())
{
    // ...
}

上記の構文で変数の宣言を行っています。 本来ILレベルでは変数のスコープはメソッドが最小単位なのですが、IDisposableを用いた機構でブロックレベルでの変数スコープを表現しています。 これは面白いと思います。

あとはほとんどILと一対一で対応している命令を放り込んで最後に

e.CreateDelegate();

でデリゲートは完成です。

実行結果はこちら。

.maxstack 2
ldarg.0
castclass SigilCSharp.Target
stloc.0 // SigilCSharp.Target _local0
newobj Void .ctor()
stloc.1 // System.Text.StringBuilder _local1
ldloc.1 // System.Text.StringBuilder _local1
ldstr 'Target{'
callvirt System.Text.StringBuilder Append(System.String)
ldstr 'Id='
callvirt System.Text.StringBuilder Append(System.String)
ldloc.0 // SigilCSharp.Target _local0
callvirt Int32 get_Id()
callvirt System.Text.StringBuilder Append(Int32)
ldstr ','
callvirt System.Text.StringBuilder Append(System.String)
ldstr 'Name='
callvirt System.Text.StringBuilder Append(System.String)
ldloc.0 // SigilCSharp.Target _local0
callvirt System.String get_Name()
callvirt System.Text.StringBuilder Append(System.String)
ldstr ','
callvirt System.Text.StringBuilder Append(System.String)
ldstr 'Hoge='
callvirt System.Text.StringBuilder Append(System.String)
ldloc.0 // SigilCSharp.Target _local0
callvirt System.DateTime get_Hoge()
box System.DateTime
callvirt System.Text.StringBuilder Append(System.Object)
ldstr '}'
callvirt System.Text.StringBuilder Append(System.String)
callvirt System.String ToString()
ret

Target{Id=1,Name=あああああ,Hoge=2016/04/30 17:36:16}

ちゃんと動いていますね。

おわりに

どちらかと言うとSigilというよりはILそのものの解説になってしまいました。

とは言っても、Sigilの命令はILと一対一で対応しており、ILを生成するためのDSLを提供しているわけではないのでどうしてもILの知識は必要になってしまいます。

最後にサンプルコードをのっけて終わりにしたいと思います。

github.com

おわり