RealProxy (実プロクシ) と透過的プロクシと IRemotingTypeInfo

CLI(や CLR) は、通常はスタックベース*1の世界でメソッド呼び出し*2を処理している。しかし、CLI にはそれらスタックベースの処理を、メッセージと呼ばれるオブジェクトを受け渡す形に変換する機構が備わっている。

どのような機構かと言うと、透過的プロクシと呼ばれるものがあり、この透過的プロクシは普通のオブジェクトインスタンスの如く振る舞う。が、透過的プロクシに対するメソッド呼び出し(など)は、CLR によって実プロクシと呼ばれるものに委譲され、実プロクシで煮るなり焼くなりすることが出来てしまうのである。

System.Runtime.Remoting.Proxies.RealProxy

具体的には、RealProxy というクラスを継承&実装することでこの機構を使うことが出来る。

RealProxy は実プロクシを作るための基底クラスであり、

public virtual object GetTransparentProxy();

という透過的プロクシのインスタンスを返すメソッドを持っている。なお、RealProxy(実プロクシ)のインスタンスと透過的プロクシのインスタンスは 1 対 1 対応しており、RealProxy に対応する透過的プロクシのインスタンスCLR が自動で作ってくれる*3

また、RealProxy は

public abstract IMessage Invoke(IMessage msg);

という唯一の抽象メソッドを持っている。RealProxy.GetTransparentProxy() によって得られた透過的プロクシに対してメソッド呼び出しなどを行うと、そのメソッド呼び出しが普通に処理*4される代わりに、透過的プロクシに対応する RealProxy の Invoke メソッドが呼ばれる。

Invoke の引数の IMessage (IMethodCallMessage であることがほとんど) にはそのメソッド呼び出しがどのようなものであるかが記述されており、Invoke の戻り値 IMessage (たいていは IMethodReturnMessage) が、件のメソッド呼び出しの結果となるのである。

これらの機構によって、

class 実プロクシ : RealProxy{
    public 実プロクシ() : RealProxy(...){}

    public IMessage Invoke(IMessage msg){
         // msg の中身を見て何かする
         return new ReturnMessage( ... );  // 結果を返す
    }
}

var 透過的プロクシ = (new 実プロクシ()).GetTransparentProxy();

// 透過的プロクシに対して施した操作は、透過的プロクシに対応する実プロクシの Invoke に転送される。
// Invoke の返した IMessage に応じて、戻り値や例外が返ってきたり投げられたりする。
透過的プロクシ.ほげほげ( ... );

といった風に、普通のオブジェクトと同じく振る舞いつつも、メソッド呼び出しなどを自由にハンドリングできるのである。なお、上のコードにある ReturnMessage は IMethodReturnMessage を実装する既存の型で、メソッドの正常終了(戻り値付き)や例外の throw などを表すことが出来る。

このメカニズムを使うと、(動作速度は落ちるものの)オブジェクトを実装する自由度を高めることが出来るのである。特にリモートオブジェクトや、DI コンテナといったフレームワークやライブラリを作ろうと思った場合に非常に便利である(そもそも RealProxy などが System.Runtime.Remoting という名前空間を冠しているぐらいである)。

プロクシの型と System.Runtime.Remoting.IRemotingTypeInfo

上で透過的プロクシが普通のオブジェクトインスタンスの如く振る舞うと述べたが、透過的プロクシの型はどうなるのであろうか。つまり、

object 透過的プロクシ = 実プロクシ.GetTransparentProxy();

透過的プロクシ.GetType();    // どうなる?
透過的プロクシ is IHogeHoge; // どうなる?

といった際の振る舞いがどうなるのか、という点である。

これについては、普通に RealProxy を実装する場合、RealProxy(Type) という Type を引数に取るコンストラクタがあるので、これを呼んでやればよい。そうすることで、その RealProxy インスタンスに対応する透過的プロクシがどの型のインスタンスとして振る舞うのかを変更する事が出来る。ちなみに、RealProxy の引数を取らないコンストラクタを読んだ場合、RealProxy.GetTransparentProxy() が null を返してしまうので、理由がない限り RealProxy のコンストラクタには型を指定してした方が楽である。

ただ、場合によっては、透過的プロクシがどの型のインスタンスとして振る舞うのかを RealProxy のコンストラクタではなく、もっと別のタイミングで決定したくなることもある*5。その場合、System.Runtime.Remoting.IRemotingTypeInfo を実プロクシにて実装する手がある。

IRemotingTypeInfo インターフェースは、

string TypeName { get; set; }
bool CanCastTo(Type, object);

のように、インスタンスの型に関する振る舞いを提供することが出来るインターフェスである。

このうち、TypeName が透過的プロクシの GetType() などの振る舞いを決定し、CanCastTo が透過的プロクシに対する as や is、それにキャスト演算の結果*6などを左右する。

参考書籍

ちなみに、これらについては以下の書籍がとてもわかりやすい(コードを書く際にも参考にした)。プロクシやメッセージ関連についても、今回述べていない事柄(任意のインターフェースなどにプロクシを自動的にあてがう方法、とか)も述べられており、参考になる。

Essential .NET ― 共通言語ランタイムの本質

Essential .NET ― 共通言語ランタイムの本質

*1:CLI実装依存。だが、どうにせよ通常ならメソッドコールなどのプロセスには割り込めない。

*2:コンストラクタやプロパティやイベントなどの処理も含む

*3:なんなら GetTransparentProxy() を override して、自分で作っても構わない。

*4:普通のメソッド(関数)呼び出し

*5:リモートオブジェクト機構を作る場合などで、RealProxy インスタンスを作った後に、リモートから型情報を取ってくる場合とか。

*6:ただし、キャスト演算子が定義されている場合、そちらが優先されてしまったりする。