COMサーバーにJScriptのイベントハンドラを登録する

VC++ 2008EE で COMサーバーをカンタンに作成」のつづきです。

C++属性を使ってC++で簡単なCOMサーバーを作成はできました。次に、ブラウザ上でやってるような、イベントを発生させて、それをjscriptの関数、たとえば、

function onExec()
{
  WScript.Echo("メソッドが実行されました");
}

というようなイベントハンドラを登録して、COMサーバーのあるメソッドを実行したときに、このonExec という関数を実行させるようにしたい! と思います。

要は、HTML+Javascriptでやってるような、window.onload = function(){…} というようなことを自分が作成したCOMサーバーにやらせよう、というわけです。実行環境は、Windows Scripting Hostです。

はじめはどうやればさっぱり分かんなかったんですが、ネットで検索していろいろ調べた結果の備忘録となります。

COMサーバーから、各種イベント通知を受け取るには、C++だとIConnectionPointとシンクオブジェクトを経由したものになるんですが、JScriptにはそういった接続ポイントをゴニョゴニョして・・・というやり方はできないようです。つまりCOMでシンクを経由したイベントを受け取る機構は通用しないということです。

ここから補足 (2012/2/10)

正確には、動的にインスタンスを生成した(new ActiveXObject()とかで作成した)オブジェクトでシンクを経由したイベントを受け取る機構がないのです。たとえばHTAとかで

<object id=’xxxx’ classid=”clsid:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee”></object>

という風にOBJECTタグをHTMLに埋め込んで、function xxxx::関数名(引数){…} というイベントハンドラを定義すれば、可能になります。たぶん。

JScriptをホストしているのがWSHならば、以下のようにイベントハンドラを定義して、シンクを経由した通常のCOMイベントを受け取れます。

var obj = WScript.CreateObject("MyServer.Utility","myserver_");
function myserver_onXXXX()
{
   WScript.Echo("Fire!");
}
obj.XXXX();

しかし、Webブラウザ上で、たとえば・・・

var button = document.getElementById('button');
button.onclick = function()
{
  window.alert("クリック!") ;
}

というようにもしたいわけです。

var myserver = WScript.CreateObject('MyServer.Utility');
myserver.onExec = function()
{
  WScript.Echo("実行");
}

とこんな感じにもできたらなぁ・・・と思ったのが今回の動機です。

単純に考えれば、COMサーバー側にonExecというプロパティを設置して、COMサーバーのあるメソッドが実行されたら、このonExecを実行させる(イベントを発生)させればいいだけかな、と思いました。function(){}の関数オブジェクトの部分。関数オブジェクトはC++で、IDispatch*で受け取ればいいですね。

ということで、いろいろネットで調べた結果、次のような単純なCOMサーバーを作成しました。

  • CurrentDirectory() というメソッドを一つ定義。
  • 関数オブジェクトの保持用にプロパティを一つ定義
  • CurrentDirectory()がコールされたら、プロパティに格納している関数オブジェクトを実行する。
    (つまりイベントを発生させる)

具体的なコードは・・・

/*
 コンパイル・コマンドライン :
   cl /LD MyServer.cpp /link /TLBOUT:MyServer.tlb
*/

#define STRICT
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0400
#endif
#define _ATL_ATTRIBUTES
#define _ATL_APARTMENT_THREADED
#define _ATL_NO_AUTOMATIC_NAMESPACE
#include <atlbase.h>
#include <atlcom.h>
#include <atlwin.h>
#include <atltypes.h>
#include <atlctl.h>
#include <atlhost.h>
using namespace ATL;

//マクロ定義
#define PPVOID(X) (reinterpret_cast<LPVOID*>(X))
#define RELEASE(X) (X ? X->Release() : 0)

// DllMain,DllCanUnloadNow,DllRegisterServer and DllUnregisterServer
[ module(dll, name = "MyServer", helpstring = "MyServer 1.0 Type Library") ];
[ emitidl ];

/////////////////////////////////////////////////////////////////////////////
// インターフェイス定義  :   IMyServer 
[
   object,
   dual,
   oleautomation,
   helpstring("IMyServer Interface"),
   pointer_default(unique)
]
__interface IMyServer : IDispatch
{
  [id(1)] HRESULT CurrentDirectory([out,retval]BSTR *sPath);

  [id(2),propput] HRESULT onCurrent([in] LPDISPATCH pDisp);
  [id(2),propget] HRESULT onCurrent([out,retval] LPDISPATCH *ppDisp);
};

/////////////////////////////////////////////////////////////////////////////
// イベントインターフェイス定義 : IMyServerEvent
[
  dispinterface,
  hidden,
  helpstring("IMyServerEvents Interface")
]
__interface IMyServerEvent
{
  [id(1)] HRESULT onCurrent();
};

/////////////////////////////////////////////////////////////////////////////
// インターフェイス実装クラス  : CMyServer
[
   coclass,
   threading(apartment),
   source(IMyServerEvent),
   event_source(com),
   vi_progid("MyServer.Utility"),
   progid("MyServer.Utility.1"),
   version(1.0),
   helpstring("MyServer Class")
]
class ATL_NO_VTABLE CMyServer :
   public IMyServer
{
public:
   CMyServer()
     {

     }

  //COMイベントのためのおまじない。
  __event __interface IMyServerEvent;

  DECLARE_PROTECT_FINAL_CONSTRUCT()
  HRESULT FinalConstruct()
    {
      //プライベート・メンバ初期化
      m_pDispatch = NULL;
      return S_OK;
    }

  void FinalRelease()
    {
      //後処理
      RELEASE(m_pDispatch);
      return;
    }

  //単にカレントディレクトリを返す。
  HRESULT CurrentDirectory(BSTR *sPath)
    {
      WCHAR pBuffer[MAX_PATH+1] = {0};
      ::GetCurrentDirectoryW(MAX_PATH+1,pBuffer);

      *sPath = ::SysAllocString(pBuffer);

      Fire();
      return S_OK;
    }

  // IDispatch*(スクリプトから渡されるオブジェクト)の設定と取得
  // スクリプトでいう、obj.onCurrent = function(){...} の部分
  HRESULT put_onCurrent(LPDISPATCH pDispatch)
    {
      LPDISPATCH pDispatchNew = NULL;
      HRESULT hRes = S_OK;

      if(SUCCEEDED(hRes =
           pDispatch->QueryInterface(IID_IDispatch,PPVOID(&pDispatchNew))))
        {
          RELEASE(m_pDispatch);
          m_pDispatch = pDispatchNew;
        }

      return hRes;
    }

  //関数オブジェクトの取得
  HRESULT get_onCurrent(LPDISPATCH *ppDisp)
    {
      HRESULT hRes = E_NOTIMPL;

      if(m_pDispatch)
        hRes = m_pDispatch->QueryInterface(IID_IDispatch,PPVOID(ppDisp));

      return hRes;
    }

private:

  LPDISPATCH m_pDispatch;

  void Fire()
    {
      DISPPARAMS dispparams = {NULL,NULL,0,0};

      //COMイベント発火
      IMyServerEvent_onCurrent();

      //設定された関数オブジェクトを実行,デフォルトDISPID(0)
      m_pDispatch->Invoke((DISPID)DISPID_VALUE,
                          IID_NULL,
                          LOCALE_USER_DEFAULT,
                          DISPATCH_METHOD,
                          &dispparams,
                          NULL,
                          NULL,
                          NULL);
    }
};

一応、シンクオブジェクト経由でもイベントを受け取れるようにもしました。仕組みが分かれば意外と簡単です。以前にも書きましたが、ほとんどサンプルの流用で、ATLを使った部分や属性部分はサンプルそのまま。

ただ、このテストサンプルは複数のイベントハンドラを登録できません。複数のイベントハンドラを登録できるようにするには、複数のIDispatch*ポインタを管理しなければなりません。内部でstd::vectorなどのポインタ配列用のコンテナ(COMだったらsafe arrayあたり?)を用意して・・・というような処理が必要になってくるでしょう。

できたMyServer.dllを regsvr32 で登録して、以下のJScriptで動作確認。

//シンクを経由した、イベントハンドラ
//WSHホストで動作可能です。
function myserver_onCurrent()
{
  WScript.Echo("Sync Event Occured!");
}

try
{
  //IEがホストなら・・・
  // var obj = new ActiveXObject("MyServer.Utility");
  // WScript.Echo は window.alert になります。
  var obj = WScript.CreateObject("MyServer.Utility","myserver_");

  obj.onCurrent = function()
    {
      WScript.Echo("Fire Current!");
    }

  WScript.Echo(obj.CurrentDirectory());
}
catch(e)
{
	WScript.Echo(e.message);
}

というわけで、備忘録終了。