MethodLogger (4)

メソッド呼び出しを RemotingServices.ExecuteMessage() や MethodInfo.Invoke() を利用して行う場合、this として proxy を渡すことができないだろうか? というところで終わっていました。
しかし、そんな簡単に事が済んだら苦労はしません。
RemotingServices.ExecuteMessage() や MethodBase.Invoke() に対して proxy を this として渡すと、再び自分自身の Invoke() に飛び込んでくるため、無制限に再帰処理してしまいプログラムが異常終了するはずです。

スタブのプロキシを作る

ということは、自分自身の呼び出しを自分自身ではない別のスタブへリダイレクトしたら?

public static MarshalByRefObject Attach(MarshalByRefObject o)
{
  Trace.Assert(o != null);
  
  MethodLogger stub = new MethodLogger(o);
  MethodLogger proxy = new MethodLogger(o);
  
  proxy.target = stub.GetTransparentProxy() as MarshalByRefObject;
  
  return proxy.GetTransparentProxy() as MarshalByRefObject;
}

と、二段構えにしてみる。しかし、この程度ではうまくいかない。
結局のところ、Form1 クラスのロジックを呼び出せるインスタンスは Form1 のインスタンスだけであり、stub から簡単に参照することはできないのだろうか?

ContextBoundObject クラス

元々、Object 派生クラスは、外部からのメソッド呼び出しを解決する処理に対して割り込むことができなかった。*1
これは MarshalByRefObject を派生元クラスにすることで解決し、CLROOP のメッセージ送受信を IMessage と Invoke() を利用した形にして、割り込み可能な状態になった。
同様に、MarshalByRefObject の派生クラスである ContextBoundObject クラスが、今回の問題を解決できる能力をもったクラスとして提供されている、ということを知らないと、この問題は解決できない。

というわけで、派生元クラスを ContextBoundObject に変更して完成……なんて都合のよい話はなかった。

*1:実は割り込めるのだが、それはまた別の機会に

ContextBoundObject クラスに関する文章

まずは、SDK の解説を読む必要があるわけだが、ContextBoundObject クラスから入っていくと、はっきり言って意味不明に近い解説が書いてあると思ってしまう人が多いのではないだろうか。
メソッドは何も増えていないし、説明はわけわからない。関係あるクラスとして紹介されている SynchronizationAttribute クラスなんて、

コンテキスト バインド オブジェクトの詳細については ContextBoundObject クラスのトピックを参照してください

なんて書いてある始末。
ここで見るべきは日本語ドキュメントでは「高度なリモート処理」で、SDK を RealProxy と ContextBoundObject の2つの単語で全文検索すると、意外に簡単に見つけることもできる。

new Form1() を乗っ取る

というわけで、上記のトピックを読めばやることはもうわかってしまうと思いますが、話を単純にすると、やるべきことは new の乗っ取りです。
乗っ取られるクラスは ContextBoundObject の派生クラスであるほかに、ProxyAttribute の派生クラスを属性として保持する必要があり、new に対して proxy を生成する必要がありました。
まずは、ProxyAttribute の派生クラスですが、CreateInstance() というわかりやすそうなメソッドを override することができるようになっています。

public class Form1ProxyAttribute : ProxyAttribute
{
  public override MarshalByRefObject CreateInstance(Type serverType)
  {
    return MethodLogger.Attach( base.CreateInstance(serverType) );
  }
}

もう、何も考える必要はないですよね?*1
このカスタム属性を Form1 クラスに付与することで、new Form1 を乗っ取ることに成功し、this を含めたすべてのメソッド呼び出しの記録が……。

*1:ドキュメントを読むと考えたくなるような記述はいっぱいありますが…

new をきちんと乗っ取る

上記のコードでは、正常に実行できません。
base.CreateInstance() で手に入ったオブジェクトに対して GetType() を呼び出すと「コンストラクタ呼び出しが進行中にプロキシを呼び出そうとしました。」と怒られて終了してしまいます。
ProxyAttribute のドキュメントにも記載がありますが、この部分は new の処理全体のうち序盤の序盤部分ですので、まだメソッドを呼び出したりすることができない状態のオブジェクトを扱っていることになります。
当然ですが、

public class Form1ProxyAttribute : ProxyAttribute
{
  public override MarshalByRefObject CreateInstance(Type serverType)
  {
    return MethodLogger.Attach( new Form1() );
  }
}

などとしたら、new Form1() が Form1ProxyAttribute.CreateInstance() を呼び出して再帰してしまうので、base.CreateInstance() が利用するオブジェクトを使うしか、選択肢はありません。
ここは MethodLogger のコンストラクタや Attach() を変更して、CreateInstance() の引数である serverType を利用して RealProxy を生成する必要があります。

CreateInstance の後

CreateInstance() の修正が終わっても、まだ new の処理はおわりません。
やっと stub の Invoke() に処理がやってくるのですが、最初の一発は new した直後に呼ばれるメソッド…、当然コンストラクタの呼び出しです。
コンストラクタと他のメソッドの大きな違いとして、呼び出し元コンテキストが異なるらしく、RemotingServices.ExecuteMessage() は例外を出して利用できないのですが、RealProxy クラスのメンバをちゃんと眺めていれば、この問題は即解決です。

public override IMessage Invoke(IMessage msg)
{
  // :(前略)
  IConstructionCallMessage ctor = msg as IConstructionCallMessage;
  if (ctor != null)
  {
    return this.InitializeServerObject(ctor);
  }
  // :(以下略)

見事にコンストラクタの呼び出しが記録され……、まだ終わらないですね。

InitializeServerObject() により返されるものは?

コンストラクタ以降のメソッド呼び出し等が Invoke() にわたって来ないため、どうも InitializeServerObject() によって本物のインスタンスを返してしまっているようです。
対象のオブジェクトが実インスタンスなのか、TransparentProxy なのかは、RemotingServices.IsTransparentProxy() で簡単に調べることができ、stub も RemotingServices.GetRealProxy() で取得することができます。
InitializeServerObject() の ReturnValue に対して RemotingServices.IsTransparentProxy() を実行すると、なんと false が帰ってきてしまうので、予想通り当然の結果のようです。

コンストラクタの戻り値を変更する

やるべきことは、Invoke() の戻り値の ReturnValue を MethodLogger の生成した proxy にすることです。ここまでの動作を見ると、CreateInsntance() によって取得された proxy はコンストラクタを呼び出すために利用されるが、new した結果のオブジェクトは Invoke() の ReturnValue を利用される、ということだからです。
実装については、前述の高度なリモート処理のトピックで触れられているため、すぐに行うことができます。

IConstructionCallMessage ctor = req as IConstructionCallMessage;
if (ctor != null)
{
  return EnterpriseServicesHelper.CreateConstructionReturnMessage(
            ctor, this.GetTransparentProxy() as MarshalByRefObject);
}

コンストラクタ呼び出しを引数にうけてくれるので、とりあえず InitializeServerObject() を削ってみましたが、見事に最初の Invoke() で target が初期化されていないままなので、メソッド呼び出しを実行することができませんでした。

最終的に

InitializeServerObject() の呼び出しを加えて、

IConstructionCallMessage ctor = req as IConstructionCallMessage;
if (ctor != null)
{
  this.InitializeServerObject(ctor);
  return EnterpriseServicesHelper.CreateConstructionReturnMessage(
            ctor, this.GetTransparentProxy() as MarshalByRefObject);
}

とすることで、Form1.Increment() において、RemotingServices.IsTransparentProxy(this) == true, RemotingServices.GetRealProxy(this) == typeof(MethodLogger) となりますし、new Form1() の戻り値も同様に MethodLogger の proxy です。
しかし、new の結果に対してメソッド呼び出しを行うと、きっちり Invoke() に処理がきますが、Increment() 内で proxy.Add() が呼び出されるにもかかわらず、Invoke() に制御がやってこなし、this を外に取り出してメソッドを呼び出しても Invoke() には到達しません。


といったところで、調査は進めておりません。
事前に完成したものにならないと予告した通り、このまま未完成で終了です。