2011年12月11日日曜日

続・C# 動的メソッド入れ替え - Sequel: Apply a monkey patch to any static languages on CLR -

申し訳程度の C# 要素です (>_<)

@urasandesu こと、杉浦と申します。はじめましての方ははじめまして!
C# Advent Calendar 2011 の 13 日目を担当させていただくことになりました。よろしくお願いいたします。

ネタは、.NET 開発の中でも日の当たりにくい低レイヤな部分、「アンマネージ API」を取り上げます。

タイトルにもある通り「C# で動的にメソッドを入れ替える」という、マネージコードだけではなかなか実現できないことをやってみることにします。また、元々日本語になっている資料やサンプルコードが少ない分野ですので、これからコンパイラやプロファイラなどをやってみたい方の学習の一助として、もしくはどんなものかちょっと覗いてみたい方への参考になればと思う次第です。
ちなみに、私の Blog のこの記事この記事辺りの続編となりますので、もし興味がありましたらそちらもどうぞ。

それでは始めることにしましょう。あ。冒頭にも書きました通り、C# の Advent Calendar にも関わらず、C# のプログラムは申し訳程度しか現れませんが、よろしければご笑覧くださいませ <(_ _)>

※文中にあるソリューションは以下のリンクからダウンロードできます。ビルド環境は、Visual Studio C++ 2008 + Boost C++ Libraries Ver.1.47.0、あと C++ 側の自動テストに Google Test 1.6.0、C# 側の自動テストに xUnit .NET を使っています。環境を整える際は、各ライブラリのインストールをお願いしますです。


こちらのページ/ソフトウェアを参考にさせていただきました!いつもお世話になっております! (`・ω・ )ゝ
Download Details - Microsoft Download Center - Shared Source Common Language Infrastructure 2.0 Release
Cecil - Mono
ECMA C# and Common Language Infrastructure Standards
Boost C++ Libraries
ファイルをメモリのように「メモリマップドファイルクラス」
@IT:インサイド .NET Framework [改訂版]第2回 アセンブリのアイデンティティ
Chapter 14. Boost.Program_options - Boost 1.47.0
Filesystem Home - Boost 1.47.0
Chapter 1. Boost.ScopeExit - Boost 1.47.0
Pro Git - Pro Git 6.6 Git のさまざまなツール サブモジュール
Fusion GAC API Samples - Junfeng Zhang's Windows Programming Notes - Site Home - MSDN Blogs
xUnit.net - Unit testing framework for C# and .NET (a successor to NUnit)





目次

お題(C#)
ここにテストコードの無いレガシーなシステムがあり、以下の機能を持っているとしましょう。
  ・現在の時刻がお昼休みかどうかを判定し、結果に応じた文字列を標準出力に吐き出す。
  ・現在の曜日について、設定ファイルに休日であると設定された曜日と比較し、結果に応じた文字列を標準出力に吐き出す。
#line 102 "CppTroll\ProfilingApiSample03Target\Program.cs"
using System;
using MyLibrary;
using ThirdPartyLibrary;

namespace ProfilingApiSample03Target
{
    class Program
    {
        static void Main(string[] args)
        {
            LifeInfo.LunchBreak();
            LifeInfo.Holiday();
        }
    }
}

namespace MyLibrary
{
    public static class LifeInfo
    {
        public static void LunchBreak()
        {
            var now = DateTime.Now;
            Console.WriteLine("時刻: " + now.Hour + "\t" + 
                (12 <= now.Hour && now.Hour < 13 ? "お昼休みなう!" : "お仕事なう・・・"));
        }

        public static void Holiday()
        {
            var dayOfWeek = ConfigurationManager.GetProperty("Holiday", DayOfWeek.Sunday);
            var now = DateTime.Now;
            Console.WriteLine("曜日: " + now.DayOfWeek + "\t" + 
                (now.DayOfWeek == dayOfWeek ? "休日なう!" : "お仕事なう・・・"));
        }
    }
}
 
ちなみに、設定ファイルの読み込みに利用しているクラス、ConfigurationManager は、サードパーティ製のライブラリのようで、ソースコードはなく、中身はわかりません。


・・・さて、よくある例で恐縮ですが、このシステムの機能を Web サービスとしても公開したい、という話が出てきました。考えられる対応は以下のようなものでしょう。
a. イチから作り直す
【工数[大]】
同じ機能を持ったシステムを Web サービス用に作り直そう。仕様の整理もできるし、実はあきらめてた別の要件も、今後盛り込みやすくなるかも。

b. 出力された情報だけを使ってとりあえずなんとかする
【工数[小]】
中身にはいっさい手を入れず、出力されてる情報を Web サービス用に整形するだけ。間に合わせだけど、今回だけならなんとかなるでしょう。期間も短いようだし。

c. リファクタリングし、新しいインターフェースを追加できるようにする
【工数[中]】
現状のコードをある程度生かす。今後の拡張性をある程度持たせた上で、Web サービス用のインターフェースを追加したらどうか。レガシーなシステムだと、いくら影響範囲を調べたところで、進めていくうちにどうしても元の機能の書き換えが必要な部分が出てくるリスクもあるんじゃない?例えば、設定ファイルでやってるところを、DB マスタ化しないとどうにもならなくなっちゃったとか。ここサードパーティ製だから嫌な予感するんだよね。

この程度のシステムでしたら、迷わず a. を選んでいただければ良いとは思いますが、色々な状況がありますので、どれになるかはその時次第です。まあ今回は話の都合上、c. で進めることにさせてください (^_^;)
あと、最初に一回りサイクルが回ればあとは何とかなると思いますので、解説は最初のテストが通るまでです。最終形は皆さんへの宿題にさせていただければと思います・・・API の解説に夢中になって忘れていたとも言う・・・orz




レガシーコードをテスト可能にしていくお仕事(C#)
リファクタリングした上で新しい機能を追加するわけですから、何はともあれ自動テストを作り、簡単にリグレッションテストができるようにしておくのが良いでしょう。
コアとなっている LifeInfo クラスは、現在を判定した後、そのまま標準出力に吐き出しているので、これが拡張のし難さに繋がっているように思います。これの現状の動作を固めるのが先決です。
こんなテストを書いてみました。
#line 357 "CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs"
using System;
using System.IO;
using MyLibrary;
using MyLibraryTest.Helper;
using Xunit;

namespace MyLibraryTest
{
    public class LifeInfoTest
    {
        [Fact]
        public void LunchBreakTest01_NowIsLunchBreak()
        {
            using (new ConsoleContext())
            using (var sw = new StringWriter())
            {
                Console.SetOut(sw);
                LifeInfo.LunchBreak();
                Assert.Equal("時刻: 12\tお昼休みなう!" + sw.NewLine, sw.ToString());
            }
        }

        [Fact]
        public void LunchBreakTest02_NowIsNotLunchBreak()
        {
            using (new ConsoleContext())
            using (var sw = new StringWriter())
            {
                Console.SetOut(sw);
                LifeInfo.LunchBreak();
                Assert.Equal("時刻: 13\tお仕事なう・・・" + sw.NewLine, sw.ToString());
            }
        }
        
        // こんな感じでテストが続く
        // ・・・
    }
}
 
370、382 行目で表れる ConsoleContext は、例外が発生しても、乗っ取った Console の標準出力を元に戻せるようにするためのヘルパークラスです。
さあ、実行してみましょう!
C:\CppTroll\Debug>xunit.console.exe ProfilingApiSample03TargetTest.dll /noshadow
xUnit.net console test runner (32-bit .NET 2.0.50727.3625)
Copyright (C) 2007-11 Microsoft Corporation.

xunit.dll:     Version 1.8.0.1545
Test assembly: C:\CppTroll\Debug\ProfilingApiSample03TargetTest.dll

MyLibraryTest.LifeInfoTest.LunchBreakTest01_NowIsHoliday [FAIL]
   Assert.Equal() Failure
   Position: First difference is at position 4
   Expected: 曜日: Sunday       休日なう!

   Actual:   曜日: Tuesday      お仕事なう・・・
   Stack Trace:
      場所 Xunit.Assert.Equal[T](T expected, T actual, IEqualityComparer`1 comparer)
      場所 Xunit.Assert.Equal[T](T expected, T actual)
      場所 MyLibraryTest.LifeInfoTest.LunchBreakTest01_NowIsHoliday() 場所 C:\CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs:行 391

MyLibraryTest.LifeInfoTest.LunchBreakTest01_NowIsLunchBreak [FAIL]
   Assert.Equal() Failure
   Position: First difference is at position 4
   Expected: 時刻: 12   お昼休みなう!

   Actual:   時刻: 7    お仕事なう・・・
   Stack Trace:
      場所 Xunit.Assert.Equal[T](T expected, T actual, IEqualityComparer`1 comparer)
      場所 Xunit.Assert.Equal[T](T expected, T actual)
      場所 MyLibraryTest.LifeInfoTest.LunchBreakTest01_NowIsLunchBreak() 場所 C:\CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs:行 367

MyLibraryTest.LifeInfoTest.LunchBreakTest02_NowIsNotLunchBreak [FAIL]
   Assert.Equal() Failure
   Position: First difference is at position 4
   Expected: 時刻: 13   お仕事なう・・・

   Actual:   時刻: 7    お仕事なう・・・
   Stack Trace:
      場所 Xunit.Assert.Equal[T](T expected, T actual, IEqualityComparer`1 comparer)
      場所 Xunit.Assert.Equal[T](T expected, T actual)
      場所 MyLibraryTest.LifeInfoTest.LunchBreakTest02_NowIsNotLunchBreak() 場所 C:\CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs:行 379

MyLibraryTest.LifeInfoTest.LunchBreakTest02_NowIsNotHoliday [FAIL]
   Assert.Equal() Failure
   Position: First difference is at position 4
   Expected: 曜日: Monday       お仕事なう・・・

   Actual:   曜日: Tuesday      お仕事なう・・・
   Stack Trace:
      場所 Xunit.Assert.Equal[T](T expected, T actual, IEqualityComparer`1 comparer)
      場所 Xunit.Assert.Equal[T](T expected, T actual)
      場所 MyLibraryTest.LifeInfoTest.LunchBreakTest02_NowIsNotHoliday() 場所 C:\CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs:行 403

4 total, 4 failed, 0 skipped, took 0.397 seconds

C:\CppTroll\Debug>
 
はい!テスト通りません!

もうやる前からわかってらっしゃった方も少なくないと思いますが、LifeInfo クラスの中身を見ると、DateTime.Now という環境に依存してしまう情報を直接見ているため、ということがわかります。

じゃあ自動化できないじゃん/(^o^)\

はい・・・と思っていた時期が私にもあったのですが、ある時気づいたのは、現状の動作を保障したいというホワイトボックス的な観点のテストから言えば、実際の処理は、ある DataTime 値による単なる分岐でしかない、ということでした。最終的には、もちろんブラックボックス的な観点のテストが必要ですが、ここでの目的を達成するには、この分岐分の DataTime 値のパターンが用意できれば良いだけということになりますよね。
となると、テスト実行前に DateTime.Now が返す値を固定の値に変更できれば良いわけです。イメージ的には、こんな感じで処理を入れ替えたいです(ビルドは通りません)。
#line 295 "CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs"
        [Fact]
        public void LunchBreakTest01_NowIsLunchBreak()
        {
            using (new ConsoleContext())
            using (var sw = new StringWriter())
            {
                Console.SetOut(sw);
                // Replace the content of DateTime.Now with a mock which always returns fixed value.
                // NOTE: This idea does not work.
                DateTime.Now = () => new DateTime(2011, 12, 13, 12, 00, 00);
                LifeInfo.LunchBreak();
                Assert.Equal("時刻: 12\tお昼休みなう!" + sw.NewLine, sw.ToString());
            }
        }
        
        // こんな感じでテストが続く
        // ・・・
 
304 行目で、DateTime.Now の中身を、常に固定値を返すモックに入れ替える気持ちです。このイメージに近い方法で処理を入れ替えられる仕組みを考えましょう。以下の 2 つの戦略を考えていきます。
  1. 呼び出すクラス側(この例の場合、LifeInfo)で、処理を入れ替えられる仕組みを用意する。
  2. 呼び出されるクラス側(この例の場合、DateTime)で、処理を入れ替えられる仕組みを用意する。
ちなみに 1. は以下で解説する通り、手動である程度シミュレートできますし、2. は Microsoft Research 謹製の Moles という有名なライブラリがあります(Moles のほうは、今回の C# Advent Calendar でも解説される方がいらっしゃりそうですね)ので、私が現在作っているライブラリを待つ必要は全然無いです・・・あれ? (´・ω・`)




レガシーコードにクサビを打ち込むお仕事(C#)
先ほどの 1. に当たる手順を、とりあえず手動でやってみます。
手を入れるといっても、いきなりがっつり変更するのでは、リグレッションテストをする前にデグレが混入する可能性がありますので、なるべく機械的に、かつなるべく変更を局所的に抑えていきましょう。
まずは打ち込むクサビを作成します。こんな感じでいかがでしょう。
#line 87 "CppTroll\ProfilingApiSample03Target\Program.cs"
        // ・・・

namespace System.Wedge
{
    struct WDateTime
    {
        public static class NowGet
        {
            static Func<DateTime> m_body = () => DateTime.Now;
            public static Func<DateTime> Body { get { return m_body; } set { m_body = value; } }
        }
    }
}
 
Now プロパティの getter を入れ替えたいので、名前は安易に決めています。入れ子にされた型になっているのは、ジェネリック メソッドに対応させるときに都合が良さそうだから、ぐらいの理由です。デフォルト値は、元の処理の通り、DateTime.Now を返すようにしておきましょう(94 行目)
さて、そうしましたら、"DateTime.Now" を "WDateTime.NowGet.Body()" に一括置換しましょう。
#line 68 "CppTroll\ProfilingApiSample03Target\Program.cs"
        // ・・・
    public static class LifeInfo
    {
        public static void LunchBreak()
        {
            var now = WDateTime.NowGet.Body();
            Console.WriteLine("時刻: " + now.Hour + "\t" + 
                (12 <= now.Hour && now.Hour < 13 ? "お昼休みなう!" : "お仕事なう・・・"));
        }

        public static void Holiday()
        {
            var dayOfWeek = ConfigurationManager.GetProperty("Holiday", DayOfWeek.Sunday);
            var now = WDateTime.NowGet.Body();
            Console.WriteLine("曜日: " + now.DayOfWeek + "\t" + 
                (now.DayOfWeek == dayOfWeek ? "休日なう!" : "お仕事なう・・・"));
        }
    }
 
73 行目、83 行目が置換されました。ビルドをしますと・・・問題無さそうですね。
ところでこの方法を手動でシミュレートするには、SCM(バージョン管理システム) があると便利です。git のような DVCS(分散型バージョン管理システム) ですと、作業単位毎にコミットが作れますので、もし失敗しても柔軟に対応することができると思います。
テストコードはこんな感じになりました。
#line 159 "CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs"
        [Fact]
        public void LunchBreakTest01_NowIsLunchBreak()
        {
            using (new WDateTimeContext.NowGet())
            using (new ConsoleContext())
            using (var sw = new StringWriter())
            {
                Console.SetOut(sw);
                WDateTime.NowGet.Body = () => new DateTime(2011, 12, 13, 12, 00, 00);
                LifeInfo.LunchBreak();
                Assert.Equal("時刻: 12\tお昼休みなう!" + sw.NewLine, sw.ToString());
            }
        }
        
        // こんな感じでテストが続く
        // ・・・
 
167 行目で常に固定値を返すモックに処理を入れ替えています。イメージにあった書き方に大分近いと思いますがいかがでしょうか?
using 文が追加されていますが(162 行目)、使ったクサビは元に戻しやすいよう、このような IDisposable を実装した Context クラスや Rent パターンを使ったヘルパークラスがあると便利だと思います。
これを実行してみると・・・
C:\CppTroll\Debug>xunit.console.exe ProfilingApiSample03TargetTest.dll /noshadow
xUnit.net console test runner (32-bit .NET 2.0.50727.3625)
Copyright (C) 2007-11 Microsoft Corporation.

xunit.dll:     Version 1.8.0.1545
Test assembly: C:\CppTroll\Debug\ProfilingApiSample03TargetTest.dll

4 total, 0 failed, 0 skipped, took 0.372 seconds

C:\CppTroll\Debug>
 
はい!テスト通りました!

後は煮るなり焼くなり、お好きにリファクタリングしていただけると思います。
なお、実際の場面ですと、一括置換の際に引数をかわさないといけなかったり、改行が途中にあったりで一筋縄ではいかないかもしれません。そうすると・・・ちょっとは私のライブラリも存在意義が出てくるかもしれませんね (^_^;)




レガシーコードにコソドロを紛れ込ませるお仕事(C++/C#)
戦略の 2. に当たる手順です。いよいよ本題のアンマネージ API ですね!リファレンスに載っている API 一覧に従えば、今回利用しているのは以下の API のものとなります。興味がある方は、MSDN を眺めていただければと思います。
サンプルコードが全く無いところが悲しいですけどね・・・(-_-;)
Fusion API
ランタイムホストがアプリケーションのリソースにアクセスするための手段を提供する。
例えば、アセンブリの完全修飾名からファイルパスを取得、など。

Host API
アンマネージアプリケーションに CLR を統合するための手段を提供する。
例えば、AppDomain の管理、CLI で規定されたメタデータを含む PE ファイル生成、など。

Meta Data API
CLR によって読み込まれる型を使用せずに、メタデータにアクセス/生成するための手段を提供する。
例えば、アセンブリに定義されたモジュールの読み取り、新しい型の追加、など。

Profiling API
CLR によってプログラムの実行を監視するための手段を提供する。
例えば、メモリリークの追跡、パフォーマンスやカバレッジの計測のための処理の書き換え、など。

Strong Naming API
アセンブリの厳密名署名を管理するための手段を提供する。
例えば、公開キーと秘密キーのペアの生成、アセンブリの署名、など

実行時に C# のメソッドを動的に入れ替えるのは、前回もやっていますので、今回は差分を見ていきましょう。
大きく異なるのは、入れ替えた部分に C# 側からアクセスできるよう、イメージにあったようなインターフェースを提供するスタブを作るプログラムを作る必要があることです。
DateTime.Now の中身を入れ替える基本的な戦略は、前回と同じく、JIT 開始をフックしてやることにしましょう。
さて、今回は元のソースコードには全く手を入れません。
#line 28 "CppTroll\ProfilingApiSample03Target\Program.cs"
        // ・・・
    public static class LifeInfo
    {
        public static void LunchBreak()
        {
            var now = DateTime.Now;
            Console.WriteLine("時刻: " + now.Hour + "\t" + 
                (12 <= now.Hour && now.Hour < 13 ? "お昼休みなう!" : "お仕事なう・・・"));
        }

        public static void Holiday()
        {
            var dayOfWeek = ConfigurationManager.GetProperty("Holiday", DayOfWeek.Sunday);
            var now = DateTime.Now;
            Console.WriteLine("曜日: " + now.DayOfWeek + "\t" + 
                (now.DayOfWeek == dayOfWeek ? "休日なう!" : "お仕事なう・・・"));
        }
    }
 
テストコードのほうは、これから作成するスタブ生成プログラムから吐き出されるスタブを使ったつもりで書きます(このままではビルドは通りません)。
#line 21 "CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs"
        [Fact]
        public void LunchBreakTest01_NowIsLunchBreak()
        {
            using (new PDateTimeContext.NowGet())
            using (new ConsoleContext())
            using (var sw = new StringWriter())
            {
                Console.SetOut(sw);
                PDateTime.NowGet.Body = () => new DateTime(2011, 12, 13, 12, 00, 00);
                LifeInfo.LunchBreak();
                Assert.Equal("時刻: 12\tお昼休みなう!" + sw.NewLine, sw.ToString());
            }
        }
        
        // こんな感じでテストが続く
        // ・・・
 
では各プログラムを見ていきましょう!
  1. スタブを生成するプログラム
  2. 処理を入れ替えるプログラム
  3. 結果

なお、.NET コア機能に手を入れる必要があったり、Func<TResult> のような汎用ジェネリック デリゲートを使う必要があったりで、ちょっと大変ですが、とりあえずサンプルということで、分岐も状態もほとんどない、バッチ的なコードになってます。
長くなりますので、結果をご覧になりたい方はこちらへどうぞ ~(´ー`~)


スタブを生成するプログラム
最終形になると、引数に渡されたアセンブリ(例えば、mscorlib)を解析し、全ての型を解析したり、オプションによって作成するスタブを制御できる形になるかと思います。Boost.Program_options のような便利なライブラリを使えば、私のような初学者でも、C++ で難しいことをやらずに済むのはありがたいですね。
ただ、今回はサンプルですので、利用するアセンブリは決め打ちとしましょう。
#line 62 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
        // Preapre Fusion API to retrieve creating its instance method.
        path corSystemDirectoryPath;
        path fusionPath;
        {
            WCHAR buffer[MAX_PATH] = { 0 };
            DWORD length = 0;
            hr = ::GetCORSystemDirectory(buffer, MAX_PATH, &length);
            if (FAILED(hr)) 
                BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));

            corSystemDirectoryPath = buffer;
            fusionPath = buffer;
            fusionPath /= L"fusion.dll";
        }

        HMODULE hmodCorEE = ::LoadLibraryW(fusionPath.c_str());
        if (!hmodCorEE)
            BOOST_THROW_EXCEPTION(CppAnonymSystemException(::GetLastError()));
        BOOST_SCOPE_EXIT((hmodCorEE))
        {
            ::FreeLibrary(hmodCorEE);
        }
        BOOST_SCOPE_EXIT_END

        typedef HRESULT (__stdcall *CreateAsmCachePtr)(IAssemblyCache **ppAsmCache, DWORD dwReserved);

        CreateAsmCachePtr pfnCreateAsmCache = NULL;
        pfnCreateAsmCache = reinterpret_cast<CreateAsmCachePtr>(
                                        ::GetProcAddress(hmodCorEE, "CreateAssemblyCache"));
        if (!pfnCreateAsmCache)
            BOOST_THROW_EXCEPTION(CppAnonymSystemException(::GetLastError()));
        
        CComPtr<IAssemblyCache> pAsmCache;
        hr = pfnCreateAsmCache(&pAsmCache, 0);
        if (FAILED(hr)) 
            BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
 
Fusion API の準備です。この API も、インスタンスを生成するメソッドが見つからなくて困ったパターンです。MSDN では、mscoree.dll が必要なことになっていますし、頼りの SSCLI でも、LoadLibraryW する DLL は mscoree.dll になっているというトラップ・・・(-_-;)
正解は、こちらの Blog にもある通り、CLR のシステムディレクトリにある、fusion.dll となります。今後は、Dependency Walker の grep 版みたいなツールが必要になってくるのかもしれませんね。
68 行目で CLR のシステムディレクトリを取得したら、それを Boost.FileSystem を使って fusion.dll のフルパスを作成します。77 行目で取得したハンドルは、Boost.ScopeExit に後処理を任せ、次に進みましょう。今回必要になるのは、IAssemblyCache オブジェクトになりますので、そのインスタンスを生成するメソッドを 89 行目で参照できるようにしておきます。実際にインスタンスを生成しているのは 95 行目ですね。
#line 101 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
        // Retrieve the place of System.Core.dll from GAC with Fusion API.
        path systemCorePath;
        {
            WCHAR buffer[MAX_PATH] = { 0 };
            ASSEMBLY_INFO asmInfo;
            ::ZeroMemory(&asmInfo, sizeof(ASSEMBLY_INFO));
            asmInfo.cbAssemblyInfo = sizeof(ASSEMBLY_INFO);
            asmInfo.pszCurrentAssemblyPathBuf = buffer;
            asmInfo.cchBuf = MAX_PATH;
            hr = pAsmCache->QueryAssemblyInfo(0, L"System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL", &asmInfo);
            if (FAILED(hr)) 
                BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
            
            D_WCOUT1(L"System.Core is here: %|1$s|", buffer);
            systemCorePath = buffer;
        }
 
Func<TResult> 汎用ジェネリック デリゲートを参照設定したいですので、それを定義した System.Core への物理パスを、Fusion API を使って GAC に問い合わせます(110 行目)。ちなみに、Mono.Cecil なんかだと、Portability が必要になるためか、この辺りは完全に手動構築になっていました。そもそも、アンマネージ API に当たる機能が、Mono にあるかどうか怪しいのですが、低レイヤな部分を移植可能にしておくのは一筋縄では行かなさそうですね。
#line 137 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
        // Get TypeDef records to add to TypeRef table.
        mdTypeDef mdtdFunc1 = mdTypeDefNil;
        hr = pImpSystemCore->FindTypeDefByName(L"System.Func`1", NULL, &mdtdFunc1);
        if (FAILED(hr))
            BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
        
        D_COUT1("Token of TypeDef for System.Func<T>: 0x%|1$08X|", mdtdFunc1);
 
得意の Meta Data API を使い、型の定義を表す TypeDef テーブルのレコードを名前で探します(139 行目)。この API は以前も出てきましたが、ジェネリック型ということで再登場です。指定する型名は、[ジェネリック型のパラメータを取り除いた型名]`[ジェネリック型のパラメータ数]が決まりのように見えますが・・・実はメタデータ的には、TypeDef から、ジェネリック型のパラメータの定義を表す GenericParam への参照は存在しません。その逆はもちろんあるのですが。
ですので、ジェネリック型かどうかをちゃんと調べるには、必ず GenericParam の列挙を試みないといけないわけで、正直めんどくさいかも・・・ (-_-;)
まあしかしこの辺り、後方互換を目指す上では仕方がなかったんでしょうね。System.Type クラスにある、IsGenericType プロパティを参考に、うまく抽象化できればと思います。
#line 478 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
        // Add to TypeRef table with the name.
        mdTypeRef mdtrFunc1 = mdTypeRefNil;
        hr = pEmtMSCorLibPrig->DefineTypeRefByName(mdarSystemCore, L"System.Func`1", &mdtrFunc1);
        if (FAILED(hr))
            BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
        
        D_COUT1("Token of TypeRef for System.Func`1: 0x%|1$08X|", mdtrFunc1);
 
一気に 300 ステップほど進みました (^_^;)
いかに決まりきったことをやっているかという・・・早くライブラリ化しないとですね・・・。
型の参照を表す TypeRef テーブルのレコードの作成にジェネリック型を作成する場合も、定義を探す時と同様です(480 行目)。
え、ジェネリック型インスタンスはどうやって作るのさ、と思うわけですが、これには TypeSpec テーブルに、改めてレコードを作成することになります。
#line 520 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
        // Add TypeRef records retrieved in above to TypeSpec table.
        mdTypeSpec mdtsSystemFunc1DateTime = mdTypeSpecNil;
        {
            COR_SIGNATURE pSigBlob[] = {
                ELEMENT_TYPE_GENERICINST,       // TYPE: GENERICINST
                ELEMENT_TYPE_CLASS,             //       CLASS
                0x05,                           //       TypeRef: 0x01000001(System.Func`1)
                1,                              //       Generics Arguments Count: 1
                ELEMENT_TYPE_VALUETYPE,         //       VALUETYPE
                0x0D                            //       TypeRef: 0x01000003(System.DateTime)
            };                                  
            ULONG sigBlobSize = sizeof(pSigBlob) / sizeof(COR_SIGNATURE);
            hr = pEmtMSCorLibPrig->GetTokenFromTypeSpec(pSigBlob, sigBlobSize, 
                                                        &mdtsSystemFunc1DateTime);
            if (FAILED(hr))
                BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
        }
        
        D_COUT1("Token of TypeSpec for System.Func<DateTime>: 0x%|1$08X|", mdtsSystemFunc1DateTime);
 
532 行目にある TypeSpec テーブルへレコードを追加するメソッドは、GetTokenFromTypeSpec というやることに反した名前をしています。なんで他と同じ Define** にしなかったし (-_-;) また、とうとうシグネチャが現れてしまいました(523 ~ 530 行目)。カスタム属性の時も思いましたが、インターフェースに困ったらシグネチャという感じです。
Mono.Cecil や System.Reflection.Emit 名前空間にある API が、いかに使いやすいかと身にしみて思います。
もう System.Type クラスにある、MakeGenericType メソッドの使い方が複雑だなんて言わないよ絶対。
#line 615 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
        // Sign to this assembly with Strong Naming API.
        path msCorLibPrigKeyPairPath = L"..\\ProfilingApiSample03Patch\\ProfilingApiSample03Patch.snk";
        auto_ptr<BYTE> pMSCorLibPrigKeyPair;
        DWORD msCorLibPrigKeyPairSize = 0;
        auto_ptr<PublicKeyBlob> pMSCorLibPrigPubKey;
        DWORD msCorLibPrigPubKeySize = 0;
        {
            HANDLE hSnk = NULL;
            hSnk = ::CreateFileW(msCorLibPrigKeyPairPath.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, 
                                 OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL);
            if (hSnk == INVALID_HANDLE_VALUE)
                BOOST_THROW_EXCEPTION(CppAnonymSystemException(::GetLastError()));
            BOOST_SCOPE_EXIT((hSnk))
            {
                ::CloseHandle(hSnk);
            }
            BOOST_SCOPE_EXIT_END
            
            msCorLibPrigKeyPairSize = ::GetFileSize(hSnk, NULL);
            if (msCorLibPrigKeyPairSize == -1)
                BOOST_THROW_EXCEPTION(CppAnonymSystemException(::GetLastError()));

            pMSCorLibPrigKeyPair = auto_ptr<BYTE>(new BYTE[msCorLibPrigKeyPairSize]);   
            if (::ReadFile(hSnk, pMSCorLibPrigKeyPair.get(), 
                        msCorLibPrigKeyPairSize, &msCorLibPrigKeyPairSize, NULL) == FALSE)
                BOOST_THROW_EXCEPTION(CppAnonymSystemException(::GetLastError()));

            BYTE *pPubKey = NULL;
            if (!::StrongNameGetPublicKey(NULL, pMSCorLibPrigKeyPair.get(), 
                              msCorLibPrigKeyPairSize, &pPubKey, &msCorLibPrigPubKeySize))
                BOOST_THROW_EXCEPTION(CppAnonymCOMException(::StrongNameErrorInfo()));
            
            pMSCorLibPrigPubKey = auto_ptr<PublicKeyBlob>(
                            reinterpret_cast<PublicKeyBlob*>(new BYTE[msCorLibPrigPubKeySize]));
            ::memcpy_s(pMSCorLibPrigPubKey.get(), msCorLibPrigPubKeySize, pPubKey, 
                       msCorLibPrigPubKeySize);

            if (msCorLibPrigPubKeySize)
                ::StrongNameFreeBuffer(pPubKey);
        }
 
mscorlib のような厳密な名前を持っているアセンブリには、厳密な名前を持っているアセンブリしか参照させることはできません。今回作成するスタブにも署名が必要になります。その名の通り、Strong Naming API を使いましょう。あらかじめ作成しておいたキーペアファイルを読み込み(623、633、638 行目)、そこから公開鍵を取得しておくことで(643 行目)、署名のための準備をしておきます。
#line 750 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
        // Create NestedClass records.
        mdTypeDef mdtdNowGet = mdTypeDefNil;
        hr = pEmtMSCorLibPrig->DefineNestedType(L"NowGet", 
                                                tdAbstract | tdAnsiClass | 
                                                tdSealed | tdNestedPublic | tdBeforeFieldInit, 
                                                mdtrObject, NULL, mdtdPDateTime, &mdtdNowGet);
        if (FAILED(hr))
            BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
        
        D_COUT1("Token of TypeDef for System.Prig.PDateTime.NowGet: 0x%|1$08X|", mdtdNowGet);
 
入れ子にされた型の定義です(752 行目)。入れ子にされた型の名前には、名前空間や外側の型の名前は必要ないです・・・特筆すべきことでは無かったかも (^_^;)
#line 763 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
        // Create Field records.
        mdFieldDef mdfdNowGetm_body = mdFieldDefNil;
        {
            COR_SIGNATURE pSigBlob[] = {
                IMAGE_CEE_CS_CALLCONV_FIELD,    // FIELD
                ELEMENT_TYPE_GENERICINST,       // TYPE: GENERICINST
                ELEMENT_TYPE_CLASS,             //       CLASS
                0x05,                           //       TypeRef: 0x01000001(System.Func`1)
                1,                              //       Generics Arguments Count: 1
                ELEMENT_TYPE_VALUETYPE,         //       VALUETYPE
                0x0D                            //       TypeRef: 0x01000003(System.DateTime)
            };                                  
            ULONG sigBlobSize = sizeof(pSigBlob) / sizeof(COR_SIGNATURE);
            hr = pEmtMSCorLibPrig->DefineField(mdtdNowGet, L"m_body", 
                                               fdPrivate | fdStatic, 
                                               pSigBlob, sigBlobSize, 
                                               ELEMENT_TYPE_VOID, NULL, 0, &mdfdNowGetm_body);
            if (FAILED(hr))
                BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
        }
            
        D_COUT1("Token of FieldDef for System.Prig.PDateTime.NowGet.m_body: 0x%|1$08X|", mdfdNowGetm_body);
 
フィールドの定義です(776 行目)。
#line 875 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
        // Create Param records.
        mdParamDef mdpdNowGetset_Body0value = mdParamDefNil;
        hr = pEmtMSCorLibPrig->DefineParam(mdmdNowGetset_Body, 0, 
                                           L"value", 0, 
                                           ELEMENT_TYPE_VOID, NULL, 0, 
                                           &mdpdNowGetset_Body0value);
        if (FAILED(hr))
            BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
            
        D_COUT1("Token of ParamDef for System.Prig.PDateTime.NowGet.set_Body, 0: value: 0x%|1$08X|", mdpdNowGetset_Body0value);
 
メソッドのパラメータの定義です(877 行目)。使うのは Body プロパティの setter 側ですね。static メソッドですので、パラメータの配置は 0 番目からです。
#line 888 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
        // Create StandAloneSig records.
        mdSignature mdsNowGetInitializeget_BodyLocals = mdSignatureNil;
        {
            COR_SIGNATURE pSigBlob[] = {
                IMAGE_CEE_CS_CALLCONV_LOCAL_SIG,// LOCAL_SIG   
                0x01,                           // Count: 1
                ELEMENT_TYPE_VALUETYPE,         // Type[0]: VALUETYPE
                0x0D                            //          TypeRef: 0x01000003(System.DateTime)
            };                                  
            ULONG sigBlobSize = sizeof(pSigBlob) / sizeof(COR_SIGNATURE);
            hr = pEmtMSCorLibPrig->GetTokenFromSig(pSigBlob, sigBlobSize, 
                                                   &mdsNowGetInitializeget_BodyLocals);
            if (FAILED(hr))
                BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
        }
            
        D_COUT1("Token of StandAloneSig for System.Prig.PDateTime.NowGet.Initializeget_Body Locals: 0x%|1$08X|", mdsNowGetInitializeget_BodyLocals);
 
ローカル変数の定義です(898 行目)。
こんな簡単なプログラムにローカル変数必要?と思われる方もいらっしゃるかもしれませんが・・・。
実は当初、DateTime.Now の getter 側を新しいメソッドとして JIT 時に複製し(DateTime.CopiedNow みたいなのを作成)、それをスタブに参照させておくという戦略を取ったのですが、実行してみると MissingMethodException の嵐で・・・。泣く泣く元のメソッドの内容を表す IL ストリームについて、**Def → **Ref 変換しながらコピーするという方針転換をしました。
DateTime.Now の中身って、Version=2.0.0.0 までは DateTime.UtcNow.ToLocalTime() なので、値型の持つメソッド使うのに、一度変数に受けてから ldloca しないといけないという・・・。

あ。ちなみにこの方法、IL ストリーム中に private なものが混じってきたら終わりなので、また新しい戦略、考えます (ToT)
#line 908 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
        // Create Property records.
        mdProperty mdpNowGetBody = mdPropertyNil;
        {
            COR_SIGNATURE pSigBlob[] = {
                IMAGE_CEE_CS_CALLCONV_PROPERTY, // PROPERTY
                0,                              // ParamCount: 0
                ELEMENT_TYPE_GENERICINST,       // TYPE: GENERICINST
                ELEMENT_TYPE_CLASS,             //       CLASS
                0x05,                           //       TypeRef: 0x01000001(System.Func`1)
                1,                              //       Generics Arguments Count: 1
                ELEMENT_TYPE_VALUETYPE,         //       VALUETYPE
                0x0D                            //       TypeRef: 0x01000003(System.DateTime)
            };                                  
            ULONG sigBlobSize = sizeof(pSigBlob) / sizeof(COR_SIGNATURE);
            hr = pEmtMSCorLibPrig->DefineProperty(mdtdNowGet, L"Body", 
                                                  0, 
                                                  pSigBlob, sigBlobSize, 
                                                  ELEMENT_TYPE_VOID, NULL, 0, 
                                                  mdmdNowGetset_Body, 
                                                  mdmdNowGetget_Body, 
                                                  NULL, &mdpNowGetBody);
            if (FAILED(hr))
                BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
        }
            
        D_COUT1("Token of Property for System.Prig.PDateTime.NowGet.Body: 0x%|1$08X|", mdpNowGetBody);
 
プロパティの定義です(922 行目)。ここまでで作成した、setter と getter を指定し、DefineProperty を呼ぶだけですね。
次に続く IL は代わり映えしないので割愛したいと思います。
PE フォーマットファイルは・・・若干新しいものが登場しますね。
#line 1165 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
            {
                // System.Prig.PDateTime.NowGet.Initializeget_Body method has Fat format header.
                // Note that SetLocalVarSigTok is called with StandAloneSig for local variables, 
                // and GetSectionBlock is called with setting to DWORD size alignment.
                COR_ILMETHOD_FAT fatHeader;
                ::ZeroMemory(&fatHeader, sizeof(COR_ILMETHOD_FAT));
                fatHeader.SetMaxStack(1);
                fatHeader.SetCodeSize(mbNowGetInitializeget_Body.Size());
                fatHeader.SetLocalVarSigTok(mdsNowGetInitializeget_BodyLocals);
                fatHeader.SetFlags(CorILMethod_InitLocals);
                
                unsigned headerSize = COR_ILMETHOD::Size(&fatHeader, false);
                unsigned totalSize = headerSize + mbNowGetInitializeget_Body.Size();

                BYTE *pBuffer = NULL;
                hr = pCeeFileGen->GetSectionBlock(textSection, totalSize, sizeof(DWORD), 
                                                  reinterpret_cast<void**>(&pBuffer));
                if (FAILED(hr))
                    BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));

                ULONG offset = 0;
                hr = pCeeFileGen->GetSectionDataLen(textSection, &offset);
                if (FAILED(hr))
                    BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));

                offset -= totalSize;
                ULONG codeRVA = 0;
                hr = pCeeFileGen->GetMethodRVA(ceeFile, offset, &codeRVA);
                if (FAILED(hr))
                    BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
                
                hr = pEmtMSCorLibPrig->SetMethodProps(mdmdNowGetInitializeget_Body, -1, codeRVA, 0);
                if (FAILED(hr))
                    BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));

                pBuffer += COR_ILMETHOD::Emit(headerSize, &fatHeader, false, pBuffer);
                ::memcpy_s(pBuffer, totalSize - headerSize, mbNowGetInitializeget_Body.Ptr(), 
                           mbNowGetInitializeget_Body.Size());
            }
 
ローカル変数を持つメソッドは、問答無用で Fat フォーマットなメソッドになります。基本は、Tiny フォーマットなメソッドと同じなのですが、作っておいたローカル変数を表す StandAlongSig の指定(1173 行目)と、それらをデフォルト初期化する指定(1174 行目)、4 バイトアライメントで領域を生成すること(1180 行目)が追加で必要になります。ちなみに ICeeFileGen が属すのは Host API です。名称から連想しにくいためか、私なんかは探すときにいつもあっち行ったりこっち行ったりしてしまいます (>_<)
#line 1207 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
            // Reserve a strong name area to sign to the assembly.
            {
                DWORD msCorLibPrigSignReserveSize = 0;
                if (!::StrongNameSignatureSize(
                                    reinterpret_cast<PBYTE>(pMSCorLibPrigPubKey.get()), 
                                    msCorLibPrigPubKeySize, &msCorLibPrigSignReserveSize))
                    BOOST_THROW_EXCEPTION(CppAnonymCOMException(::StrongNameErrorInfo()));

                BYTE *pBuffer = NULL;
                hr = pCeeFileGen->GetSectionBlock(textSection, 
                                                  msCorLibPrigSignReserveSize, 1, 
                                                  reinterpret_cast<void**>(&pBuffer));
                if (FAILED(hr))
                    BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));

                ULONG offset = 0;
                hr = pCeeFileGen->GetSectionDataLen(textSection, &offset);
                if (FAILED(hr))
                    BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
                
                offset -= msCorLibPrigSignReserveSize;
                ULONG codeRVA = 0;
                hr = pCeeFileGen->GetMethodRVA(ceeFile, offset, &codeRVA);
                if (FAILED(hr))
                    BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
                
                ::ZeroMemory(pBuffer, msCorLibPrigSignReserveSize);
                
                hr = pCeeFileGen->SetStrongNameEntry(ceeFile, msCorLibPrigSignReserveSize, codeRVA);
                if (FAILED(hr))
                    BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
            }
 
アセンブリに署名をするための領域を予約します。取得しておいた公開鍵から、署名のための領域サイズを取得し(1210 行目)、.text セクションに続けて配置します(1215 ~ 1231 行目)。そして SetStrongNameEntry を使い、紐付けを行います(1235 行目)。
#line 1257 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
            // Sign to the assembly with Strong Naming API.
            if (!::StrongNameSignatureGenerationEx(moduleNameOfMSCorLibPrig, NULL, 
                                       pMSCorLibPrigKeyPair.get(), msCorLibPrigKeyPairSize, 
                                       NULL, NULL, 0))
                BOOST_THROW_EXCEPTION(CppAnonymCOMException(::StrongNameErrorInfo()));
 
最後に署名をして完成です!早速実行してみましょう!
C:\CppTroll\Debug>ProfilingApiSample03Stubber.exe
System.Core is here: C:\WINDOWS\assembly\GAC_MSIL\System.Core\3.5.0.0__b77a5c561934e089\System.Core.dll
Token of TypeDef for System.Func<T>: 0x02000058
Token of MethodDef for System.Func<T>..ctor: 0x06000232
Token of Assembly for System.Core.dll: 0x20000001
Assembly Name: System.Core
mscorlib is here: C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll
Token of TypeDef for System.Object: 0x02000002
Token of TypeDef for System.DateTime: 0x02000032
Token of TypeDef for System.Runtime.CompilerServices.CompilationRelaxationsAttribute: 0x020005CC
Token of TypeDef for System.Runtime.CompilerServices.RuntimeCompatibilityAttribute: 0x020005CC
Token of MethodDef for System.DateTime.get_Now: 0x060002D1
Token of MethodDef for System.Runtime.CompilerServices.CompilationRelaxationsAttribute..ctor: 0x0600374D
Token of MethodDef for System.Runtime.CompilerServices.RuntimeCompatibilityAttribute..ctor: 0x06003772
Token of Assembly for mscorlib.dll: 0x20000001
Assembly Name: mscorlib
Token of AssemblyRef for mscorlib.dll: 0x23000001
Token of AssemblyRef for System.Core.dll: 0x23000002
Token of TypeRef for System.Func`1: 0x01000001
Token of TypeRef for System.Object: 0x01000002
Token of TypeRef for System.DateTime: 0x01000003
Token of TypeRef for System.Runtime.CompilerServices.CompilationRelaxationsAttribute: 0x01000004
Token of TypeRef for System.Runtime.CompilerServices.RuntimeCompatibilityAttribute: 0x01000005
Token of TypeSpec for System.Func<DateTime>: 0x1B000001
Token of MemberRef for System.Func<DateTime>..ctor: 0x0A000001
Token of MemberRef for System.Runtime.CompilerServices.CompilationRelaxationsAttribute..ctor: 0x0A000002
Token of MemberRef for System.Runtime.CompilerServices.RuntimeCompatibilityAttribute..ctor: 0x0A000003
Token of MemberRef for System.DateTime.get_UtcNow: 0x0A000004
Token of MemberRef for System.DateTime.ToLocalTime: 0x0A000005
Public Key Blob of mscorlib.Prig:
    SigAlgID: 0x00002400
    HashAlgID: 0x00008004
    Public Key: 0602000000240000525341310004000001000100B31D0603CEE69405E9D120F774839A632ABE14EB7E53812300ACF21778579F95720DE1B3F1E98BA0282B947D0FC1B177CD1BD5DFA2C781261ACE5C9D597F1CAB4565FA557C86AF9F5B550E1F3B88B70CB0C1B22E1413C2DCCD6352C4593FAF6E7FC7B2CB41A7744FDC097F4649396594F4C840429AA86D8B0EE48C5DF81613BD
    Raw data: 0024000004800000940000000602000000240000525341310004000001000100B31D0603CEE69405E9D120F774839A632ABE14EB7E53812300ACF21778579F95720DE1B3F1E98BA0282B947D0FC1B177CD1BD5DFA2C781261ACE5C9D597F1CAB4565FA557C86AF9F5B550E1F3B88B70CB0C1B22E1413C2DCCD6352C4593FAF6E7FC7B2CB41A7744FDC097F4649396594F4C840429AA86D8B0EE48C5DF81613BD
Token of Assembly for mscorlib.Prig: 0x20000001
Token of CustomAttribute for System.Runtime.CompilerServices.CompilationRelaxationsAttribute: 0x0C000001
Token of CustomAttribute for System.Runtime.CompilerServices.RuntimeCompatibilityAttribute: 0x0C000002
Token of TypeDef for System.Prig.PDateTime: 0x02000002
Token of TypeDef for System.Prig.PDateTime.NowGet: 0x02000003
Token of FieldDef for System.Prig.PDateTime.NowGet.m_body: 0x04000001
Token of MethodDef for System.Prig.PDateTime.NowGet.get_Body: 0x06000001
Token of MethodDef for System.Prig.PDateTime.NowGet.set_Body: 0x06000002
Token of MethodDef for System.Prig.PDateTime.NowGet..cctor: 0x06000003
Token of MethodDef for System.Prig.PDateTime.NowGet.Initializeget_Body: 0x06000004
Token of ParamDef for System.Prig.PDateTime.NowGet.set_Body, 0: value: 0x08000001
Token of StandAloneSig for System.Prig.PDateTime.NowGet.Initializeget_Body Locals: 0x11000001
Token of Property for System.Prig.PDateTime.NowGet.Body: 0x17000001

C:\CppTroll\Debug>
 
GitHub に上がっているものは OUTPUT_DEBUG が有効になっていますので、こんな感じでベロベロと情報が出力されます。例外が発生しなければ、同フォルダに、処理を盗むコソドロを紛れ込ませた mscorlib.Prig.dll が生成されているはずです。中身を Reflector で確認するとこんな感じ。

え、今回の処理だったら、普通に C# で書けるって?・・・はい・・・確かに・・・そうですよね・・・ (´・ω・`)


2. 処理を入れ替えるプログラム
プロファイラの記述にマネージコードが使えればいいのですが、MSDN にもあるとおり、そううまくはいかないみたいですので、きっとスタブ作るプログラム書いたのも無駄にはならないと信じましょう (^_^;)
さて、処理を入れ替えるためのプロファイラです。こちらも前回からの差分だけ見ていきます。
#line 58 "CppTroll\ProfilingApiSample03\ExeWeaver3.cpp"
    // Initialize the profiling API.
    hr = pICorProfilerInfoUnk->QueryInterface(IID_ICorProfilerInfo2, 
                                              reinterpret_cast<void**>(&m_pInfo));
    if (FAILED(hr)) 
        BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));


    // Set a value that specifies the types of events for which the profiler 
    // wants to receive notification from CLR.
    // NOTE: If you want to profile core APIs such as types in mscorlib, 
    //       you should set COR_PRF_USE_PROFILE_IMAGES.
    DWORD event_ = COR_PRF_MONITOR_MODULE_LOADS | 
                   COR_PRF_MONITOR_JIT_COMPILATION | 
                   COR_PRF_USE_PROFILE_IMAGES;
    hr = m_pInfo->SetEventMask(event_);
    if (FAILED(hr)) 
        BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
 
mscorlib のような標準ライブラリを含めてプロファイルする時は、COR_PRF_USE_PROFILE_IMAGES フラグを指定して拡張イメージ検索を有効にする必要があります(69 行目)。後はほとんど同じですね。
#line 99 "CppTroll\ProfilingApiSample03\ExeWeaver3.cpp"
    // Convert ModuleID to the name.
    LPCBYTE pBaseLoadAddress = NULL;
    WCHAR modName[MAX_SYM_NAME] = { 0 };
    ULONG modNameSize = sizeof(modName);
    AssemblyID asmId = 0;
    
    hr = m_pInfo->GetModuleInfo(moduleId, &pBaseLoadAddress, 
                                modNameSize, &modNameSize, modName, &asmId);
    if (hr != CORPROF_E_DATAINCOMPLETE && FAILED(hr))
        BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
    

    // If target module is detected, the object implemented IMetaDataEmit is initialized 
    // to use the following process.
    path modPath(modName);

    V_WCOUT1(L"ModuleLoadStarted: %|1$s|", modName);
    if (modPath.filename().wstring() == MODULE_NAME_OF_MS_COR_LIB)
    {
        V_WCOUT(L"The target module is detected. Getting module meta data is started.");

        hr = m_pInfo->GetModuleMetaData(moduleId, ofRead | ofWrite, IID_IMetaDataEmit2, 
                                        reinterpret_cast<IUnknown **>(&m_pEmtMSCorLib));
        if (hr != CORPROF_E_DATAINCOMPLETE && FAILED(hr))
            BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
    }
 
プロファイル中に、メモリ上のアセンブリを書き換えますので、読み書きモードで Meta Data API への参照を取得しておきます。利用する Profiling API によっては、まだ完全にデータが揃っていないことを示す CORPROF_E_DATAINCOMPLETE を返してくることもありますが、必要なデータさえあるのであれば無視してしまって良いでしょう。
さて・・・続き・・・と思いましたが、後は以前解説した API とスタブ生成の時に使用した API しか使っていませんので割愛します。
書き換えのイメージはこんな感じになります。
// Before
public struct DateTime
{
    public static DateTime Now
    {
        get
        {
            return UtcNow.ToLocalTime();
        }
    }
}

↓↓↓↓

// After
public struct DateTime
{
    public static DateTime Now
    {
        get
        {
            return PDateTime.NowGet.Body();
        }
    }
}
 


結果
実行です・・・とその前にビルドが通らなかったテストコードをビルドしておきます。mscorlib.Prig.dll を参照設定してビルドっと。
・・・O.K. ですね!
こちらのやりかたですと、元々のコードに全く手を加えていませんので、普通に実行すると元の通りの動きになります。
C:\CppTroll\Debug>xunit.console.exe ProfilingApiSample03TargetTest.dll /noshadow
xUnit.net console test runner (32-bit .NET 2.0.50727.3625)
Copyright (C) 2007-11 Microsoft Corporation.

xunit.dll:     Version 1.8.0.1545
Test assembly: C:\CppTroll\Debug\ProfilingApiSample03TargetTest.dll

MyLibraryTest.LifeInfoTest.LunchBreakTest01_NowIsLunchBreak [FAIL]
   Assert.Equal() Failure
   Position: First difference is at position 4
   Expected: 時刻: 12   お昼休みなう!

   Actual:   時刻: 7    お仕事なう・・・
   Stack Trace:
      場所 Xunit.Assert.Equal[T](T expected, T actual, IEqualityComparer`1 comparer)
      場所 Xunit.Assert.Equal[T](T expected, T actual)
      場所 MyLibraryTest.LifeInfoTest.LunchBreakTest01_NowIsLunchBreak() 場所 C:\CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs:行 31

MyLibraryTest.LifeInfoTest.LunchBreakTest02_NowIsNotLunchBreak [FAIL]
   Assert.Equal() Failure
   Position: First difference is at position 4
   Expected: 時刻: 13   お仕事なう・・・

   Actual:   時刻: 7    お仕事なう・・・
   Stack Trace:
      場所 Xunit.Assert.Equal[T](T expected, T actual, IEqualityComparer`1 comparer)
      場所 Xunit.Assert.Equal[T](T expected, T actual)
      場所 MyLibraryTest.LifeInfoTest.LunchBreakTest02_NowIsNotLunchBreak() 場所 C:\CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs:行 45

MyLibraryTest.LifeInfoTest.LunchBreakTest01_NowIsHoliday [FAIL]
   Assert.Equal() Failure
   Position: First difference is at position 4
   Expected: 曜日: Sunday       休日なう!

   Actual:   曜日: Tuesday      お仕事なう・・・
   Stack Trace:
      場所 Xunit.Assert.Equal[T](T expected, T actual, IEqualityComparer`1 comparer)
      場所 Xunit.Assert.Equal[T](T expected, T actual)
      場所 MyLibraryTest.LifeInfoTest.LunchBreakTest01_NowIsHoliday() 場所 C:\CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs:行 59

MyLibraryTest.LifeInfoTest.LunchBreakTest02_NowIsNotHoliday [FAIL]
   Assert.Equal() Failure
   Position: First difference is at position 4
   Expected: 曜日: Monday       お仕事なう・・・

   Actual:   曜日: Tuesday      お仕事なう・・・
   Stack Trace:
      場所 Xunit.Assert.Equal[T](T expected, T actual, IEqualityComparer`1 comparer)
      場所 Xunit.Assert.Equal[T](T expected, T actual)
      場所 MyLibraryTest.LifeInfoTest.LunchBreakTest02_NowIsNotHoliday() 場所 C:\CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs:行 73

4 total, 4 failed, 0 skipped, took 0.442 seconds

C:\CppTroll\Debug>
 
プロファイラを有効にして、処理を入れ替えます。
本来であれば、環境変数を弄って実行環境を整えるための **Runner のようなアプリが欲しいところですが、とりあえず手動でプロファイラを有効にしましょう。
C:\CppTroll\Debug>SET COR_ENABLE_PROFILING=1

C:\CppTroll\Debug>SET COR_PROFILER={ACC35A1C-B127-4A75-9EB8-B4E54A49F6CF}

C:\CppTroll\Debug>xunit.console.exe ProfilingApiSample03TargetTest.dll /noshadow
xUnit.net console test runner (32-bit .NET 2.0.50727.3625)
Copyright (C) 2007-11 Microsoft Corporation.

xunit.dll:     Version 1.8.0.1545
Test assembly: C:\CppTroll\Debug\ProfilingApiSample03TargetTest.dll

4 total, 0 failed, 0 skipped, took 0.463 seconds

C:\CppTroll\Debug>
 
はい!テスト通りました!

後は煮るなり焼くなり、お好きにリファクタリングしていただけると思います。
ところで、まだ構想段階ですが、どうせ後発でこういうもの作るなら、もっと API を整理して、処理を入れ替えるにしても、テスト時は手早く、リリース時は確実に(もしくはパフォーマンスを上げて)織り込んで、のような柔軟性があると面白いと思います。
それこそ、動的言語のような Monkey Paching ができると、うれしい場面も少なくないでしょう。そうすると・・・ちょっとは私のライブラリも存在意義が出てくるかもしれませんね (^_^;)




終わりに
C# Advent Calendar 2011 13 日目は、「アンマネージ API」を取り上げさせていただきました。いかがでしたか?
ほんと、申し訳程度の C# 要素でゴメンナサイ (>_<) でも、こんな世界もあるということで、低レイヤなことに興味を持っていただける方が少しでも増えれば幸いです。

最後になりましたが、Advent Calendar の参加をどうしようか迷ってたときに背中を押してくださった @biac さん、ありがとうございます!このような錚々たる面々の記事の中で、今年の成果を報告できたことはすごく刺激になりました <(_ _)>

もし来年もこのようなイベントがあれば、ぜひまた参加させていただければと思います。
次は C# ばかりのサンプルになることを祈りつつ、ですね!・・・( = =)

2011年10月30日日曜日

C# 動的メソッド入れ替え - Apply a monkey patch to any static languages on CLR -

たまにはこんな、手品の話も。

会社の飲み会の時のネタとしては使えないですが、仕事では使えること請け合いです! (`・ω・´)
手元にコンパイル済みの dll/exe があるとき、その振る舞いを実行時にだけ変えてみたいってことがしばしばあるかと。例えば、以下のようなシチュエーションが思いつきます・・・というか実際ありました (^_^;)
  • ドキュメントが不十分で挙動がよくわからない。調査するために、デバッグプリントを仕込みたい。
  • 特定の条件になると内部で例外がスローされちゃう。どうもバグみたいなので、修正したいけどソースコードがない。
  • パラメータを変化させながらタイミングを調整したい。開発効率を上げるために、動的言語と連携するような仕組みを一時的に入れ込みたい。
  • 外部機器やネットワーク、DB に依存してて自動テストがしにくい。テスト中はスタブに入れ替えたい。
Ruby や Python みたいな動的言語の世界には「Monkey Patching」って言葉があって、上に書いたようなことが、結構普通に行われてるみたい。こんな強力なことが簡単にできるのはうらやましい限りですが、何事もやりすぎはよろしくないですので、ご利用は計画的にというところでしょう。
これに対して、静的言語である C# や Java、C++ はどうかっていうと、やればできないことはない、というレベル。Java だったら、クラスがロードされるときに Bytecode を書き換えれば良いですし、C++ みたいなメモリを直接触れる言語であれば、ジャンプ先を書き換えちゃうとかで実現は可能だったりします。我らがごった煮言語、C# でも例に漏れず、今回紹介する Profiling API (アンマネージ API)を使えば実現可能です。.NET Framework で良いのは、この API を使ったプロファイルが、CLR 上で動く全てのマネージコードに適用できるということですね。寂しいのは、これを解説して下さる方が、現在日本にいらっしゃらなさそうなところでしょうか (´・ω・`)

お仲間を増やすためにも、できかけでも情報共有させていただくのが早道と、今日も拙い成果を晒させていただきます。C++ 初学者にありがちな、見苦しい失敗もしていそうですが、よろしければ最後までお付き合いくださいませ。

なお、文中にあるソリューションは以下のリンクからダウンロードできます。ビルド環境は、Visual Studio C++ 2008 + Boost C++ Libraries Ver.1.47.0、あと自動テストは Google Test 1.6.0 を使っています。環境を整える際は、各ライブラリのインストールをお願いしますです。


こちらのページ/ソフトウェアを参考にさせていただきました!情報発信されてる方には感謝するばかりです (´д⊂)
Download Details - Microsoft Download Center - Shared Source Common Language Infrastructure 2.0 Release
Cecil - Mono
ECMA C# and Common Language Infrastructure Standards
Boost C++ Libraries
Rewrite MSIL Code on the Fly with the .NET Framework Profiling API
boost.exceptionでエラー箇所を階層ごとに記録する。 - in the box
Visual Studio で GoogleTest を使う - かおるんダイアリー
Boostライブラリのビルド方法 - boostjp
2006-01-08 ■[C++][Boost]動的削除子 (dynamic deleter) - 意外と知られていない? boost::shared_ptr の側面 - Cry’s Diary
usskim / ワトソン博士
スタックトレースを表示する - encom wiki
DebugInfo.com - Examples
C言語系/呼び出し規約/x86/naked - Glamenv-Septzen.net
simple-assembly-explorer - Simple Assembly Explorer - Google Project Hosting
MSDN Magazine & Microsoft News
型システム - Wikipedia




目次

準備とか
まずは Profiling API 事始。そもそもプロファイルといえば、元々は、別のアプリケーションの実行を監視するツールということで、各メソッドの実行時間や、アプリケーションのメモリ使用状況を、一定の時間測定するということが主な目的だったはずです。しかし、.NET Framework においては、その対象をコードカバレッジを計測するためのユーティリティや、デバッグ支援ツールなどまでに広げてるのが、ごった煮好きな Microsoft らしいところ。CLR 上で動くアプリケーションには、プロセス境界より柔軟にアプリケーションを分離する仕組みである AppDomin や GC、マネージ例外処理や JIT が存在するため、通常のマシン語コードのプロファイルより難しいのが常ですが、そこは Profiling API が良しなにしてくれることになってます。
カバレッジやデバッグ支援って話が出ましたが、そのための準備を行うのに JIT は絶好の機会。Profiling API は、メモリ内 MSIL コードストリームを変更するための仕組みも提供してるので、動的な処理の変更が可能になるわけですね。
前口上はこれぐらいにして、実際にどうすればいいのかを見ていきます。また、その後実際に実装に入る際の、最低限のフレームワークも作っておきましょう。
  1. .NET Framework でのプロファイルのやり方
  2. アダプタ
  3. スタックトレース


1. .NET Framework でのプロファイルのやり方
ここは日本語になってるので、MSDN の情報が一番でしょう。中にはめずらしく図付きの解説もあります Σ (゚Д゚;)
要点をまとめると以下の通りです。
  • データ収集部分とデータ分析部分は別々のプロセス空間で動く必要があるよ。
  • データ収集部分は、Profiling API を使用して、CLR とやり取りできる dll にしてね。
  • データ分析部分は、データ収集 DLL とはログファイルや名前付きパイプでやり取りする dll/exe にしてね。
データ収集部分で言っている「CLR とやり取りできる dll」というのは、いわゆるインプロセス COM サーバーです。前回のサンプルで、申し訳ないことになっていたのは、この実験も兼ねていたせいもありました (>_<)
具体的には、(1)ICorProfilerCallback* インターフェースを実装したコクラスを作る、(2)それの CLSID を環境変数に登録する、(3)プロファイルを有効にする、という手順を踏むと、CLR のほうで CreateInstance してくれます。インターフェースの実装は後で行うので、先に環境変数の設定を見てみましょう。
C:\Documents and Settings\User>SET COR_PROFILER={1DC70D11-5E46-48C6-BB07-75CFFF188327}
 
C:\Documents and Settings\User>SET COR_ENABLE_PROFILING=1
 
COR_PROFILER に指定するのが、ICorProfilerCallback* インターフェースを実装したコクラスの CLSID になります。MSDN には、このような CLSID 形式の指定以外に、ProgID 形式の指定でも可能とありますが、なぜか私の環境ではうまく拾ってくれませんでした・・・。この状態から、さらに COR_ENABLE_PROFILING を 1 に設定すると、プロファイルが有効になります。無効化する際は、こちらのフラグを 0 に設定するだけで大丈夫です。
なお、あるセッションの環境変数でプロファイルを有効にすると、それ以降、そのセッションから立ち上がる全てのマネージプロセスについて、データ収集する dll がアタッチされることになります。Windows サービスをプロファイルするなどの特別な目的が無い限り、システム環境変数などには登録しないほうが良いでしょう ...( = =)(ぉ


2. アダプタ
データ収集に必要になる ICorProfilerCallback* インターフェースを実装したコクラスですが、実は最新の ICorProfilerCallback2 インターフェースになると、実装しなければならないメソッドはなんと 77 もの数に上ります(ぇー('A`) 私のように、とりあえず動かしながら実装や設計を詰めていく人にとっては、これを毎回作成するだけでも嫌になると思います。ですので、デフォルトで何もしないような実装を提供してくれる、アダプタクラスを作っておきましょう。
ちなみに、まだ全部は試してないのですが、Profiling API は、HRESULT で異常な結果コードを返しても、基本的に握りつぶすような挙動をするようです。ですので、例外を確実に catch し、標準出力に中身のデバッグプリントをベロベロと出してくれるよう機能も持たせておくと、デバッグが楽になるでしょう。
ところで、こういう定型処理は、マクロを持つ言語では非常に楽ですね。Boost.Preprocessor のようなライブラリを組み合わせることで、実際の記述はかなり読みやすく、かつ省略することができます。
#line 11 "CppTroll\ProfilingApiSample02\Urasandesu\NAnonym\DefaultSTDMETHODWrapper.h"
#define NANONYM_DECLARE_DEFAULT_STDMETHOD_WRAPPER_ARG(r, data, i, elem) \
    BOOST_PP_COMMA_IF(i) BOOST_PP_TUPLE_ELEM(2, 0, elem) BOOST_PP_TUPLE_ELEM(2, 1, elem)

#define NANONYM_DECLARE_DEFAULT_STDMETHOD_WRAPPER_LOAD_ARG(r, data, i, elem) \
    BOOST_PP_COMMA_IF(i) BOOST_PP_TUPLE_ELEM(2, 1, elem)

#define NANONYM_DECLARE_DEFAULT_STDMETHOD_WRAPPER(method, args_tuple_seq) \
    public: \
        STDMETHOD(method)(BOOST_PP_SEQ_FOR_EACH_I(NANONYM_DECLARE_DEFAULT_STDMETHOD_WRAPPER_ARG, _, args_tuple_seq)) \
        { \
            using namespace std; \
            using namespace boost; \
            using namespace Urasandesu::NAnonym; \
             \
            try \
            { \
                return method##Core(BOOST_PP_SEQ_FOR_EACH_I(NANONYM_DECLARE_DEFAULT_STDMETHOD_WRAPPER_LOAD_ARG, _, args_tuple_seq)); \
            } \
            catch (NAnonymException &e) \
            { \
                cout << diagnostic_information(e) << endl; \
            } \
            catch (...) \
            { \
                cout << diagnostic_information(current_exception()) << endl; \
            } \
             \
            return S_OK; \
        } \
         \
    protected: \
        STDMETHOD(method##Core)(BOOST_PP_SEQ_FOR_EACH_I(NANONYM_DECLARE_DEFAULT_STDMETHOD_WRAPPER_ARG, _, args_tuple_seq)) \
        { \
            return S_OK; \
        }
 
Boost.Preprocessor のデータ構造を使うことで、Sequence の中に Tuple で (型,変数名)のような定義を埋め込むことができます(args_tuple_seq の部分)。method が実装するメソッド名で、全体を try ~ catch で囲み、catch した例外の中身は全て標準出力に流し込むようにしました。method##Core をオーバーライドし、本来やりたい処理を記述することになります。
これを使って作った、ICorProfilerCallback2 のアダプタクラスはこんな感じです。
#line 11 "CppTroll\ProfilingApiSample02\Urasandesu\NAnonym\Profiling\ICorProfilerCallback2Impl.h"
    template<
        class Base
    >
    class ATL_NO_VTABLE ICorProfilerCallback2Impl : public Base
    {
        NANONYM_DECLARE_DEFAULT_STDMETHOD_WRAPPER(Initialize, ((IUnknown*,pICorProfilerInfoUnk)))
        NANONYM_DECLARE_DEFAULT_STDMETHOD_WRAPPER(Shutdown, BOOST_PP_EMPTY())
        NANONYM_DECLARE_DEFAULT_STDMETHOD_WRAPPER(AppDomainCreationStarted, ((AppDomainID,appDomainId)))
        NANONYM_DECLARE_DEFAULT_STDMETHOD_WRAPPER(AppDomainCreationFinished, ((AppDomainID,appDomainId))((HRESULT,hrStatus)))
        // … こんな感じで 77 個デフォルト実装が続く。
    };
 


3. スタックトレース
C# や Java には、例外をスローすると、自動的に実行時のメソッド呼び出し履歴をキャプチャし、後から参照できる「スタックトレース」なんて便利な機能があります。残念ながら C++ には標準にはありません。なので、例外を作成したときに、同様のことを行う機能を作っておくと便利に使えそうです。%INCLUDE%\DbgHelp.h の各デバッグヘルパー関数を、こちらこちらで説明して下さっているものを参考に作ってみました。メインの処理はこんな感じになってます。
#line 23 "CppTroll\ProfilingApiSample02\Urasandesu\NAnonym\StackTrace.cpp"
    void StackTrace::Capture(INT skipCount,
                 HANDLE hProcess, 
                 HANDLE hThread, 
                 LPCWSTR userSearchPath, 
                 LPCONTEXT pContext)
    {
        using boost::filesystem::path;
        using boost::filesystem::absolute;
        using boost::filesystem::exists;

        m_hProcess = hProcess;
        
        std::string absoluteSearchPath = absolute(userSearchPath).string();
        PSTR userSearchPath_ = exists(absoluteSearchPath) ? 
                                 const_cast<PSTR>(absoluteSearchPath.c_str()) : NULL;

        DWORD options = ::SymGetOptions();
        options |= SYMOPT_LOAD_LINES;
        options &= ~SYMOPT_UNDNAME;
        ::SymSetOptions(options); 

        ::SymInitialize(m_hProcess, userSearchPath_, TRUE);

        STACKFRAME sf; 
        ::ZeroMemory(&sf, sizeof(STACKFRAME));
        if (!pContext)
        {
            unsigned long instPtr;
            unsigned long stackPtr;
            unsigned long basePtr; 

            __asm call(x)
            __asm x: pop eax
            __asm mov [instPtr], eax
            __asm mov [stackPtr], esp
            __asm mov [basePtr], ebp

            sf.AddrPC.Offset = instPtr;
            sf.AddrPC.Mode = AddrModeFlat;
            sf.AddrStack.Offset = stackPtr;
            sf.AddrStack.Mode = AddrModeFlat;
            sf.AddrFrame.Offset = basePtr;
            sf.AddrFrame.Mode = AddrModeFlat;
        }
        else
        {
            sf.AddrPC.Offset = pContext->Eip;
            sf.AddrPC.Mode = AddrModeFlat;
            sf.AddrStack.Offset = pContext->Esp;
            sf.AddrStack.Mode = AddrModeFlat;
            sf.AddrFrame.Offset = pContext->Ebp;
            sf.AddrFrame.Mode = AddrModeFlat;
        }

        while (::StackWalk(IMAGE_FILE_MACHINE_I386, m_hProcess, hThread, &sf, NULL, 
                           NULL, ::SymFunctionTableAccess, ::SymGetModuleBase, NULL) == TRUE) 
        {
            if (sf.AddrFrame.Offset == 0) 
                break;

            if (sf.AddrPC.Offset == 0) 
                continue;

            if (sf.AddrPC.Offset == sf.AddrReturn.Offset) 
                continue;

            if (0 < skipCount) 
            {
                --skipCount;
                continue;
            }

            StackFrame *pFrame = new StackFrame();
            pFrame->Init(m_hProcess, sf.AddrPC.Offset);
            m_frames.push_back(pFrame);
        }
    }
 
これを、DefaultSTDMETHODWrapper.h でちらっと見えていた例外クラス、NAnonymException のコンストラクタで呼び出すようにします。
#line 18 "CppTroll\ProfilingApiSample02\Urasandesu\NAnonym\NAnonymException.cpp"
    NAnonymException::NAnonymException() : 
        m_pStackTrace(boost::make_shared<StackTrace>())
    {
        CaptureStackTrace(this);
    }
    
    NAnonymException::NAnonymException(std::string const &what) : 
        m_what(what),
        m_pStackTrace(boost::make_shared<StackTrace>())
    { 
        CaptureStackTrace(this);
    }
    
    NAnonymException::NAnonymException(std::string const &what, NAnonymException &innerException) : 
        m_what(what),
        m_pStackTrace(boost::make_shared<StackTrace>())
    {
        CaptureStackTrace(this);
        *this << boost::errinfo_nested_exception(boost::copy_exception(innerException));
    }
    
    
    // …(略)…
    
    

    void NAnonymException::CaptureStackTrace(NAnonymException *this_)
    {
        this_->m_pStackTrace->Capture(3);
        *this_ << ThrowStackTrace(this_->m_pStackTrace.get());
    }
 
例外の中身を出すために使っていた Boost.Exception のメソッド、boost::diagnostic_information は、boost::error_info で登録しておいた型を引数に持つ to_string を定義しておくことで、それを呼び出してくれるようになってます。この仕組みを利用し、C# や Java の出力方式を参考に、出力文字列を構成しています。
#line 51 "CppTroll\ProfilingApiSample02\Urasandesu\NAnonym\NAnonymException.cpp"
    inline std::string to_string(StackTrace *pStackTrace)
    {
        using namespace std;
        using namespace boost;
        
        ostringstream oss;
        ptr_vector<StackFrame> *pFrames = pStackTrace->GetStackFrames();
        for (ptr_vector<StackFrame>::iterator i = pFrames->begin(), i_end = pFrames->end(); i != i_end; ++i)
        {
            oss << "at ";
            oss << i->GetSymbolName();
            oss << " in ";
            oss << i->GetModuleName();
            if (0 < i->GetFileLineNumber())
            {
                oss << "(";
                oss << i->GetFileName();
                oss << ":";
                oss << i->GetFileLineNumber();
                oss << ")";
            }
            oss << "\n";
        }
        return oss.str();
    }
 

BOOST_THROW_EXCEPTION(NAnonymException("An error is occurred!!")) して、catch (...) して、cout << diagnostic_information(current_exception()) << endl するだけの、簡単なテストを書いて実行してみると・・・。
Microsoft Windows XP [Version 5.1.2600]
(C) Copyright 1985-2001 Microsoft Corp.

C:\Documents and Settings\User>cd C:\Documents and Settings\User\CppTroll\Debug

C:\Documents and Settings\User\CppTroll\Debug>ProfilingApiSample02Test.exe --gtest_filter=ProfilingApiSample02TestSuite.ProfilingApiSample02Test
Running main() from gtest_main.cc
Note: Google Test filter = ProfilingApiSample02TestSuite.ProfilingApiSample02Test
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from ProfilingApiSample02TestSuite
[ RUN      ] ProfilingApiSample02TestSuite.ProfilingApiSample02Test
c:\documents and settings\User\cpptroll\profilingapisample02test\profilingapisample02test.cpp(32): Throw in function void __thiscall `anonymous-namespace'::ProfilingApiSample02TestSuite_ProfilingApiSample02Test_Test::TestBody(void)
Dynamic exception type: class boost::exception_detail::clone_impl<class Urasandesu::NAnonym::NAnonymException>
std::exception::what: An error is occurred!!
[struct Urasandesu::NAnonym::tag_stack_trace *] = at `anonymous namespace'::ProfilingApiSample02TestSuite_ProfilingApiSample02Test_Test::TestBody in ProfilingApiSample02Test.exe(c:\documents and settings\User\cpptroll\profilingapisample02test\profilingapisample02test.cpp:32)
at testing::internal::HandleSehExceptionsInMethodIfSupported<testing::Test,void> in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2075)
at testing::internal::HandleExceptionsInMethodIfSupported<testing::Test,void> in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2126)
at testing::Test::Run in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2162)
at testing::TestInfo::Run in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2342)
at testing::TestCase::Run in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2446)
at testing::internal::UnitTestImpl::RunAllTests in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:4238)
at testing::internal::HandleSehExceptionsInMethodIfSupported<testing::internal::UnitTestImpl,bool> in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2075)
at testing::internal::HandleExceptionsInMethodIfSupported<testing::internal::UnitTestImpl,bool> in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2126)
at testing::UnitTest::Run in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:3874)
at main in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest_main.cc:39)
at __tmainCRTStartup in ProfilingApiSample02Test.exe(f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c:266)
at mainCRTStartup in ProfilingApiSample02Test.exe(f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c:182)
at RegisterWaitForInputIdle in kernel32.dll


[       OK ] ProfilingApiSample02TestSuite.ProfilingApiSample02Test (359 ms)
[----------] 1 test from ProfilingApiSample02TestSuite (359 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (359 ms total)
[  PASSED  ] 1 test.

C:\Documents and Settings\User\CppTroll\Debug>
 
いいですね~。VC++ ランタイムのソースコードは f ドライブで開発されてたのかなーみたいな不必要な情報まで出てますが、大は小を兼ねるってことで (b^ー゚)
これで準備が整いました!では、実装を始めましょう!




Hello, Profiling API !!
まず手始めに、一番簡単に追加ができる #US ヒープを弄ってみます。文字列を返すメソッドについて、環境変数を通じて与えた文字列に入れ替えられるようにしましょう。書き換える対象のプログラムはこんな感じ。
#line 1 "CppTroll\ProfilingApiSample01Target\Program.cs"
using System;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(new Class1().Print("Hello, World !!"));
        Console.WriteLine(new Class2().Print("こんにちは、世界!"));
    }
}

public class Class1
{
    public string Print(string value)
    {
        return "Hello, " + new Class2().Print(value) + " World !!";
    }
}

public class Class2
{
    public string Print(string value)
    {
        return "こんにちは、" + value + " 世界!";
    }
}
 
章立ては以下の通りです。
  1. 初期設定
  2. JIT 開始のフック、処理の入れ替え
    2-1. FunctionID から MethodDef テーブルレコードへの変換
    2-2. メソッド/クラスの詳細な情報を取得
    2-3. IL メソッドボディの書き換え
  3. 結果


1. 初期設定
ICorProfilerCallback* インターフェースを実装したコクラスで、一番に呼ばれるのが Initialize メソッド。このメソッドは最初に説明した最低限の設定で呼ばれるのですが、この後の処理は個別に CLR に呼び出してもらうよう設定する必要があります(SetEventMask)。このサンプルでは、JIT 開始をフックするだけで良いですので、COR_PRF_MONITOR_JIT_COMPILATION フラグを立てています(45 行目)。
順番が前後してしまいましたが、Initialize メソッドでは CLR とやりとりをするための ICorProfilerInfo* インターフェース実装オブジェクトが引数で渡されてきます。ただ、引数で渡されるのは、ここしかありませんので、グローバル変数もしくはメンバ変数に保持しておきましょう(36 行目)。
あとは、環境変数から、書き換える文字列を取得しておきます(50 行目~52 行目)。書き換え対象となるメソッド名は NANONYM_TARGET_METHOD を、#US ヒープに追加する文字列は NANONYM_NEW_MESSAGE を通じて取得するようにしました。こっそりと環境変数を取得するためのユーティリティクラス(Environment)を追加してますが、_dupenv_s をラップし、エラーがあれば前述のスタックトレース付き例外をスローするようにしただけですので、特に解説はしません。
ちなみに、アダプタクラスを作ったおかげで、本処理が記述されている ExeWeaver.cpp は、たったの 198 行です。携帯からでも、難なく読めちゃうかもですね (^_^;)
#line 31 "CppTroll\ProfilingApiSample01\ExeWeaver.cpp"
    // Reset the timer.
    m_timer.restart();
    

    // Initialize the profiling API.
    hr = pICorProfilerInfoUnk->QueryInterface(IID_ICorProfilerInfo2, 
                                              reinterpret_cast<void**>(&m_pInfo));
    if (FAILED(hr)) 
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));


    // Set a value that specifies the types of events for which the profiler 
    // wants to receive notification from CLR.
    DWORD event_ = COR_PRF_MONITOR_JIT_COMPILATION;
    hr = m_pInfo->SetEventMask(event_);
    if (FAILED(hr)) 
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));

    
    // Get the name of the target method and the new message.
    m_targetMethodName = wstring(CA2W(Environment::GetEnvironmentVariable("NANONYM_TARGET_METHOD").c_str()));
    m_newMessage = wstring(CA2W(Environment::GetEnvironmentVariable("NANONYM_NEW_MESSAGE").c_str()));
 


2. JIT 開始のフック、処理の入れ替え
各メソッドの JIT の開始は、JITCompilationStarted メソッドが通知してくれます。早速、中を見ていきましょう。

2-1. FunctionID から MethodDef テーブルレコードへの変換
#line 80 "CppTroll\ProfilingApiSample01\ExeWeaver.cpp"
    // Convert FunctionID to MethodDef token.
    mdMethodDef mdmd = mdMethodDefNil;
    CComPtr<IMetaDataImport2> pImport;
    hr = m_pInfo->GetTokenAndMetaDataFromFunction(functionId, IID_IMetaDataImport2, 
                                                  reinterpret_cast<IUnknown**>(&pImport), 
                                                  &mdmd);
    if (FAILED(hr)) 
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));
    
 
Profiling API の共通の概念として、プロファイル ID というものがあります。各イベントを通知するメソッドの引数で渡されるこの情報、実際の意味は、各項目を説明するメモリのアドレスなのですが、そのままでは扱い辛いので、人間が読める情報に変換します。最終的に、MethodDef テーブルのレコード情報が欲しいので、Meta Data API 用のインターフェースを取り出しておきます。

2-2. メソッド/クラスの詳細な情報を取得
#line 90 "CppTroll\ProfilingApiSample01\ExeWeaver.cpp"
    // Get the properties of this method.
    mdTypeDef mdtd = mdTypeDefNil;
    WCHAR methodName[MAX_SYM_NAME] = { 0 };
    {
        ULONG methodNameSize = sizeof(methodName);
        DWORD methodAttr = 0;
        PCCOR_SIGNATURE pMethodSig = NULL;
        ULONG methodSigSize = 0;
        ULONG methodRVA = 0;
        DWORD methodImplFlag = 0;
        hr = pImport->GetMethodProps(mdmd, &mdtd, methodName, methodNameSize, 
                                     &methodNameSize, &methodAttr, &pMethodSig, 
                                     &methodSigSize, &methodRVA, &methodImplFlag);
        if (FAILED(hr)) 
            BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));
        
        ULONG callConv = IMAGE_CEE_CS_CALLCONV_MAX;
        pMethodSig += ::CorSigUncompressData(pMethodSig, &callConv);

        ULONG paramCount = 0;
        pMethodSig += ::CorSigUncompressData(pMethodSig, &paramCount);

        CorElementType retType = ELEMENT_TYPE_END;
        pMethodSig += ::CorSigUncompressElementType(pMethodSig, &retType);
        if (retType != ELEMENT_TYPE_STRING)
            return S_OK;
    }
    

    // Get the properties of type that this method is declared.
    WCHAR typeName[MAX_SYM_NAME] = { 0 };
    {
        ULONG typeNameSize = sizeof(typeName);
        DWORD typeAttr = 0;
        mdToken mdtTypeExtends = mdTokenNil;
        hr = pImport->GetTypeDefProps(mdtd, typeName, typeNameSize, &typeNameSize, 
                                      &typeAttr, &mdtTypeExtends);
        if (FAILED(hr)) 
            BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));
    }
    

    // Check, is this method the instrumentation target?
    wstring methodFullName(typeName);
    methodFullName += L".";
    methodFullName += methodName;
    if (methodFullName != m_targetMethodName)
        return S_OK;
    
 
Meta Data API を通じ、メソッドの詳細情報と、それが定義されたクラスの詳細情報を取得します。106 行目~115 行目で行っているメソッドシグネチャの Parse は、本当はもっとちゃんと行う必要があるのですが・・・。今回はとりあえず、戻り値の型が文字列であることさえわかればいいので、最低限の処理しかしていません。132 行目で取得したクラス名とメソッド名を連結し、環境変数から取得した、書き換え対象となるメソッドかどうかを判定しています。

2-3. IL メソッドボディの書き換え
#line 140 "CppTroll\ProfilingApiSample01\ExeWeaver.cpp"
    // Define the new message to #US heap.
    CComPtr<IMetaDataEmit2> pEmit;
    hr = pImport->QueryInterface(IID_IMetaDataEmit2, 
                                 reinterpret_cast<void**>(&pEmit));
    if (FAILED(hr)) 
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));

    mdString mdsMessage = mdStringNil;
    hr = pEmit->DefineUserString(m_newMessage.c_str(), m_newMessage.size(), &mdsMessage);
    if (FAILED(hr))
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));
    

    // Emit the new IL method body.
    SimpleBlob sb;
    sb.Put<BYTE>(OpCodes::Encodings[OpCodes::CEE_LDSTR].byte2);
    sb.Put<DWORD>(mdsMessage);
    sb.Put<BYTE>(OpCodes::Encodings[OpCodes::CEE_RET].byte2);
    
    COR_ILMETHOD ilMethod;
    ::ZeroMemory(&ilMethod, sizeof(COR_ILMETHOD));
    ilMethod.Fat.SetMaxStack(1);
    ilMethod.Fat.SetCodeSize(sb.Size());
    ilMethod.Fat.SetLocalVarSigTok(mdTokenNil);
    ilMethod.Fat.SetFlags(0);
    
    unsigned headerSize = COR_ILMETHOD::Size(&ilMethod.Fat, false);
    unsigned totalSize = headerSize + sb.Size();
    

    // Allocate the area for new IL method body and set it.
    ModuleID mid = NULL;
    {
        ClassID cid = NULL;
        mdMethodDef mdmd_ = mdMethodDefNil;
        hr = m_pInfo->GetFunctionInfo(functionId, &cid, &mid, &mdmd_);
        if (FAILED(hr)) 
            BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));
    }

    CComPtr<IMethodMalloc> pMethodMalloc;
    hr = m_pInfo->GetILFunctionBodyAllocator(mid, &pMethodMalloc);
    if (FAILED(hr)) 
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));

    BYTE *pNewILFunctionBody = reinterpret_cast<BYTE*>(pMethodMalloc->Alloc(totalSize));

    BYTE *pBuffer = pNewILFunctionBody;
    pBuffer += COR_ILMETHOD::Emit(headerSize, &ilMethod.Fat, false, pBuffer);
    ::memcpy_s(pBuffer, totalSize - headerSize, sb.Ptr(), sb.Size());

    hr = m_pInfo->SetILFunctionBody(mid, mdmd, pNewILFunctionBody);
    if (FAILED(hr)) 
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));
    
 
環境変数から取得した文字列を #US ヒープに追加し(140 行目~150 行目)、それを使って IL ぷちぷち(153 行目~167 行目)。Profiling API の機能を使って新しい IL メソッドボディ領域を作ったら、そこに IL の並びをコピーします(170 行目~193 行目)。打ち込む IL が単純なので、特筆すべきところはないですね (^^ゞ


3. 結果
さて、新しいコマンドプロンプトを立ち上げ、実行してみましょう!サンプルプログラムの中の Class2.Print メソッドについて、返す文字列を変更してみます。
Microsoft Windows XP [Version 5.1.2600]
(C) Copyright 1985-2001 Microsoft Corp.

C:\Documents and Settings\User>cd C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>dir
 ドライブ C のボリューム ラベルは S3A4509D001 です
 ボリューム シリアル番号は 758E-2116 です

 C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug のディレクトリ

2011/10/29  17:01    <DIR>          .
2011/10/29  17:01    <DIR>          ..
2011/10/29  17:05             5,120 ProfilingApiSample01Target.exe
2011/10/29  17:05            13,824 ProfilingApiSample01Target.pdb
               2 個のファイル              18,944 バイト
               2 個のディレクトリ  23,052,824,576 バイトの空き領域

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>SET COR_PROFILER={1DC70D11-5E46-48C6-BB07-75CFFF188327}

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>SET NANONYM_TARGET_METHOD=Class2.Print

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>SET NANONYM_NEW_MESSAGE=Hello, Dynamic Languages World!!

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>SET COR_ENABLE_PROFILING=0

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>ProfilingApiSample01Target.exe
Hello, こんにちは、Hello, World !! 世界! World !!
こんにちは、こんにちは、世界! 世界!

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>SET COR_ENABLE_PROFILING=1

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>ProfilingApiSample01Target.exe
Hello, Hello, Dynamic Languages World!! World !!
Hello, Dynamic Languages World!!
Time Elapsed: 0.078000s

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>dir
 ドライブ C のボリューム ラベルは S3A4509D001 です
 ボリューム シリアル番号は 758E-2116 です

 C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug のディレクトリ

2011/10/29  17:01    <DIR>          .
2011/10/29  17:01    <DIR>          ..
2011/10/29  17:05             5,120 ProfilingApiSample01Target.exe
2011/10/29  17:05            13,824 ProfilingApiSample01Target.pdb
               2 個のファイル              18,944 バイト
               2 個のディレクトリ  23,051,894,784 バイトの空き領域

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>
 
おー!入れ替わりましたね!28 行目~29 行目で出ていた内容が、COR_ENABLE_PROFILING=1 することによって、34 行目~35 行目では見事に入れ替わっています。ファイルのタイムスタンプに変わりは無いため、実行時に、メモリ上にある IL メソッドボディだけが変化していることがわかるかと思います。次はもう少し複雑なサンプルを作ってみましょう。




Monkey ... Swapping ?
今度はメソッドの中身を丸ごと交換してみます。シグネチャが同じであれば、そう難しくはなさそうですね。また今後を考えて、ぼちぼち使い勝手の良いラッパーも設計していきます。仕事と違って、何度も作り直しながらブラッシュアップできるのは気が楽です (^_^;)
書き換える対象のプログラムはこんな感じでいかがでしょう。
#line 1 "CppTroll\ProfilingApiSample02Target\Program.cs"
using System;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello world!!");
    }
}

class AlternativeProgram
{
    static void Main(string[] args)
    {
        Console.WriteLine(@"Welcome to the low layer world of CLR!! In the normal case, 
you can not replace the method body at the runtime, 
but you can do it by using the unmanaged profiling API!!");
    }
}
 
とりあえず動くもの、ということでラッパーには最低限必要な処理だけ持たせることにします。なので、クラスやメソッドの識別には、メタデータテーブルの ID を直接指定することにしました。ildasm を使えば、あらかじめその辺りを調べておくことができます。全体の雰囲気がわかって来たら、細かなところを作りこんで行く予定です。指定の方法は先ほどと同じく、環境変数を通じて行うこととしましょう。章立ては以下の通りです。
  1. 初期設定
  2. AppDomain 作成開始のフック
  3. Assembly 読み込み開始のフック
  4. Module 読み込み開始のフック
  5. JIT 開始のフック、処理の入れ替え
  6. 結果
中身を交換できるのは同じ Assembly に定義してあるクラス/メソッドだけ、しかも Generics や例外は無視しているという手抜きっぷりですが、シグネチャさえ合えば中身をごっそり換えられるということで、できることは先ほどのサンプルとは段違いに多いです。それでも本処理が記述されている ExeWeaver2.cpp は、たったの 183 行。我ながらイイ線行ってるのではないでしょうか (^^ゞ


1. 初期設定
#line 53 "CppTroll\ProfilingApiSample02\ExeWeaver2.cpp"
    // Reset the timer.
    m_timer.restart();


    // Initialize the unmanaged profiling API.
    m_pProfInfo->Init(pICorProfilerInfoUnk);
    m_pProfInfo->SetEventMask(COR_PRF_MONITOR_ASSEMBLY_LOADS | 
                              COR_PRF_MONITOR_MODULE_LOADS | 
                              COR_PRF_MONITOR_APPDOMAIN_LOADS | 
                              COR_PRF_MONITOR_JIT_COMPILATION);


    // Get name of the target assembly, replaced method and its declaring type and 
    // replacing method and its declaring type.
    m_targetAssemblyName = wstring(CA2W(Environment::GetEnvironmentVariable("NANONYM_TARGET_ASSEMBLY").c_str()));
    {
        istringstream is(Environment::GetEnvironmentVariable("NANONYM_REPLACE_TYPE_FROM"));
        is >> hex >> m_mdtdReplaceTypeFrom;
    }
    {
        istringstream is(Environment::GetEnvironmentVariable("NANONYM_REPLACE_METHOD_FROM"));
        is >> hex >> m_mdmdReplaceMethodFrom;
    }
    {
        istringstream is(Environment::GetEnvironmentVariable("NANONYM_REPLACE_TYPE_TO"));
        is >> hex >> m_mdtdReplaceTypeTo;
    }
    {
        istringstream is(Environment::GetEnvironmentVariable("NANONYM_REPLACE_METHOD_TO"));
        is >> hex >> m_mdtdReplaceMethodTo;
    }


    // Create pseudo AppDomain to load mscorlib.dll.
    m_pProfInfo->GetCurrentProcess()->GetPseudoDomain();
 
Initialize メソッドでやることは先ほどと大体一緒です。環境変数を通じて取得する情報(65 行目~83 行目)は以下の通りとしました。
環境変数名 概要
NANONYM_TARGET_ASSEMBLY 入れ替えるクラス/メソッドが定義されている Assembly 表示名。「ProfilingApiSample02Target」とか。
NANONYM_REPLACE_TYPE_FROM 入れ替え元のクラス(TypeDef テーブルのレコード ID)。「0x02000002」とか。
NANONYM_REPLACE_METHOD_FROM 入れ替え元のメソッド(MethodDef テーブルのレコード ID)。「0x06000001」とか。
NANONYM_REPLACE_TYPE_TO 入れ替え先のクラス(TypeDef テーブルのレコード ID)。「0x02000003」とか。
NANONYM_REPLACE_METHOD_TO 入れ替え先のメソッド(MethodDef テーブルのレコード ID)。「0x06000003」とか。
Profiling API のラッパーは、この API を通じて CLR が通知してくるイベントを元にモデル化しています。ただ、mscorlib.dll は決まった AppDomain に読み込まれることがないようなので、モデルを揃えるために、ここで擬似 AppDomain を作成しています(87 行目)。


2. AppDomain 作成開始のフック
#line 109 "CppTroll\ProfilingApiSample02\ExeWeaver2.cpp"
    // Create the wrapper referring the AppDomainID.
    m_pProfInfo->GetCurrentProcess()->CreateIfNecessary<AppDomainProfile>(appDomainId);
 
AppDomain 作成開始は、AppDomainCreationStarted を通じて通知されます。ここでは AppDomainID に紐付ける Profiling API ラッパーを作成しておきます。.NET Framework の機能上、AppDomain 単位での Load/Unload ができるため、AppDomain に対応する Profiling API ラッパーが解放されると、それに紐付く情報が全て解放されるような形を目指しています。また、同じ AppDomain であれば、Assembly や Module、Type、Method ・・・は常に同じものを指すため、map を使い、プロファイル ID からこれまで作成したインスタンスを引けるような構造にしてみました。
あと、Profiling API に限ったことではないのですが、CLR では %INCLUDE%\WinError.h に定義されている通常の HRESULT に加え、%INCLUDE%\CorError.h に定義された HRESULT も使用しています。最終的には、これも例外メッセージにうまく載せたいところです。


3. Assembly 読み込み開始のフック
#line 120 "CppTroll\ProfilingApiSample02\ExeWeaver2.cpp"
    // Create the wrapper referring the AssemblyID through the current AppDoamin.    
    AppDomainProfile *pDomainProf = m_pProfInfo->GetCurrentProcess()->GetCurrentDomain();
    AssemblyProfile *pAsmProf = pDomainProf->CreateIfNecessary<AssemblyProfile>(assemblyId);
    if (pAsmProf->GetName() != m_targetAssemblyName)
        return S_OK;
    
    m_pTargetAssemblyProf = pAsmProf;
 
Assembly 読み込み開始を通知してくれるのは、AssemblyLoadStarted メソッドとなります。現在の AppDomain を表す Profiling API ラッパーから、AssemblyID に紐付ける Profiling API ラッパーを作成します。もし、その名前が、環境変数で指定されていた対象となる Assembly の修飾名であれば、それを保持しておきましょう。


4. Module 読み込み開始のフック
#line 137 "CppTroll\ProfilingApiSample02\ExeWeaver2.cpp"
    if (m_pTargetAssemblyProf == NULL || m_pConv->HasInitialized())
        return S_OK;
    
    // Initialize the value converter to convert the wrapper of the profiling API 
    // to the wrapper of the meta data API.
    ModuleProfile *pModProf = m_pTargetAssemblyProf->CreateIfNecessary<ModuleProfile>(moduleId);
    AssemblyMetaData *pAsmMeta = m_pMetaInfo->CreatePseudo<AssemblyMetaData>();
    m_pConv->Initialize(pAsmMeta, m_pProfInfo->GetCurrentProcess(), pModProf);
 
Module 読み込み開始のフックは ModuleLoadStarted で行います。対象となる Assembly を表すラッパーが作成されているのであれば、Meta Data Api ラッパーと相互変換するためのオブジェクトを初期化します。


5. JIT 開始のフック、処理の入れ替え
#line 156 "CppTroll\ProfilingApiSample02\ExeWeaver2.cpp"
    if (!m_pConv->HasInitialized())
        return S_OK;

    // Get the properties this method.
    MethodProfile *pMethodProfFrom = m_pProfInfo->GetCurrentProcess()->CreateIfNecessary<MethodProfile>(functionId);
    MethodMetaData *pMethodMetaFrom = m_pConv->Convert(pMethodProfFrom);
    if (pMethodMetaFrom->GetToken() != m_mdmdReplaceMethodFrom)
        return S_OK;

    
    // Get the properties of the type that this method is declared.
    TypeMetaData *pTypeMetaFrom = pMethodMetaFrom->GetDeclaringType();
    if (pTypeMetaFrom->GetToken() != m_mdtdReplaceTypeFrom)
        return S_OK;

    
    // Replace the IL method body of them that are set above.
    ModuleMetaData *pModMetaFrom = pTypeMetaFrom->GetModule();
    TypeMetaData *pTypeMetaTo = pModMetaFrom->GetType(m_mdtdReplaceTypeTo);
    TypeProfile *pTypeProfTo = m_pConv->ConvertBack(pTypeMetaTo);   // NOTE: To resolve the type defined explicitly 
    MethodMetaData *pMethodMetaTo = pTypeMetaTo->GetMethod(m_mdtdReplaceMethodTo);
    MethodProfile *pMethodProfTo = m_pConv->ConvertBack(pMethodMetaTo);
    MethodBodyProfile *pBodyProfTo = pMethodProfTo->GetMethodBody();
    pMethodProfFrom->SetMethodBody(pBodyProfTo);
 
JIT 開始のフックは先ほどのサンプルでも出てきました。これまでの処理で、情報が揃った場合、実際の交換を行います。FunctionID から入れ替え元のメソッドの詳細情報を取得(159 行目~163 行目)、そのメソッドが定義されたクラスの TypeDef テーブル ID を取得し(166 行目~169 行目)、条件が合うのであれば入れ替え先のメソッドから IL メソッドボディをコピーし、入れ替え元のメソッドの IL メソッドボディを設定し直します(172 行目~ 179 行目)。若干嵌ったのは、Profiling API には、Meta Data API で扱うメタデータテーブルの ID から、プロファイル ID に変換するメソッドがあるのですが、どうも上から順(Assembly → Module → Type → Method →…)に解決しないと、E_INVALIDARG にされてしまうようです。175 行目の処理はこれを示しています。


6. 結果
こちらも、新しいコマンドプロンプトを立ち上げ実行してみます。サンプルプログラムの中の Program(0x02000002).Main(0x06000001)メソッドについて、別のクラスのメソッド AlternativeProgram(0x02000003).Main(0x06000003)に変更してみましょう。
Microsoft Windows XP [Version 5.1.2600]
(C) Copyright 1985-2001 Microsoft Corp.

C:\Documents and Settings\User>cd C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>dir
 ドライブ C のボリューム ラベルは S3A4509D001 です
 ボリューム シリアル番号は 758E-2116 です

 C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug のディレクトリ

2011/10/29  18:53    <DIR>          .
2011/10/29  18:53    <DIR>          ..
2011/10/29  18:53             5,120 ProfilingApiSample02Target.exe
2011/10/29  18:53            11,776 ProfilingApiSample02Target.pdb
               2 個のファイル              16,896 バイト
               2 個のディレクトリ  23,116,296,192 バイトの空き領域

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET COR_PROFILER={F60DB91B-5932-4964-818A-CA697CF46A5F}

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET NANONYM_TARGET_ASSEMBLY=ProfilingApiSample02Target

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET NANONYM_REPLACE_TYPE_FROM=0x02000002

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET NANONYM_REPLACE_METHOD_FROM=0x06000001

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET NANONYM_REPLACE_TYPE_TO=0x02000003

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET NANONYM_REPLACE_METHOD_TO=0x06000003

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET COR_ENABLE_PROFILING=0

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>ProfilingApiSample02Target.exe
Hello world!!

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET COR_ENABLE_PROFILING=1

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>ProfilingApiSample02Target.exe
Welcome to the low layer world of CLR!! In the normal case,
you can not replace the method body at the runtime,
but you can do it by using the unmanaged profiling API!!
Time Elapsed: 0.063000s

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>dir
 ドライブ C のボリューム ラベルは S3A4509D001 です
 ボリューム シリアル番号は 758E-2116 です

 C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug のディレクトリ

2011/10/29  18:53    <DIR>          .
2011/10/29  18:53    <DIR>          ..
2011/10/29  18:53             5,120 ProfilingApiSample02Target.exe
2011/10/29  18:53            11,776 ProfilingApiSample02Target.pdb
               2 個のファイル              16,896 バイト
               2 個のディレクトリ  23,116,296,192 バイトの空き領域

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>
 
さっくり変わります(34 行目→39 行目~41 行目)。すばらしい。
ところで、プロセスの初期化から、終了までの処理時間を入れていますが、凄まじいですね。Profiling API を謳うだけあって、パフォーマンスの劣化がほとんど感じられないのはさすがです。式木こねくりまわして、AppDomainのLoad/Unload繰り返してた頃の自分に教えてあげたいですね (^_^;)




さらなる魔改造!
Profiling API の使い方を見てきましたが、いかがでしたでしょうか。メソッドのトレースとかまではちょっと紹介できませんでしたが、マネージコードではどうしようもなかったことができるようになるのは、ちょっとは自分の自信に繋がりそう。これまで魔法にしか思えなかったものが、タネがある手品だったということがわかった感じですね! (>_<)
そして、シンプルな方法だけでの入れ替えを試した結果ではありますが、やはりパフォーマンスは段違いだということがわかりました。以前行き詰っていた状態から、実に 30 倍以上の高速化が図れています。これはすごい!アンマネージ万歳!COM 万歳!C++ 万歳!
・・・まあ、最終的には C# のコード側から、変更情報(対象の名前やらMSIL ストリームやら…)を、名前付きパイプ経由で送り込むようなアーキテクチャになると思いますので、ここまでサクサクになるかはわかりませんが、だいぶ希望が持てる結果になったと思います。もう少し使い方に慣れてから、C# 側のデザインも考えて行こうと思います。