株式会社エス・スリー・フォー

COMからのイベントを捕まえる方法

"見てくれ"を持たないCOMからのイベントの受理

Visual C++が提供してくれるATL(Active Template Library)を使うと、COM(Component Object Model)を簡単に作ることができます。

COMに発生した状態の変化をイベントとしてCOM利用者(Client)に通知したいことがあるでしょう。 この機能が提供されていないと、COMの状態変化を知りたいClientはCOMに対してメソッドもしくはプロパティを定期的にコール/取得しなければなりません。

Clientに対してイベントを発行する機能を持ったCOMを作るには、dispinterface(ディスパッチ・インタフェース)と呼ばれる接続ポイントを用意します。接続ポイントには、COMが発行できるイベントを定義しておきます。このイベントは、実際にはイベント毎にユニークなIDとして表されます。

一方、COMからのイベントを受理するClientは、COMが用意した接続ポイントを実装し、COMが送出したIDに対応するハンドラを実装します。

COMはイベントの発行時に、ID、そしてイベントに付随するパラメータ(引数)をClientに引き渡すことができますが、このパラメータは型およびパラメータの数に自由度を与えるため、"任意の型"を表現できる型VARIANTの配列としてClientに引き渡されます。
Clientは引き渡されたVARIANT配列の一つ一つの要素をID(イベントの種類)に応じて、COMがイベントの発行時に引き渡した"本来の型"に戻さなくてはなりません。

ActiveXコントロールとしてCOMを作れば Visual BasicやVisual C++などのActiveXコントロール・コンテナを作れる開発環境下でActiveXコントロールをフォームやダイアログに貼り付ければ、COMから発せられるイベントのハンドリング、すなわちイベントIDとパラメータの解釈を自動的に行なってくれます。

ところがActiveXコントロールではないCOM、すなわち"見てくれ"を伴わないCOMの場合、イベントの受理に関するコードはプログラマが書かねばなりません。

NTTデータの中野さんが、ATLが提供するクラスIDispEventImplを使って、COMイベントをエレガントに受理する方法を見つけてくれました。
ビジネスロジックなどのように"見てくれ"を持たないCOMを利用するときのイベントの受理がとても簡単になるテクニックです。

お試しCOMを作る

まず、イベントを発行し、見てくれを持たないCOMをさくさくっと作っておきます。

Visual C++の"ATL COM AppWizard"で"CounterServer"なるDLLを作ります。

COM  event figure #1

COM event figure #2

この段階ではまだCOMのいれものができただけです。
ATLオブジェクトを新規作成します。
作るのはCounter。"+1する"/"-1する"/"値を返す"の3つの機能をサポートします。

COM event figure #3

COM event figure #4

COM event figure #5

"オブジェクトウィザードのプロパティ"で"コネクションポイントのサポート"をチェックしておくのをお忘れなく。

ICounterにメソッドincr/decr、そしてプロパティvalueを追加します。

COM event figure #6

COM event figure #7

COM event figure #8

イベントはCounterの保持する値に変化が生じたときに、"changed"を発行させましょう。
_ICounterEventsにメソッドchangedを追加します。

そのとき、CounterのインタフェースICounterのポインタをパラメータとして渡し、Client側でイベントの発生したCOMを取得できるようにします。

COM event #9

インタフェースICounterの実装であるCCounterにイベントの接続ポイント_ICounterEventsをサポートさせます。

COM event figure #10

CCounterにはカウンタ値を保持するメンバ変数long value_を追加します。

COM event figure #11

ここまでの作業によって、すべての準備は整いました。

COM event figure #12

ではCCounterの実装に取り掛かりましょう。といっても実に単純な実装です。コンストラクト時にvalue_を0にし、incr()で+1、decr()で-1、get_value()でvalue_を返すだけです。incr()/decr()中でイベントを発行します。Wizardが作ってくれたFire_changed()を呼び出せばイベントの送出は完了です。

class ATL_NO_VTABLE CCounter : 
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CCounter, &CLSID_Counter>,
  public IConnectionPointContainerImpl<CCounter>,
  public IDispatchImpl<ICounter, &IID_ICounter, &LIBID_COUNTERSERVERLib>,
  public CProxy_ICounterEvents< CCounter > {
public:
  CCounter();

DECLARE_REGISTRY_RESOURCEID(IDR_COUNTER)

DECLARE_PROTECT_FINAL_CONSTRUCT()

BEGIN_COM_MAP(CCounter)
  COM_INTERFACE_ENTRY(ICounter)
  COM_INTERFACE_ENTRY(IDispatch)
  COM_INTERFACE_ENTRY(IConnectionPointContainer)
  COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer)
END_COM_MAP()
BEGIN_CONNECTION_POINT_MAP(CCounter)
CONNECTION_POINT_ENTRY(DIID__ICounterEvents)
END_CONNECTION_POINT_MAP()

public:
  STDMETHOD(get_value)(/*[out, retval]*/ long *pVal);
  STDMETHOD(decr)();
  STDMETHOD(incr)();
private:
  long value_;
};

// ---------- implementaion ----------

CCounter::CCounter() {
  value_ = 0;
}

STDMETHODIMP CCounter::incr() {
  ++value_;
  Fire_changed(this);
  return S_OK;
}

STDMETHODIMP CCounter::decr() {
  --value_;
  Fire_changed(this);
  return S_OK;
}

STDMETHODIMP CCounter::get_value(long *pVal) {
  *pVal = value_;
  return S_OK;
}

イベントを捕まえる

ここからが本題です。こうやって作られたCOMを動かすのに難しいことは何もありません。
COMの初期化や後始末を取り除けば、クラスIDからインスタンスを生成し、メソッドを呼び出すだけです。

int main() {
  CComPtr<ICounter> counter;
  counter.CoCreateInstance(CLSID_Counter);
 
  int i;
  for (i = 0; i < 10; i++) 
    counter->incr();
  for (i = 0; i < 10; i++) 
    counter->decr();

  return 0;
}

counterをince()/decr()するたびにcounterの中ではイベントを発行するのですが、
現時点ではそのイベントの受付窓口を用意していないので、ただ単にカウンタ値が増減しているだけです。

では、このCOM-ClientにCounterからのイベントを受理させましょう。

Counterはイベント発生時に、イベント通知のための接続ポイント_ICounterEventsに宣言されたメソッドを呼び出します。Clientにはインタフェース_ICounterEventsを実装したインスタンスを用意し、Counterに接続、つまり"何か起こったら僕にしらせてね"とお願いしておけばいいのです。が、これがけっこうややこしいんですね。

Counterがイベントを発行するメソッドFire_changed()の実装を見てみましょう:

template <class T>
class CProxy_ICounterEvents 
  : public IConnectionPointImpl<T, &DIID__ICounterEvents, CComDynamicUnkArray>
{
public:
  VOID Fire_changed(ICounter * source) {
    T* pT = static_cast<T*>(this);
    int nConnectionIndex;
    CComVariant* pvars = new CComVariant[1];
    int nConnections = m_vec.GetSize();
    for (nConnectionIndex = 0; nConnectionIndex < nConnections; nConnectionIndex++) {
      pT->Lock();
      CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex);
      pT->Unlock();
      IDispatch* pDispatch = reinterpret_cast<IDispatch*>(sp.p);
      if (pDispatch != NULL) {
        pvars[0] = source;
        DISPPARAMS disp = { pvars, NULL, 1, 0 };
        pDispatch->Invoke(0x1, IID_NULL, LOCALE_USER_DEFAULT, 
                         DISPATCH_METHOD, &disp, NULL, NULL, NULL);
      }
    }
    delete[] pvars;
  }
};

ようするにFire_changed()は接続されたハンドラそれぞれに対し、関数Invoke()を呼んでいます。
したがってClient側に用意するインスタンスには関数Invoke()をこさえておけば、イベント発生時にはそいつが呼ばれてくれることでしょう。

Client側に用意した関数Invoke()の中ではイベントのIDを識別し、さらにイベントに応じて第5引数(VARIANT型の集合)を解釈しなければならないのです。
ここでATLが提供するIDispEventImplを使えば、イベントの解釈がぐっと楽になるわけです。

イベント受理クラスCounterEventsはCComObjectRootExとIDispEventImplから導出します:

#define SINKID_COUNTEREVENTS 0

class ATL_NO_VTABLE CounterEvents :
  public CComObjectRootEx<CComSingleThreadModel>,
  public IDispEventImpl<SINKID_COUNTEREVENTS, CounterEvents, 
                              &DIID__ICounterEvents, &LIBID_COUNTERSERVERLib, 1, 0>
{
public:
  CounterEvents() {}

そして、CounterEventsはインタフェース_ICounterEventsを実装していることを宣言します。

BEGIN_COM_MAP(CounterEvents)
  COM_INTERFACE_ENTRY_IID(DIID__ICounterEvents, CounterEvents)
END_COM_MAP()

次に、こいつに対してイベントID=1なるイベントを受理したら、メソッドhandle_changed()を呼び出してくれるよう仕組みます。

BEGIN_SINK_MAP(CounterEvents)
  SINK_ENTRY_EX(SINKID_COUNTEREVENTS, DIID__ICounterEvents,
                        1, handle_changed)
END_SINK_MAP()

handle_changed()は以下のように実装しました。

  virtual void changed(ICounter* pCounter) =0;

  HRESULT _stdcall handle_changed(ICounter* pCounter) {
    changed(pCounter);
    return S_OK;
  }
};

CcounterEventsの派生クラスでchanged()を再定義します。

class DerivedEvents : public CounterEvents {
  virtual void changed(ICounter* pCounter) {
    long lValue;
    pCounter->get_value(&lValue);
    std::cout << "update to " << lValue << std::endl;
  }
};

最終的に出来上がったClientはこうなります。

#include "stdafx.h"
#include <iostream>
#import "CounterServer/CounterServer.tlb" no_namespace, named_guids

CComModule _Module;

/*  CComInitializer : COM初期化クラス
 */
class CComInitializer {
  bool initialized_;

public:
  CComInitializer() : initialized_(false) { }
  HRESULT init() {
    HRESULT hr;
    hr = ::CoInitialize(NULL);
    if ( initialized_ = SUCCEEDED(hr) )
      _Module.Init(NULL, ::GetModuleHandle(NULL));
    return hr;
  }

  ‾CComInitializer() {
    if ( initialized_ ) {
      _Module.Term();
      ::CoUninitialize();
    }
  }
};

/* com_assert : エラーチェックと例外送出
 */
void com_assert(HRESULT result, const std::string& msg) {
  if ( FAILED(result) )
    throw std::runtime_error(msg);
}

#define SINKID_COUNTEREVENTS 0
#define MY_SINK_ENTRY(id,fn) ¥
  SINK_ENTRY_EX(SINKID_COUNTEREVENTS, DIID__ICounterEvents, id, fn)

/* CounterEvents : シンク(イベント受付窓口)クラス
 */
class ATL_NO_VTABLE CounterEvents :
  public CComObjectRootEx<CComSingleThreadModel>,
  public IDispEventImpl<SINKID_COUNTEREVENTS, CounterEvents, 
                        &DIID__ICounterEvents, &LIBID_COUNTERSERVERLib, 1, 0>
{
public:
  CounterEvents() {}

BEGIN_COM_MAP(CounterEvents)
  COM_INTERFACE_ENTRY_IID(DIID__ICounterEvents, CounterEvents)
END_COM_MAP()

BEGIN_SINK_MAP(CounterEvents)
//  MY_SINK_ENTRY(1, handle_changed)
  SINK_ENTRY_EX(SINKID_COUNTEREVENTS, DIID__ICounterEvents, 1, handle_changed)
END_SINK_MAP()

  virtual void changed(ICounter* pCounter) =0;

  HRESULT _stdcall handle_changed(ICounter* pCounter) {
    changed(pCounter);
    return S_OK;
  }
};

#undef MY_SINK_ENTRY

/* DerivedEvents : イベントハンドラ
 */
class DerivedEvents : public CounterEvents {
  void changed(ICounter* pCounter) {
    HRESULT hr;
    long lValue = 0;
    hr = pCounter->get_value(&lValue);
    com_assert(hr,"get_value failed in changed event handler");
    std::cout << "update to " << lValue << std::endl;
  }
};

/* ecom_event_sink : シンクラッパー
 */
template<class Event>
class com_event_sink {
  typedef CComObject<Event> com_object_type;
  com_object_type*  sink_;
  CComPtr<IUnknown> unk_;
public:
  HRESULT create() {
    HRESULT hr = com_object_type::CreateInstance(&sink_);
    if ( SUCCEEDED(hr) ) 
      sink_->QueryInterface(IID_IUnknown,(void**)&unk_);
    return hr;
  }
  com_object_type* operator->() {
    return sink_;
  }
};

/* main : Let's try!
 */
int main() {
  try {
    HRESULT hr;
    CComInitializer com;
    hr = com.init();
    com_assert(hr, "CoInitialize failed");

    {
          // カウンタの生成
      CComPtr<ICounter> counter;
      hr = counter.CoCreateInstance(CLSID_Counter);
      com_assert(hr, "CCI ControlCounter failed");

      // イベントハンドラの生成
      com_event_sink<DerivedEvents> sink;
      hr = sink.create();
      com_assert(hr,"CCI Event sink failed");

      // 接続
      hr = sink->DispEventAdvise(counter);
      com_assert(hr, "DispEventAdvise failed");

      // メソッド呼び出し
      int i;
      for (i = 0; i < 10; i++) counter->incr();
      for (i = 0; i < 10; i++) counter->decr();

      // 解放
      hr = sink->DispEventUnadvise(counter);
      com_assert(hr, "DispEventUnadvise failed");
    }
  } catch ( std::exception& ex ) {
    std::cout << ex.what() << std::endl;
    return -1;
  }
  return 0;
}

source archive