この特集のトップページへ
Chapter 2:COMアーキテクチャの概要

見出し 2.2.2 インタフェースの仕組み
 COMコンポーネントは,自身を利用するクライアントアプリケーションに対して,「インタフェース」と呼ばれる窓口を公開している。インタフェースにはメソッドやプロパティが定義されており,クライアントアプリケーションはインタフェースを通じてのみ,それらのメソッドやプロパティ(Get/Putプロパティ名のメソッドによる)を呼び出すことができる。

 では,インタフェースとはいったいどのようなものなのであろうか。Fig.2-2に,その概念図を示す。

Fig.2-2 インタフェース=仮想関数テーブル(VFTable)+仮想関数テーブルへのポインタ(pVFTable)
fig.2-2

 詳しくはすぐあとで説明するが,COMには,必ずIUnknownという特別なインタフェースを定義しなければならない。IUnknownインタフェースを定義すると,仮想関数テーブル(VFTable:仮想関数の実装を指し示すポインタの配列)と仮想関数テーブルへのポインタが生成される。この2つが,COMにおけるインタフェースの実体である。

●IUnknownインタフェースとは

 COMのあらゆるインタフェースはIUnknownインタフェースを継承して実装することになっているため,仮想関数テーブルにはIUnknownインタフェースに実装されている3つのメソッド(QueryInterface,AddRef,Release)へのポインタが必ず存在する。COMコンポーネントにアクセスするときは,仮想関数テーブルに格納されたポインタ配列と,それを指し示す仮想関数テーブルのポインタ(Fig.2-2では,QueryInterface,AddRef,Releaseという各メソッドへのポインタ)を介して,COMコンポーネントのメソッドを呼び出すことになる。つまり,どのようなインタフェースであっても,継承元であるIUnknownインタフェースを通じて同じ手続きで呼び出すことにより,等しくアクセスできるのである。

 以上で述べた点は,COMの根幹にかかわる部分であるため,くり返しになる部分もあるが,もう少し具体的に説明しておこう。今度はFig.2-3を参照していただきたい。この図は,これから作成する辞書検索用COMコンポーネントの概念図である。

Fig.2-3 辞書検索用COMコンポーネントの概念図(インタフェースとクラスの関係)
fig.2-3

 この図を見るとわかるように,辞書検索用COMコンポーネントにはインタフェースとクラスがそれぞれ2つ存在する。2つのインタフェースとは,IUnknownインタフェースとIDictionaryインタフェースである。このうち前者は,COMで必ず定義しなければならない必須インタフェースであり,後者は,IUnknownインタフェースを継承して独自に定義したカスタムインタフェースである。ユーザーは,COMコンポーネントに実装したいメソッドを,カスタムインタフェースに実装することになる。具体的には,カスタムインタフェースを継承するカスタムクラス――ここでいうところのCDictionaryクラス――を定義する。つまり,IUnknownインタフェースを継承したIDictionaryインタフェースを実装しているのが,CDictionaryクラスというCOMクラスなのである。これにより,IUnknownインタフェースには変更を加えることなく,IUnknownインタフェースを継承し,独自のメソッドをもつカスタムインタフェースを定義できる(インタフェースを継承できる)。

 以上の説明で,IUnknownインタフェースがCOMコンポーネントの根幹を担うインタフェースであることは理解できたと思う。だが,肝心のIUnknownインタフェースそのものは,いかにも不明瞭な存在である。そこで,IUnknownインタフェースに対する理解を深めるため,このインタフェースに宣言される3つのメソッドについて概説しておく。

 すでに述べたように,IUnknownインタフェースには,QueryInterface,AddRef,Releaseという3つのメソッドが宣言されている。

○QueryInterfaceメソッド
 指定したCOMコンポーネントに使用したいインタフェースが存在しているかどうかを問い合わせるメソッドである。該当するインタフェースが存在していれば,指定されたインタフェースへのポインタを返す。
 QueryInterfaceメソッドの実装については,Fig.2-4を参照することで理解を深めることができるだろう。IUnknownインタフェースで宣言されたQueryInterfaceメソッドは,それを継承したIDictionaryインタフェースで宣言され,その実装はCDictionaryクラスに対して行われる。いったんCDictionaryオブジェクトが生成されると,その派生元であるIDictionaryインタフェースへのポインタをQueryInterfaceメソッドが返すことになるのである。

○AddRefメソッド,Releaseメソッド
 本来,COMコンポーネントは,リモートコントロールのようにユーザーによって制御されるものである。したがって,コンポーネント側にはオブジェクトのライフタイムを管理する仕組みが必要になる。
 COMでオブジェクトのライフタイム管理に利用されているのが,この2つのメソッドである。COMコンポーネントからCOMオブジェクトを生成するときにはAddRefメソッドを呼び出し,参照カウントをインクリメントする。逆に,COMオブジェクトが不要になったときには,参照カウントをデクリメントする。こうすることで,参照カウントが0でなければ,COMオブジェクトが生成されており,しかも参照されていることがわかる。参照カウントが0になった時点で,そのCOMオブジェクトは破棄することができる。COMは,このような仕組みによって,COMオブジェクトのライフタイムを一貫して管理している。

Fig.2-4 IUnknownインタフェースとIDictionaryインタフェース,CDictionaryクラスの関係
fig.2-4

●IUnknownインタフェースの定義

 では引き続き,IUnknownインタフェースの定義を具体的に見てみよう。

 Fig.2-2のようなインタフェースを定義するには,通常のクラスではなく,純粋抽象基本クラス(pure abstract class)を使用する。純粋抽象クラスを指定するには,クラスのメンバ関数として純粋仮想関数(pure virtual function)を宣言すればよい。

 純粋抽象基本クラスを定義する方法は,通常のクラスを定義する場合と変わらない。ただし,メンバ関数を純粋仮想関数として宣言する必要があるので,関数名のまえに“virtual”という関数指定子を記述し,関数名のあとに初期化指定子“= 0”を追加する必要がある。

 たとえば,IUnknownインタフェースにQueryInterfaceというメソッドを宣言する場合は,次のように記述する。

   virtual HRESULT __stdcall 
      QueryInterface(REFIID riid, void** ppvObj) = 0;

 QueryInterfaceメソッドと同様に,IUnknownインタフェースにAddRefメソッドとReleaseメソッドを宣言すると,次のようになる。

   virtual ULONG __stdcall AddRef() = 0;
   virtual ULONG __stdcall Release() = 0;

 では,確認を含めて,IUnknownインタフェースの定義をList 2-4に示す。なお,純粋仮想関数に追加したHRESULTと__stdcallについては,「COLUMN __stdcall」で解説する。

 では,Visual C++を起動し,IUnknownインタフェースを定義するために新規プロジェクトを生成してみよう。プロジェクトのスタイルとしては,[Win32 Console Application]を指定する。プロジェクト名には“CDictionaryCOM”という名前を設定することにする。

Fig.2-5 新規プロジェクトの作成
fig.2-5
Fig.2-6 ["Hello,World!"アプリケーション(W)]の指定
fig.2-6

 プロジェクトの生成が終了したら,ClassViewでプロジェクトのCDictionaryCOM.cppファイルを開き,List 2-4で示したとおり,IUnknownインタフェースを定義する(Fig.2-7)。

Fig.2-7 CDictionaryCOM.cppファイルにIUnknownインタフェースを定義(画像をクリックすると拡大可能)
fig.2-7

 このとき,QueryInterfaceメソッドの引数に指定するREFIIDやHRESULT,ULONGなどのマクロも定義しておく必要がある。ここでは,それらのマクロのなかから,通常のアプリケーションをプログラミングするときにはあまり利用することがないREFIIDというマクロについて見てみることにする。REFIIDとは「インタフェースIDの参照」という意味であり,具体的にはインタフェースのアドレスを返すという定義である。実際には,次のようなマクロを追加する。

   #define REFIID        const IID &

 IID――つまりインタフェースID――は,typedefを使用し,“typedef GUID IID;”と定義されている。この定義からもわかるように,インタフェースIDにはGUID(Globally Unique Identifiers)が利用されている。GUIDは128ビットの数値列であり,ネットワークアダプタのMACアドレス(ネットワーク機器メーカーがネットワークアダプタに割り当てた12バイトの数値。世界中で唯一無二であることが保証されている)や構築時刻などに基づいて生成されるため,生成させるたびに必ず世界で唯一の値となる。COM/COM+は,COMコンポーネントのインタフェースを識別するためにGUIDを使用するので,どのような環境下でもインタフェースを識別できる。

   typedef struct _GUID
   {
       unsigned long Data1;
       unsigned short Data2;
       unsigned short Data3;
       unsigned char Data4[8];
   } GUID;

 ちなみに,ここで定義したIUnknownインタフェースのインタフェースIDは,Unknwn.hファイルに次のように定義されている。

   // IID_IUnknown {00000000-0000-0000-C000-000000000046}
   static const IID IID_IUnknown =
       { 0x00000000, 0x0000, 0x0000,
         { 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46 } };

 ユーザーがインタフェースを実装するために任意のインタフェースIDを必要としたときには,Visual StudioやVisual C++のCommon\ToolsフォルダやWin32 SDKなどに収録されているGUIDGEN.EXEというプログラムを利用すればよい。このプログラムを実行して必要なGUIDの書式を選択したあと,[New GUID]ボタンを押す。このボタンを押すたびに,世界で唯一無二のGUIDが生成される(Fig.2-8)。必要であれば,生成されたGUIDをコピーして必要なファイルに貼り付けることもできる。

Fig.2-8 GUIDGEN.EXE
fig.2-8

 インタフェースIDを実装したら,インタフェースIDやGUIDを使用するときに必要となるマクロ(List 2-5の3〜23行目)を追加する。このマクロを追加することにより,プロジェクトをビルドできるようになる。ここまでの実装は,CDictionaryCOMのプロジェクトに保管する。

Fig.2-9 IUnknownインタフェース
fig.2-9

●カスタムインタフェースの定義

 IUnknownインタフェースを定義してビルドに成功したら,今度はList 2-6のようにIUnknownインタフェースを継承したカスタムインタフェース(IDictionaryインタフェース)を追加してみる。

 IDictionaryインタフェースには,英和辞書を検索するためのLookupWordメソッドを宣言する。このメソッドは“virtual”と“= 0”を指定し,純粋仮想関数として宣言する。これにより,IDictionaryクラスがインタフェースとして機能するようになり,メソッドはIDictionaryインタフェースを継承したクラスで実装可能となる。また,純粋仮想関数を宣言した抽象クラスのメソッドは,本クラスを継承したクラスでなければ実装することはできない。

Fig.2-10 IDictionaryインタフェース
fig.2-10
prev Chapter 2 5/9 next