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# 側のデザインも考えて行こうと思います。