続 NativeWindowクラス

今の仕事で対象にしているスマートクライアントではどうしても実現しなければならない機能がある。VisualBasic 2.0や3.0などを使用した16bitアプリケーションの時代を知っているプログラマには懐かしい話だと思うがその機能とはDDE(Dynamic Data Exchange)と呼ばれたアプリケーション間のプロセス間通信の機能だ。
DDE自体OLEの下位レイヤとして利用され、後にはCOM、DCOMにとって代わられた技術であるが他のアプリケーションを外部から制御、データをやりとりする機能は今でも広く使われておりその後の仕事で使用するGUIクライアントには必須の機能になったのである。

スマートクライアントで必要な(元DDEの)機能

  • 1 (通信相手の)サーバアプリケーションを起動して通信の確立を待つ
  • 2 サーバアプリケーションとの通信が確立したらデータの通信を行う

レガシーC/S(クライアント/サーバ)時代のVB2.0を使用したアプリケーションではVBX(だったかな?)が提供されていたのでそれを利用していた。その後32bitアプリケーションの時代になり開発にはDelphiを使っていたがDelphiも同様にDDEのコンポーネントがあったが1の機能のタイミングが微妙な部分があったのでDDEを使わずにマップドメモリとミューテクス(Mutex)を使用した独自のIPC(Inter-process Comunication)コンポーネントを開発して同様の機能を実現していた。
そして現在だ。C/Sの時代は終焉を告げWebベースのシステムが台頭する。C/Sで使われていたGUIクライアントはHTTP通信、自己更新、非接続型データベースアクセスなどの新しいフィーチャーを追加されて「リッチクライアント」「スマートクライアント」と呼ばれるようになった。そのスマートクライアントの開発には.NET Frameworkを使用しているのだが古のDDE機能は当然ながらそのまま残っているはずもないので別な手段で同様の機能を実現しなくてはならない。実現方法の候補に上がったのは次の2つ

  • .NET Remotingを使用する
  • NativeWindowクラスで捕捉したWM_COPYDATA Windowメッセージを通信プロトコルとして使用する

.NET Remotingを使用する案は言わずもがなだろう。NET Remotingは,オブジェクトがアプリケーション/ドメインの境界を越えて通信するためのフレームワークでありオブジェクト間通信の手段としてはDCOMの正当な後継技術だからだ。だが、今回は.NET Remotingは採用しなかった。採用したのはWM_COPYDATAを使用する方法。この名前を持つWindowメッセージは送り先のWindowハンドルさえ判明していればそれが同一プロセスだろうが異なるプロセスだろうがメモリブロックを指し示す正しいポインタを渡せるという特徴がある。(この方法は実際にWindowsが用意しているアプリケーションにも見られるので特段変わった方法ではない。)プロセス間で受渡しを行うデータをシリアライズ可能なオブジェクトに限定してやれば.NETのInterop機能を用いてメモリブロックからストリームを取得しデシリアライズしてオブジェクトを復元できるのだ。


WM_COPYDATAで使用されるCOPYDATASTRUCT構造体

[StructLayout(LayoutKind.Sequential)]
private struct COPYDATASTRUCT
{
    public IntPtr dwData;
    public int cbData;
    public IntPtr lpData;
}

NativeWindowクラスでWM_COPYDATAをインターセプトし、そのポインタからシリアライズ可能なオブジェクトを復元する
(WM_COPYDATAでオブジェクトを送信する側の処理は省略)

protected override void WndProc (ref System.Windows.Forms.Message m )
{
    if (m.Msg == WM_COPYDATA)
    {
        COPYDATASTRUCT cds = new COPYDATASTRUCT();
        cds = (COPYDATASTRUCT) Marshal.PtrToStructure(m.LParam, typeof(COPYDATASTRUCT));
        if (cds.cbData > 0)
        {
            byte[] data = new byte[cds.cbData];                
            Marshal.Copy(cds.lpData, data, 0, cds.cbData);
            MemoryStream stream = new MemoryStream(data);
            BinaryFormatter b = new BinaryFormatter(); //バイナリフォーマッタ使用
            object o = b.Deserialize(stream);
            
            デシリアライズしたオブジェクトを別な処理で使用 〜
        }
    }
}

現状は主に性能の面、リソース管理の面でこの方法がベストと考えているが前回の日記同様にWindowメッセージベースのプログラミングに依存しているという意味では不安が残るのも事実だ。