@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 を使っています。環境を整える際は、各ライブラリのインストールをお願いしますです。
- ソリューション(GitHub):urasandesu/CppTroll - GitHub
こちらのページ/ソフトウェアを参考にさせていただきました!いつもお世話になっております! (`・ω・ )ゝ
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 つの戦略を考えていきます。
- 呼び出すクラス側(この例の場合、LifeInfo)で、処理を入れ替えられる仕組みを用意する。
- 呼び出されるクラス側(この例の場合、DateTime)で、処理を入れ替えられる仕組みを用意する。
レガシーコードにクサビを打ち込むお仕事(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# ばかりのサンプルになることを祈りつつ、ですね!・・・( = =)
0 件のコメント:
コメントを投稿