2011年7月31日日曜日

Memo 01

Twilog で遡るのが億劫になってきたので、資料とか手順とか経緯とか。取り留めのないメモ。

C# の魔改造を始めて早 1 年…。やり始めた頃に比べると知識も増えた反面、取り入れたけど結局使わなかった技術や、なんでこれ使うようになったんだっけという技術も増えつつあり、成果物ということでは全然形にはなっていないのだけど、一度ちょっと棚卸しないといけないと思いまして、のエントリです。


以下の書籍・記事/ライブラリを参考/使用させていただいてます。著作者の方々には本当に感謝です!
Amazon.co.jp: メタプログラミングRuby: Paolo Perrotta, 角征典: 本
Amazon.co.jp: レガシーコード改善ガイド (Object Oriented SELECTION): マイケル・C・フェザーズ, ウルシステムズ株式会社, 平澤 章, 越智 典子, 稲葉 信之, 田村 友彦, 小堀 真義: 本
Amazon.co.jp: .NET Common Language Runtime Unleashed: Kevin Burton: 本
Amazon.co.jp: Domain-Specific Languages (Addison-Wesley Signature Series (Fowler)): Martin Fowler: 本
SEXYHOOKで始めるテスト とある関数の接合部(1):CodeZine
SEXYHOOK
Matzにっき(2010-11-13)
Moles - Isolation framework for .NET - Microsoft Research
neue cc - Rx + MolesによるC#での次世代非同期モックテスト考察
さすがMoles!Moq たちにできない事を平然とやってのけるッ - present
Moles - .NETのモック・スタブフレームワーク - Jamzzの日々



NAnonym の現状
GitHub で公開している NAnonym(えぬ・あのにむ)。対象をものすごく限定してしまっているのだけど、処理の中身を実行時に入れ替えられる。とりあえずこんなテストは動くようになってました。

Test.Urasandesu.NAnonym.Etc.dll
元の処理。

namespace Test.Urasandesu.NAnonym.Etc
{
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 + " 世界!";
}
}
}



Test.Urasandesu.NAnonym.Cecil.DW.dll
入れ替える設定を書く。

using Test.Urasandesu.NAnonym.Etc;
using Urasandesu.NAnonym.DW;
using Urasandesu.NAnonym.Cecil.DW;

namespace Test.Urasandesu.NAnonym.Cecil.DW
{
public class GlobalClass1 : GlobalClass
{
protected override DependencyClass OnRegister()
{
// Class1 の処理を入れ替える。
var class1GlobalClass = new GlobalClass<Class1>();
class1GlobalClass.Setup(o =>
{
o.HideMethod<string, string>(_ => _.Print).By(
value =>
{
return "Modified prefix at Class1.Print" + new Class2().Print(value) + "Modified suffix at Class1.Print";
});
});
return class1GlobalClass;
}

protected override string CodeBase
{
get { return typeof(Class1).Assembly.CodeBase; }
}

protected override string Location
{
get { return typeof(Class2).Assembly.Location; }
}
}

public class GlobalClass2 : GlobalClass
{
protected override DependencyClass OnRegister()
{
// Class2 の処理を入れ替える。
var class2GlobalClass = new GlobalClass<Class2>();
class2GlobalClass.Setup(o =>
{
o.HideMethod<string, string>(_ => _.Print).By(
value =>
{
return "Modified prefix at Class2.Print" + value + "Modified suffix at Class2.Print";
});
});
return class2GlobalClass;
}

protected override string CodeBase
{
get { return typeof(Class2).Assembly.CodeBase; }
}

protected override string Location
{
get { return typeof(Class2).Assembly.Location; }
}
}
}



Test.Urasandesu.NAnonym.Cecil.dll
テスト。ちなみに、「入れ替えるクラスを登録。」の処理~「入れ替え。」の処理をコメントアウトすると、ちゃんとテストに通らなくなる。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
using Test.Urasandesu.NAnonym.Etc;
using Urasandesu.NAnonym.Cecil.DW;
using Urasandesu.NAnonym.Test;
using Assert = Urasandesu.NAnonym.Test.Assert;

namespace Test.Urasandesu.NAnonym.Cecil.DW
{
[TestFixture]
public class GlobalClassTest
{
[TestFixtureSetUp]
public void TestFixtureSetUp()
{
// 元の状態に戻す。
GlobalDomain.Revert();

// 入れ替えるクラスを登録。
GlobalDomain.Register<GlobalClass1>();
GlobalDomain.Register<GlobalClass2>();

// 入れ替え。
GlobalDomain.Load();
}


[Test]
public void Class1Class2Test()
{
var class1 = new Class1();
var class2 = new Class2();
string value = "aiueo";

Assert.AreEqual(
"Modified prefix at Class1.Print" +
"Modified prefix at Class2.Print" +
value +
"Modified suffix at Class2.Print" +
"Modified suffix at Class1.Print",
class1.Print(value));

Assert.AreEqual(
"Modified prefix at Class2.Print" +
value +
"Modified suffix at Class2.Print",
class2.Print(value));
}
}
}



入れ替えるための設定で書く言語内 DSL がわかりにくい上に使いにくい(特に VB からだと本当にゴメンナサイってなる)、入れ替え対象の Assembly を AppDomain へ Load するタイミングとの兼ね合いで、入れ替えるための設定を別 Assembly にしなくちゃいけないし、テスト実行のタイミングによっては Revert がうまく行かなくて元に戻らなくなる、等々細かなまずい部分はあるのだけれど、設計から見直さないとどうにもならないなーっていうのが、

  • パフォーマンス


上に書いたような簡単な入れ替えでも 2 秒以上かかっちゃって(Windows XP SP3, CPU: Core 2 Duo 1.2 GHz, RAM: 2 GB, HDD: Intel SSD X25-M 80GB)、目標値より 200 倍ほど遅い。一応ネックになる処理はわかっているのだけれども、

  • AppDomain の Load/Unload


という、またどうにもならなさげなもの。このままの設計で進めるのは難しいと判断し、しばらく触るのを止めてます。
@super_rti さんの SEXYHOOK みたく、テスト中は実行時のメモリ状態だけを書き換えられて、動的言語のモンキーパッチみたく、実際の実行時はバイナリに織り込むようなことができれば最高なのだけれど…。



Moles - Isolation framework for .NET
NAnonym を作り出してから知ったのですけど、私がやりたいことの半分は Microsoft Research で作成されている Moles を使えば実現できます。公式のドキュメントは、ここの中ほどにあります「Unit Testing with Microsoft Moles」がステップバイステップで使い方が書いてあってわかりやすいです。リファレンスも、同じページに「Microsoft Moles Reference Manual」を見つけられます。
日本語のサイトですと、@neuecc さんの「neue cc - Rx + MolesによるC#での次世代非同期モックテスト考察」や、@t_nakamura さんの「さすがMoles!Moq たちにできない事を平然とやってのけるッ - present」、@Jamzz さんの「Moles - .NETのモック・スタブフレームワーク - Jamzzの日々」が参考になると思いますです。


Moles でいいんじゃね?
半分は~っていうのは、2 点ほどどうにもならないことがあって、

  • 入れ替えた結果を固定できない(バイナリに織り込めない)

  • ライセンス


…はい、別に固定できなくても、毎回 Moles 経由して実行(もしくは Moles をキックするランチャー経由で実行)すればいいだけ、と思ったこともあったんですが、そもそも商用サービスのために Moles を使うことはライセンスで止められてるんですよね(ライセンスに関してはこちらに。下のほうに「Moles Visual Studio 2010 Power Tools」ってところがあって、そこからソフトウェア使用許諾契約書がダウンロードできる)。名前の通り、開発時に使うツール、という限定された使い方しかできないということでしょう。
また、Microsoft Research の成果っていうのは Moles に限ったことではないようですが、ソースコードが公開されていないため、処理の解析や参考にもできません。ぐぬぬ…。



\(^o^)/
いいえ。Google で "microsoft moles how implemented" みたいな検索をすると、「c# - How Moles Isolation framework is implemented? - Stack Overflow」っていう QA が見つかります。その回答を見ると処理内容はこんなふうになってるよ、って言い切っててすごいっ、と思うのですが、回答者の Peli さんのプロフィールを見ると「I'm working on Pex at Microsoft Research.」って書いてあります・・・中の人、答え書いちゃったよ!
まあ全然極秘事項ってわけではなく、Moles のマニュアルを読んでいると、「the Moles framework uses a profiler to rewrite the method bodies to be able to redirect method calls.」とか「The mole types rely on runtime code rewriting, which is implemented by a CLR profiler.」、「The mole types require a CLR profiler to be installed on the machine to execute the tests.」っていう記述が出てきてますし、そもそも MSDN のアンマネージ API リファレンスのプロファイル API のトピック、「Profiling Overview」には、「プロファイル API を使用すると、JIT コンパイルをフックして、メモリ内 MSIL コード ストリームを変更できるよー。」ってあるので、CLR のプロファイラに関わる仕事されてる方にとっては、よく知られたことなのでしょう。



C++、アンマネージ API の世界へ
私は基本的に仕事でも C# を使っていて、アンマネージ API で主戦力となる C++ 使いではありません。テンプレートやら STL やら Boost やら COM やら ATL やら…歴史がある言語な分、覚えなければならないこともたくさんあるし、公開されているソースコードのビルド通すにも暗黙の了解があったり、デバッグの仕方がよくわからなかったり…で遅々としてしか進んでいないのが現状です。次の Memo では、そんな C++ の自分向け覚書と、アンマネージ API の入り口を書き留められればと...( = =)