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

COMサーバーにJScriptのイベントハンドラを登録する の「修正版」です。

前回の例には、致命的なエラーが起こる場合があります。onCurrent というプロパティーに関数オブジェクトを設定し、取得することができます。しかし、まだonCurrentプロパティに関数オブジェクトを設定しない、未設定の場合、onCurrent を参照すると、スクリプト側にエラー値を戻すとので、エラーがスローされ実行が止まってしまいます。

つまり、

var obj = new ActiveXObject("MyServer.Utility");
WScript.Echo(obj.onCurrent);

とすると、エラーで止まってしまいます。まだプロパティが未設定ならば、nullを返すようにすれば万事OKです(^^;

というわけで、修正です。

まず、onCurrentのプロパティの設定と取得でのデータ受け渡しを、IDispatch* ではなく、VARIANT にします。関数オブジェクトは、このVARIANTで、vtメンバをVT_DISPATCHにして受け渡すことになります。

ところどころ修正したものが次のとおり。

/*
 compiler command line :
   cl /LD MyServer.cpp /link /TLBOUT:MyServer.tlb
*/

#pragma comment(lib,"comsuppw.lib")

#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;

#include <comutil.h>

//マクロ定義
#define PPVOID(X) (reinterpret_cast<LPVOID*>(X))
#define QI(X,Y,Z) ((X)->QueryInterface((Y),(reinterpret_cast<LPVOID*>(Z))))
#define RELEASE(X) {if(X){(X)->Release(); X=NULL;}}

// 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] VARIANTARG var);
  [id(2),propget] HRESULT onCurrent([out,retval] VARIANTARG *pvar);

};

/////////////////////////////////////////////////////////////////////////////
// イベントインターフェイス定義 : 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()
     {

     }

  __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)
    {
      TCHAR pBuffer[MAX_PATH+1] = {0};
      ::GetCurrentDirectory(MAX_PATH+1,pBuffer);

      *sPath = _bstr_t(pBuffer).Detach();

      Fire();
      return S_OK;
    }

  // スクリプトから渡されるオブジェクトの設定と取得
  HRESULT put_onCurrent(VARIANTARG var)
    {
      HRESULT hRes = E_INVALIDARG;
      LPDISPATCH pDispatchNew = NULL;

      if(var.vt == VT_DISPATCH)
        {
          if(SUCCEEDED(hRes = QI(var.pdispVal,IID_IDispatch,&pDispatchNew)))
            {
              /*if(m_pDispatch)
                m_pDispatch->Release();*/
              RELEASE(m_pDispatch);

              m_pDispatch = pDispatchNew;
            }
        }
      else
        hRes = E_INVALIDARG;

      return hRes;
    }

  //関数オブジェクトの取得
  HRESULT get_onCurrent(VARIANTARG *pvar)
    {
      HRESULT hRes = S_OK;
      IDispatch *pDisp = NULL;
      if(m_pDispatch)
        {
          if(SUCCEEDED(hRes = QI(m_pDispatch,IID_IDispatch,&pDisp)))
            {
              VariantClear(pvar);
              pvar->vt = VT_DISPATCH;
              pvar->pdispVal = pDisp;
            }
        }
      else
        {
          //もし保持している関数オブジェクトがなければ空に。
          VariantClear(pvar);
        }

      return hRes;
    }

private:

  LPDISPATCH m_pDispatch;

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

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

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

VARIANT型とIDispatchインターフェイスは、C++とJScriptとでデータを受け渡しする上で必ず付いてまわるのですが、あまりこのあたりを詳しく解説してある本を見つけることができないのがネックです。

この件の備忘録終了。

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);
}

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

VC++ 2008EE で COMサーバーをカンタンに作成

【追記】
ご承知のように、Visual Studio Commuity Edition がリリースされました。ライセンス的にやや難がありますが、商用/非商用アプリケーションの個人開発者にはVisual Studio の Professional 版相当のフルセットが無料で提供されるようになりましたね、バンザーイ!
ってことで、このページの内容は、意味をなさないので、とっととCommunity Editionをインストールしてラクをしてください。


備忘録のエントリです。

フリーの開発環境でラクにCOMサーバーを作ろう、という主旨です(^^;;;

キーワードは「C++属性」です。この属性を利用することで手作業であっても退屈なメンドクサイ、コードの大部分(ほとんど)が自動的に作成されます。コーディングするのは、実装するインターフェイスのメソッドの中身だけ。簡単です。ただ・・・ATLだけはどうしても必要です。

しかし・・・Visual Studio 2008 Express EditionにはATLは付属していません。

ATLのヘッダーファイルとライブラリファイルをどこぞから調達しなくてはいけませんが、幸いなことにマイクロソフトがこれまた無料で配布しているWDK(Windows Driver Kit)をインストールすることにより利用することが可能です。WDKはかつては DDK(Driver Developper Kit)と呼ばれていたモノです。WDKにはVC++2008と同じコンパイラ・リンカなどの開発ツール、ATL/MFCやWindowsでのカーネルモードドライバを作成するために必要なすべてのファイル・ドキュメントが同梱されています。

無料で手に入れることができるのですが・・・一点、不安なことがあります。

それは・・・Visual Studio 2008 Express Editionと、WDK内の構成ファイル(ATLヘッダーファイルとライブラリファイル)を混ぜてコードをビルドするのがライセンス的に良いのか駄目なのか・・・等は、調査不足で分かりません。Visual Studio 2008 Express Editionで作成したアプリケーションは商用・非営利を問わず配布できたかと思いますが、WDKはそのあたり分かりません。まぁ非営利・個人的な利用なら問題はないかと思いますが・・・。

(WDKのダウンロードにはWindows Liveやマイクロソフト・パスポートのアカウントが必要です)

MSのダウンロードセンターからダウンロードできるようになってました。( 2012/1/11)

で、話を本題に戻します。 このWDKをインストール後、VC++ 2008 Express Editionのディレクトリ設定にこれらのパスを追加登録してあげればATLを利用したアプリケーション開発が可能となります。ただし、ATL関連ファイルがあるというだけで、便利なプロジェクト・ウィザードはないので全部手作業です(^^;;; やっぱりラクはできません(笑)。

さて、COMサーバーの実際の手順なんですが・・・これは日本語ドキュメントが既にあります(^^ MSDNライブラリの「COM属性によるCOM DLLの開発 -> チュートリアル : テキスト エディタを使った COM サーバーの作成」です。

サンプルがありますのでこれを自分用に修正していけばいいだけ。僕は以下のようにMyServerというCOMオブジェクトをテストとして作成することにしました。見れば分かりますが・・・[ ] でくくっているところが属性と呼ばれるマイクロソフトの独自の拡張です。かなり違和感のあるコーディングです。が、これをコンパイルすることで必要なファイル、足りないコードを自動生成してくれます。理屈は分かりません(笑)  ある程度理解するにはやはりCOM/ATLの知識が必要です。魔法(ブラックボックス)として扱うのがよさそうです。

IMyServerインターフェイスを定義して、CMyServerクラスで実装。インターフェイスメソッド、CurrentDirectory()を定義。これは現在のカレントディレクトリを文字列で返すメソッドをサンプルとして定義しました。必要に応じてインターフェイスやメソッドを増やしたい場合もチョチョッと手を加えるだけで済みます。

んで、これだけのコードで完璧なオートメーションサーバーができてしまいます。属性の詳細は・・・あまり知る必要はないかと。詳細はかなり難解です。何度も書きますが、理解するにはCOMとかATLに関する中級~上級レベルの知識が必須かと。ただ公開するものの実装さえしてやればいいので、あとのCOMに関連するおきまりのコードはコンパイラとリンカが勝手にやってくれますのでかなりラクです。従来ならIDLファイルを書いて、IDispatch,IUnknowを実装して、レジストリへの登録処理を書いて・・・などが必要でしたが・・・。

このファイルをたとえば  cl /LD MyServer.cpp /link /TLBOUT:MyServer.tlb  みたいにコンパイルしてやればインプロセスサーバー(DLL)、タイプライブラリが生成されます。

あとはRegsvr32.exe でDLLファイルを登録し、以下のようなテストコードで動作確認。

パフォーマンスにシビアなコードはC++で実装して、あとはC#やJScriptでお手軽に・・・というような用途にいいかもです。

簡易メモリアロケータ

最近面白そうな機能満載の携帯がいっぱい出てきて、意味無く携帯を新しくしたいです。どっちかっていうと、時間潰しができて遊べる携帯(^^;;; 早くDOCOMOからiPhone出てくれないかな~。坂本龍一がCMやってたSamsungも良いなぁ。面白そうなやつは全部ソフトバンクなんだなぁ~。DOCOMOももっと変わったもん出して欲しいなぁ。

そんなことはさておき、

ローカルスコープ内で、使い捨てみたいな使い方をするような小さなメモリを確保するとき、たとえば、Win32APIでなんらかの文字列を受け取るときのバッファとか、いちいちnewでメモリ確保して、使い終わったらdeleteして・・・が面倒くさい。特にC#を覚えてnewしっぱなしでdeleteする必要がない言語に慣れきってしまうとC++は非常にやりづらい。

と、かなり前に思って、以前ずっとかなり前にWin32APIの勉強のため書いていた簡易メモリアロケータを引っ張り出してきて、ちょこっと手直し。

正直、メモリアロケータなんて大層なもんじゃなくて、コンストラクタでnewしてメモリを確保して、スコープから抜けるとデストラクタでdeleteする、という超簡単なもん。

手直ししたのは、コンストラクタとデストラクタでメモリの確保と解放する際に、newとdelete演算子を使わず、Windowsの Heap系のAPIで置き換えたことと、& 演算子をオーバーロードして、内部バッファのポインタを返すようにしたことの2点。Advanced Windows のメモリ管理を参考に参照カウンタも付けてみました(^^;

ほとんどサンプルと同じになっちゃった(^^;

具体的には・・・

CAllocator alloc(MAX_PATH); //MAX_PATHバイトのメモリ確保
CAllocator allocs[5] = {100,100,100,100,100}; //配列とか、
CAllocator alloc; alloc->Alloc(100); //などでメモリ確保
CAllocator alloc; alloc.Alloc(100); //などでメモリ確保

メモリブロックのポインタを得るには、*演算子。

LPSTR lpStr = reinterpret_cast<LPSTR>(*alloc);

スコープを抜けるとデストラクタがコールされて、メモリが解放。参照数がゼロになると、プライベートヒープハンドルも解放。

修正すべき欠点は、スレッドセーフじゃない。複数スレッドで使用すると参照カウンタとかで競合が起きるし。大きなメモリ(たとえば1MBとか)を確保したいときとかは、VirtualAlloc系を使うべき、らしい。
まぁ・・・スレッドをおこすようなプログラムは複雑になればなるほどC#の方が安全で簡単なんで。

追記) &演算子で確保したメモリのポインタをリターンしてますが、スコープを抜けると問答無用でメモリ解放してしまうので、気をつけないといけません。CAllocatorを動的に確保すればいいんでしょうけど、それだと意味がなくなる(^^;

これをベースに実用的に使えるまで改良していきたいと思います。


8月30日、ところどころ修正しました。

シェルエクステンション

以前に作ったクリップボード関連のツールをシェルエクステンションで実装しよう、と思って長い間放置していたんですが・・・昼休みと、昨日、今日帰ってから寝るまでの時間で突貫工事で仕上げました(^^ ついでにテキストファイルの内容をコピーする機能も追加。なかなか重宝してます(^^

CF_TEXT2File

CF_TEXT2File

処理の中心となるクリップボードの処理は既に作ってあったので、後はシェルエクステンションのコンテキストメニューハンドラを実装するだけ・・・すぐできると思ってましたが・・・デバッグが思うようにできなくて時間がかかってしまいました。一度エクスプローラ上で右クリックしてコンテキストメニューを出すと、そのエクスプローラ・プロセスを終了させないとシェルエクステンション本体(DLL)を上書きできないので非常に面倒。しかも・・・不用意にデスクトップを右クリックしてしまうと・・・デスクトップ(エクスプローラ)の再起動がかかる・・・

で、やっと望む動作をして完成・・・したと同時に気付いたよ。

「Virtual PC 上のXPでデバッグすりゃいいじゃん!」

年取るのは、いやだねぇ(ーー;;;

また、暇があったら、CF_HDROP フォーマットにも対応させよう。っと。