2010年7月18日日曜日

C# 魔改造 01 - Wish List(or Design Goal) -

ソフトウェアって思ったより"ソフト"じゃないじゃない?って思ったのが発端。

結果がわからないものや、とりあえず反応が見てみたいものはプロトタイピングしながら作っていくことが少なくないと思うけど、さて動くものができたって段階になると作り直すのもあれだし…って感情がたぶん湧く。お金掛けて作った(作ってもらった)ものは特に。こんな時に相談された場合には必ず「作り直したほうがいいものができる」って思うと思う。でも時間やお金の問題、色んな状況や立場の人がいらっしゃる場所では、完全にゼロからやり直すのは難しい。

プログラム作る時、テストを書く人が増えてきていると思う。納品物として、ユニットテストのソースコードも指定するお客さんがいたり、オープンソースのコードを読もうとすると大抵付いてたり。TDD って言葉も知ってる人が多いみたい。テストを書いておけば、特に最初の立ち上げが有利になるし、変更があった場合もデグレが発見されやすい。適当な場所にブレークを張って動かせるのは、動作の理解にも役に立つ。良いことが多いのだけれど、良いテストを書くにはたくさん訓練しなければならないし、最初から条件を揃えるのはやっぱり試行錯誤が必要になる。

経緯はこんな感じです。ソフトウェアの動作を安全にかつ根本的に変える治具?のようなライブラリがあってもいいんじゃないかと思いましたので、とりあえず今あるアイディア、吐き出してみました。



以下の記事/ライブラリを使用/参考にさせていただいてます。ありがたく使わせていただきますm(_ _)m
CodeDom Assistant - CodeProject
 http://www.codeproject.com/KB/cs/codedom_assistant.aspx
CodeDOM, <providerOption name="CompilerVersion" value="3.5" /> not working?! and how to retrieve providerOption at runtime?
 http://social.msdn.microsoft.com/Forums/en-US/netfxbcl/thread/512d9fdd-61af-4a0c-b78a-2f88738e651a
Creating and Initializing Objects in CodeDom [Benet Devereux] - BCL Team Blog - Site Home - MSDN Blogs
 http://blogs.msdn.com/b/bclteam/archive/2006/04/10/571096.aspx
DynamicProxy :: Castle Project
 http://www.castleproject.org/dynamicproxy/index.html
LINQ Expression Trees-Lambdas to CodeDom Conversion | Coding Day
 http://www.codingday.com/meta-programming-with-expression-trees-lambdas-to-codedom-conversion/
moq - Project Hosting on Google Code
 http://code.google.com/p/moq/
C# 3.0 Supplemental Library: Achiral - NyaRuRuの日記
 http://d.hatena.ne.jp/NyaRuRu/20080115/p1
NUnit - Home
 http://www.nunit.org/index.php?p=home
ECMA C# and Common Language Infrastructure Standards
 http://msdn.microsoft.com/en-us/netframework/aa569283.aspx
Cecil - Mono
 http://www.mono-project.com/Cecil
.NET Reflector, class browser, analyzer and decompiler for .NET
 http://www.red-gate.com/products/reflector/
Reflexil | Download Reflexil software for free at SourceForge.net
 http://sourceforge.net/projects/reflexil/
TomCarter.Developer - DSM Plugin for Reflector.NET
 http://tcdev.free.fr/
SequenceViz
 http://sequenceviz.codeplex.com/wikipage?title=ReflectorPlugin&ProjectName=sequenceviz
.NET Reflector Add-Ins ReflectionEmitLanguage
 http://reflectoraddins.codeplex.com/wikipage?title=ReflectionEmitLanguage&referringTitle=Home
Seasar.NET プロジェクト
 http://s2container.net.seasar.org/ja/seasarnet.html



1. 式木 + CIL - ExpressiveILProcessor -
式木がせっかく強力なので、もう少し微調整ができるとうれしいと思いついた、式木の中身を CIL に展開してくれるクラス。System.Reflection.Emit 名前空間や Mono.Cecil.Cil 名前空間のクラスと組み合わせて強力な縁の下の力持ちになってくれるはず。

ExpressiveILProcessor の利用イメージ
Program.cs
呼び側のイメージ。

using System;
using System.Reflection.Emit;
using Mono.Cecil;
using Urasandesu.NAnonym.CREUtilities;
using MC = Mono.Cecil;
using SR = System.Reflection;

namespace Test
{
class Program
{
static void Main()
{
// ToTypeDef は System.Type から Mono.Cecil.TypeDefinition に変換する拡張メソッド。
var mainDef = typeof(Program).ToTypeDef();

var testDef = new MethodDefinition("Test",
MC.MethodAttributes.Private | MC.MethodAttributes.Static, mainDef.Module.Import(typeof(void)));
mainDef.Methods.Add(testDef);

// 式木で直接 Emit。
var egen = new ExpressiveILProcessor(testDef);
egen.Emit(_ => Console.WriteLine("aiueo"));

// .NET 3.5 までだと変数の宣言や代入式無理。
// → ExpressiveILProcessor 自身のメソッドを定義。
int a = 0;
egen.Emit(_ => _.Addloc(() => a, default(int)));
egen.Emit(_ => _.Stloc(() => a, 100));

// ローカル変数に入れた上ににアクセスする場合。
var cachedAnonymousMethod = default(DynamicMethod);
var gen = default(ILGenerator);
var label27 = default(Label);
egen.Emit(_ => _.Addloc(() => cachedAnonymousMethod,
new DynamicMethod("cachedAnonymousMethod", typeof(string), new Type[] { typeof(string) }, true)));
egen.Emit(_ => _.Addloc(() => gen, _.Ldloc(() => cachedAnonymousMethod).GetILGenerator()));
egen.Emit(_ => _.Addloc(() => label27, _.Ldloc(() => gen).DefineLabel()));
egen.Emit(_ => _.Ldloc(() => gen).Emit(SR.Emit.OpCodes.Brtrue_S, _.Ldloc(() => label27)));

// 通常の Emit 処理混合。
egen.Emit(_ => _.Direct.Emit(MC.Cil.OpCodes.Ldc_I4_S, (sbyte)100));
egen.Emit(_ => _.Direct.Emit(MC.Cil.OpCodes.Stloc, _.Locals(() => a)));
}
}
}





2. Java の便利を C# にも - LocalClass -
interface を受け取るメソッドと匿名~が相性悪いと思いついた。Java のローカルクラスをイメージ。moq が近かったのだけれど、所詮は式木だったので (^_^;)。.NET 4.0 になればもう少し多機能化されるのかも。

LocalClass の利用イメージ
Program.cs
呼び側のイメージ。

using System;
using Urasandesu.NAnonym.DI;

namespace Test
{
public class Program
{
static void Main()
{
var localClass = new LocalClass<IHoge>();
// Setup で中身を編集。
localClass.Setup(the =>
{
// メソッドの設定。中身を定義し、Override で同じ I/F を持つメソッドをオーバーライド。
the.Method(() =>
{
if (DateTime.Now < new DateTime(2010, 1, 1))
{
Console.WriteLine("こんにちは!世界!");
}
else
{
Console.WriteLine("Hello, World!!");
}
})
.Override(_ => _.Output);

the.Method(() =>
{
return "Hello, Local Class !!";
})
.Override(_ => _.Print);

the.Method((string content) =>
{
return "Hello, " + content + " World !!";
})
.Override(_ => _.Print);



// プロパティの設定。中身を定義し、Override で同じ I/F を持つプロパティをオーバーライド。
int this_value = 0;
the.Property(() =>
{
return this_value;
})
.Override(_ => () => _.Value);

the.Property((int value) =>
{
this_value = value * 2;
})
.Override(_ => value => _.Value = value);
});

// Load でアセンブリ生成。キャッシュ。
localClass.Load();

// New でインスタンス化。
var hoge = localClass.New();

// 実行
hoge.Value = 10;
Console.WriteLine(hoge.Value);
Console.WriteLine(hoge.Print());
Console.WriteLine(hoge.Print("Local Class"));
/*
* 20
* Hello, Local Class !!
* Hello, Local Class World !!
*/
}
}

// 対象のインターフェース
interface IHoge
{
int Value { get; set; }
void Output();
string Print();
string Print(string content);
}
}




3. Load Time Weaving - GlobalClass -
これが一番核になると思う。I/F は LocalClass のそれを踏襲。.NET Framework はアセンブリの厳密名があるので、そこまで自由には Weaving できないけど、変更がだいぶ気前良くできるようになるはず。

GlobalClass の利用イメージ
Class1.cs
元のクラス 1

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



Class2.cs
元のクラス 2

namespace Test
{
public class Class2
{
public string Print(string value)
{
return "こんにちは、" + value + " 世界!";
}
}
}



GlobalClass1.cs
元のクラス 1を Weaving する設定。別 Assembly にする必要がありそう。

using Test.Urasandesu.NAnonym.Etc;
using Urasandesu.NAnonym.DI;

namespace Test
{
public class GlobalClass1 : GlobalClassBase // AppDomain を超える必要があるため、GlobalClassBase は MarshalByRefObjectを継承。
{
protected override GlobalClassBase SetUp()
{
var class1 = new GlobalClass<Class1>();
class1.SetUp(the =>
{
the.Method((string value) =>
{
return "Modified prefix at Class1.Print" + new Class2().Print(value) + "Modified suffix at Class1.Print";
})
.Instead(_ => _.Print);
});
class1.Load();
return class1;
}

private string NewPrint(string value)
{
return "Modified prefix at Class1.Print" + new Class2().Print(value) + "Modified suffix at Class1.Print";
}
}
}



GlobalClass2.cs
元のクラス 2を Weaving する設定。やはり別 Assembly にする必要がありそう。

using Test.Urasandesu.NAnonym.Etc;
using Urasandesu.NAnonym.DI;

namespace Test
{
public class GlobalClass2 : GlobalClassBase // AppDomain を超える必要があるため、GlobalClassBase は MarshalByRefObjectを継承。
{
protected override GlobalClassBase SetUp()
{
var class2 = new GlobalClass<Class2>();
class2.SetUp(the =>
{
the.Method((string value) =>
{
return "Modified prefix at Class2.Print" + value + "Modified suffix at Class2.Print";
})
.Instead(_ => _.Print);
});
return class2;
}
}
}



Program.cs
呼び側のイメージ。

using System;
using NUnit.Framework;
using Test.Urasandesu.NAnonym.DI;
using Test.Urasandesu.NAnonym.Etc;
using Urasandesu.NAnonym.DI;

namespace Test
{
public class Program
{
static Program()
{
// Inject、AcceptChanges は System.AppDomain の拡張メソッド。
// 新しい AppDomain を作成し、Load Time Weaving を行った後、元ファイルを入れ替える。
// ジェネリックパラメータに Weaving 設定用のクラスを指定。
// ※元のファイルが Load されるより先に実行させる必要がある。
AppDomain.CurrentDomain.Inject<GlobalClass1>();
AppDomain.CurrentDomain.Inject<GlobalClass2>();
AppDomain.CurrentDomain.AcceptChanges();
}

static void Main()
{
var class1 = new Class1();
var class2 = new Class2();

// 実行
string value = "aiueo";
Console.WriteLine(class1.Print(value));
Console.WriteLine(class2.Print(value));
/*
* Modified prefix at Class1.PrintModified prefix at Class2.Print aiueo Modified suffix at Class2.PrintModified suffix at Class1.Print
* Modified prefix at Class2.Print aiueo Modified suffix at Class2.Print
*/
}
}
}





作っていくうちに必要になったユーティリティもいっしょに公開していく予定。今年中には 1 パス動くといいな ...( = =)