SourceForge.net Logo

ObjectPeepHoleとは

岡嶋 大介

 ObjectPeepHoleライブラリを使うと、任意の.NETのオブジェクトについて、

 ができます。

 このときあなたがしなければならないことは、所定のルールに従ってインタフェースを書くだけです。そのインタフェースにあわせて実装を実行時に生成するのがこのライブラリのミソです。

何に使うの?

 主にNUnitなどの単体テストのテストコードを書く支援を想定しています。

 本来はprivate/protectedなフィールドやメソッドを、テストケースで呼び出す必要があるというだけのためにやむをえずinternalやpublicにすることはよくあります。ObjectPeepHoleを使うとprivate/protectedなものに直接アクセスできるので、そのような回避をする必要がありません。

 より重要なのはイベントの発行です。たとえばGUIアプリケーションを単体テストするためには、キーボードやマウス関係のイベントを自在に発行できなければ不便で仕方ないですが、.NET Framework単独ではそれはできません。(少なくともMicrosoftの実装ではそうです。Monoは調べてません)

 ObjectPeepHoleを使うとインタフェースに所定の名前でメソッドを書くだけでイベントの発行ができます(実はこれがObjectPeepHoleを作成した最初の動機です)。.NETにはPerformClick()メソッドなど、一部のイベントについては苦し紛れにイベント発行用の機能がついていたりしますが、このObjectPeepHoleを使うとWinFormsのほぼすべてのクラスとイベントが一挙にカバーできます。

 フィールドやメソッドのアクセスは、FieldInfo.GetValue()MethodInfo.Invoke()を使えばObjectPeepHoleでなくてもたしかに可能ですが、これはインタフェースを書くだけでよいので、単体テストのコードを書くときにふつうのクラスであるかのように扱えますし、IntelliSenseの恩恵をフルに受けることができる点で優れています。

ダウンロード

ダウンロード(SourceForge.net内)

 使い方の質問、機能追加の要望等はsourceforgeのプロジェクトページの各機能を使っていただくか、okajima@lagarto.co.jp(@はスパムよけのため全角になってます)までメールにてご連絡ください。

 ちなみに、ObjectPeepHoleは私が開発した株式取引用プラットフォームTacticoのGUIテストを自動化する方法を考えているうちに思いついたものです。

使い方

 ターゲットになるクラス1つにつきインタフェースを1つ書きます。

 例えばこんなクラス Foo があるとして、単体テスト用にprivateなもののアクセスやイベント発行がしたいとします。

class Foo {
  private string _name;
  private int SomeMethod(string a) {
    ...
  }
  public EventHandler SomeEvent;
}
 それに対し、こんなインタフェース IFooAccess を書きます。
public interface IFooAccess {
  string name { get; }       (1)
  int SomeMethod(string a);  (2)
  void FireSomeEvent(EventArgs args);  (3)
}

 そして、実行時にはこのようにするだけです。

Foo foo = (アクセスしたいインスタンス)
PeepHoleBuilder bld = new PeepHoleBuilder(typeof(Foo), typeof(IFooAccess));
IFooAccess a = (IFooAccess)bld.CreateAccessor(foo);

string name = a.name; //fooの_nameが取れます!
a.SomeMethod("zzz");  //fooのSomeMethodが呼べます!
a.FireSoomeEvent(new EventArgs()); //fooのSomeEventイベントが発生します!

すると、IFooAccessインタフェースに書いたプロパティやメソッドを呼び出すことで、fooの対応するフィールドにアクセスしたりイベントを発行したりできるのです! あなたが書くのはIFooAccessインタフェースだけで、IFooAccessを実装したクラスは実行時にObjectPeepHoleが生成してくれるのです。

インタフェースのルール

(1) フィールド

 private/protectedフィールドのアクセスをするには、インタフェースにプロパティを宣言します。名前は先頭のアンダースコアを除いて一致し、型は同じでなければいけません。フィールドのセットがしたい場合はインタフェースのプロパティに { get; set; }とすればOKです。また、対象のフィールドがstaticでも大丈夫です。

(2) メソッド

 private/protectedメソッドの呼び出しをするには、名前・引数の構成・戻り値の型がすべて一致するメソッドをインタフェースに書きます。対象のメソッドがstaticでも大丈夫です。

(3) イベント−1

 イベントの発行をするには、次の名前でメソッドをインタフェースに宣言します。

(4) イベント−2

 さらに、対象のクラスのフィールドのイベントを発行することもできます。これはダイアログボックスのテスト等で便利で、

class SomeForm : Form {
  private Button _button1;
}
とあるときに、
interface SomeFormAccess {
  void FireButton1_Click(EventArgs args);
}
と書くと_button1のClickイベントが発行できます。

 このスタイルのルールは、

 です。

注意点

  1. System.Windows.Forms内のクラスのイベントの発行については、マイクロソフトの現在の実装をハックして仕組みを作っています。Monoではおそらく異なる実装なので動作しないと思います。
  2. WinFormsであっても次のクラスについては未サポートです。それ以外は全クラス・全イベントで動作確認しています。
  3. メソッドの呼び出しのうち、Genericsを使ったものと可変個引数を使ったものは未サポートです。これは要望があれば対応を考えます。出力引数(refやoutを使うもの)はOKです。
  4. イベントの発行では、そのイベントに関連付けたデリゲートの呼び出しだけが行われます。イベントに関連づけられたメソッド(Clickイベントに対するOnClickなど)は呼ばれません。

マニア向けの話題

イベント発行について

 そもそも、イベントの発行はそれを宣言したクラスからしか行えない(派生クラスでもダメ)というのが厳しい制約です。これは知っている人も多いと思います。

 イベントの発行はEventInfo.GetRaiseMethod を使って呼び出せばよいと思われた方は鋭いです。ところが、実際これを呼び出すとnullが返るのでだめなのです!

 そこで、ildasmを使ってSystem.Windows.Forms.dllを読み込み、どのような手順でイベントを発行しているのかを調べてみると、次の2種類があることがわかりました。WinFormsには無数のクラスとイベントがありますが意外と少ないですね。

(1) delegateがフィールドとして存在する場合

 イベントに対応した名前で、Delegate型のフィールドがあるパターンです。例えば, TreeViewにAfterLabelEditイベントハンドラを追加するILコードを読みだしたらこんな感じです。

.method public hidebysig specialname instance void 
        add_AfterLabelEdit(class System.Windows.Forms.NodeLabelEditEventHandler 'value') cil managed
{
  IL_0000:  ldarg.0
  IL_0001:  dup
  IL_0002:  ldfld      class System.Windows.Forms.NodeLabelEditEventHandler System.Windows.Forms.TreeView::onAfterLabelEdit
  IL_0007:  ldarg.1
  IL_0008:  call       class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
                                                                                          class [mscorlib]System.Delegate)
  IL_000d:  castclass  System.Windows.Forms.NodeLabelEditEventHandler
  IL_0012:  stfld      class System.Windows.Forms.NodeLabelEditEventHandler System.Windows.Forms.TreeView::onAfterLabelEdit
  IL_0017:  ret
} 

(2) イベントに対応したstaticなキーを使う場合

 イベントの種類ごとにstaticなオブジェクトがあり、これをキーとしてEventHandlerListに問い合わせるパターンです。例えば, ControlにDoubleClickイベントハンドラを追加するILコードを読みだしたらこんな感じです。

.method public hidebysig specialname instance void 
        add_DoubleClick(class [mscorlib]System.EventHandler 'value') cil managed
{
  IL_0000:  ldarg.0
  IL_0001:  call       instance class [System]System.ComponentModel.EventHandlerList [System]System.ComponentModel.Component::get_Events()
  IL_0006:  ldsfld     object System.Windows.Forms.Control::EventDoubleClick
  IL_000b:  ldarg.1
  IL_000c:  callvirt   instance void [System]System.ComponentModel.EventHandlerList::AddHandler(object, class [mscorlib]System.Delegate)
  IL_0011:  ret
}

 どちらのパターンを使うかはクラスごとに異なるようでしたが、1つのクラス内でイベントの種類ごとに違うということはないみたいです。また、ユーザ自身が書いたクラスのイベントは(2)のパターンになるようです。各クラスがどちらのパターンに従うかがわかったら、その構造に沿う形でイベントハンドラを呼び出すILコードを生成しました。

 また、イベントに関連づけられたメソッド(Clickイベントに対するOnClickなど)を呼ぶことでも関連づけたデリゲートが呼ばれることは呼ばれますが、OnClickではデリゲートを呼ぶ以外の動作もしているため副作用が起きる危険があります。状況に応じて使い分ければよいでしょう。

動的な型生成について

 ObjectPeepHoleの主要なアイデアは、ユーザが書くのはインタフェースだけで、それを実装したクラスは実行時に動的に作るというものです。この動的なクラス内の各メソッドはILを直接生成するので、割と柔軟に動作できるはずなのですが、ldfldやcallといったインストラクションはprivate/protectedな対象には使用できないみたいです。実行するとFieldAccessException等の例外が発生します。アセンブリのセキュリティ関係の設定次第で何とかなるのかもしれませんがそれは不明なままでした。もし情報があれば教えてくれると助かります。

 そこで仕方なく、FieldInfo.GetValueMethodInfo.Invokeを呼び出すようなILを生成してシノいでいます。特にInvokeは引数をobject[]に詰めなおす必要があり、となるとboxingや参照型のことを考慮する必要があって面倒きわまりないんですが仕方ないですね。