Chapter 5.
IUnknownとQueryInterface



引き続き、COMのメカニズムを解説していきます。が、今回は小話無しです。

前回のサンプルでは、一つのコンポーネントにアクセスするインターフェースを別々の関数で取得し、 更に開放する関数も別々に用意してありました。が、これはChapter.3で述べられたCOMの利点と大きく食い違います。
それを解決するために、COMが用意したメカニズムが、この章のタイトルでもあるIUnknownです。 IUnknownは、全てのコンポーネントに実装が義務付けられたインターフェースです。これはCOMの仕様部分と言ったところでしょう。 これによって前回の問題がどのように解決されるかは、サンプルを見ながら解説していきましょう。



interface.h
インターフェースを定義しています。徐々に完全なCOMの実装に近づいて行きます。
// IChap5_1インターフェース
interface IChap5_1 : public IUnknown
{
    virtual void FuncA(void) = 0;
};

// IChap5_2インターフェース
interface IChap5_2 : public IUnknown
{
    virtual void FuncB(void) = 0;
};

// {F86DB061-E8ED-413b-93B0-F4BBC0E23ECD}
static const IID IID_IChap5_1 = 
{ 0xf86db061, 0xe8ed, 0x413b, { 0x93, 0xb0, 0xf4, 0xbb, 0xc0, 0xe2, 0x3e, 0xcd } };

// {16DEFC8D-D997-48c3-8C7D-5D6D31533DAA}
static const IID IID_IChap5_2 = 
{ 0x16defc8d, 0xd997, 0x48c3, { 0x8c, 0x7d, 0x5d, 0x6d, 0x31, 0x53, 0x3d, 0xaa } };

// コンポーネント作成関数
extern void CreateInstance(IUnknown **);

component.cpp
コンポーネントの実装部分です。見慣れない単語が、かなり出てきてると思いますが、時期に判明するでしょう。
#include <objbase.h>
#include <iostream.h>
#include "interface.h"


///////////////////////////////////////
// CChap5クラス

class CChap5 : IChap5_1, IChap5_2
{
public:
    // IUnknownインターフェース
    virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject);
    virtual ULONG STDMETHODCALLTYPE AddRef(void);
    virtual ULONG STDMETHODCALLTYPE Release(void);

    // IChap5_1インターフェース
    virtual void FuncA(void);

    // IChap5_2インターフェース
    virtual void FuncB(void);
};

HRESULT STDMETHODCALLTYPE CChap5::QueryInterface(REFIID riid, void **ppvObject)
{
    // 指定されたIID(Interface ID)に応じて、そのインターフェースポインタを
    // *ppvObjectに渡してやります。
    if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IChap5_1))
    {
        *ppvObject = static_cast<IChap5_1 *>(this);
    }
    else if (IsEqualIID(riid, IID_IChap5_2))
    {
        *ppvObject = static_cast<IChap5_2 *>(this);
    }
    else
    {
        *ppvObject = NULL;
        return E_NOINTERFACE;
    }

    return S_OK;
}

ULONG STDMETHODCALLTYPE CChap5::AddRef(void)
{
    // 今は何もしません。
    return 0;
}

ULONG STDMETHODCALLTYPE CChap5::Release(void)
{
    // Release関数は、その名の通りコンポーネントの開放を行います。
    delete this;
    return 0;
}

void CChap5::FuncA(void)
{
    cout << "CChap5::FuncAが呼ばれたよ~" << endl;
}

void CChap5::FuncB(void)
{
    cout << "CChap5::FuncBが呼ばれたよ~" << endl;
}


///////////////////////////////////////
// コンポーネント作成関数

extern void CreateInstance(IUnknown **ppUnk)
{
    CChap5   *pChap5 = new CChap5;
    pChap5->QueryInterface(IID_IUnknown, (void **)ppUnk);
}

client.cpp
クライアントのソースです。かなりすっきりしましたね。
#include <objbase.h>
#include <iostream.h>
#include "interface.h"

int main(void)
{
    IUnknown    *pUnk;
    IChap5_1    *pChap5_1;
    IChap5_2    *pChap5_2;

    // コンポーネントのインスタンスを作成する。
    CreateInstance(&pUnk);

    // IChap5_1インターフェースを取得して使用する。
    pUnk->QueryInterface(IID_IChap5_1, (void **)&pChap5_1);
    pChap5_1->FuncA();

    // IChap5_2インターフェースを・・・。
    pUnk->QueryInterface(IID_IChap5_2, (void **)&pChap5_2);
    pChap5_2->FuncB();

    // 使い終わったコンポーネントを開放する。
    pUnk->Release();

    return 0;
}



interface.hから見ていくことにしましょう。
前回からの変更点としては、IChap5_1とIChap5_2のインターフェースは共にIUnknownを継承するように修正されていることが見て取れます。 IUnknownはunknwn.hによって下記のように定義されています。
unknwn.hの一部
interface IUnknown
{
    virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) = 0;
    virtual ULONG STDMETHODCALLTYPE AddRef(void) = 0;
    virtual ULONG STDMETHODCALLTYPE Release(void) = 0;
};
unknwn.hはobjbase.hに含まれており、 objbase.hをインクルードした時点で自動的にインクルードしたことになります。
IUnknownに3つの関数が定義されているため、IChap5_1とIChap5_2では、4つの関数を含んだインターフェースを定義していることになります。

次に、IID型で宣言された2つの変数 IID_IChap5_1とIID_IChap5_2が目を引きます。 IIDとは、Interface IDentifierの頭文字を取ったもので、 各インターフェースを識別するための整理番号みたいなものです。実際には128ビットの構造体となっています。詳細については、第9章で解説することにします。
最後に、コンポーネントを作成する関数が1つとなり、開放する関数は削除されました。 作成関数はIUnknownインターフェースポインタを格納する変数へのポインタを受け取るようになっています。



コンポーネントを実装するcomponent.cppを見ていきます。
クラス定義で特筆すべきは、やはりIUnknownの3つの関数の宣言が追加されたことです。
このうち、今回はAddRef関数を特に実装する必要はないのですが、抽象基本クラスを実装する場合、 抽象基本クラスの全メンバー関数を実装する必要があるため、不要でも宣言しています。

実装部分において、今回のサンプルで特に重要なのは、QueryInterface関数です。
この関数はコンポーネントがサポートするインターフェースを取得するために存在しています。
この関数は、第1引数にIIDを取り、第2引数に取得するインターフェースへのポインタを格納する変数のポインタを取ります。クライアントは、コンポーネントに対してインターフェースの要求をする場合、第1引数のIIDに目的のインターフェースIDを渡します。
コンポーネントが指定されたインターフェースをサポートしているならば、第2引数にそのインターフェースへのポインタを代入し、戻り値として正常終了を意味するS_OKを返却し、逆にインターフェースをサポートしていない場合は、第2引数をNULLを代入して戻り値E_NOINTERFACEを返却します。戻り値の型であるHRESULTについては後の章で解説します。
これがCOMのオブジェクトの実行時要求のカラクリです。クライアントはコンポーネントに想定していたインターフェースが実装されていない場合、E_NOINTERFACEを受け取るため、それに応じた処理を行うことが可能となっています。
なお、この戻り値はCOMの仕様によって明確に定められており、別の値を返却することは(基本的に)許されていません。

AddRef関数は、今回は実装していません。戻り値として0を返却しているだけです。

Release関数は、その名の通りコンポーネントの開放を行います。
この関数を実装したことによって、コンポーネントを開放する関数が不要になったことが分かります。 インターフェースを使わないアクセスが1つ削除されたことにより、完全なCOMの実装に少しだけ近づいたことになります。
しかし、現時点では単純にクラスをdelete演算子で削除していますが、実際にはこれだけでは不完全です。 AddRefReleaseは次の章で完全な実装を行います。

最後にコンポーネントを作成するCreateInstance関数です。
これはコンポーネントのインスタンスをnewし、 QueryInterface関数を呼び出しIUnknownインターフェースをコンポーネントに要求しているだけです。
これはなるべくコンポーネントの実装に依らずに、インターフェースを取得するためです。
が、実際に作成する上では、特にこれに従う必要はありません。newした後で単純に型キャストするだけの実装もよく使うと思います。



最後にコンポーネントを使用するクライアント側のソースclient.cppを見ていきます。
コンポーネントがよりCOMオブジェクトの実装に近づいたこともあり、クライアント側のコードは相当に簡略化されています。
インターフェースIChap5_1とIChap5_2の両方を使用するために、作成しなければならないインスタンスは1つだけです。 これは前回のChapter.4に比べれば大きな進歩と言えます。

また、インターフェースは使用する時に要求していることもわかります。 これはChapter.3で述べたオブジェクトの実行時要求を満たしていることを意味します。
今回はサンプルであるため、インターフェースの要求が失敗した場合については、特に何も考慮されていません。 まあ、次回も何も考慮されていないでしょう。(笑

最後に使い終わったコンポーネントを開放するために必要なのは、インターフェースに対してRelease関数を呼び出しているだけです。 これはコンポーネントのカプセル化に一役買っています。 何故ならば、IUnknownが実装されたコンポーネント(COMオブジェクトには全て実装されている)に対しては、 Release関数の呼び出しを行うことによって、どんなコンポーネントも開放できることを意味しているからです。



お疲れ様でした。
この章の要点は、オブジェクトの実行時要求オブジェクトのカプセル化を提供するための手段としてインターフェースIUnknownが設計されているということです。
さらにIUnknownには、もう1つ役目があるのですが、それは次の章に委ねます。勘のいい人なら気付くと思いますが、今回は実装されていないAddRef関数の役割に関することです。

この章は書き始めたのが一昨年(2001年)の1月で、それ以降書く暇がなかったので、書き始めた頃とは大きく変わってきてしまいました。また、既出の文章に関しても、内容に矛盾が出ないように手を加えたので、結構な修正量になってしまいました。

最後に上記3ファイルを固めたサンプルです。コンソールアプリケーションとして、ビルド&実行してください。
C++用ソース:chap5.lzh(2KB未満)


前へ 次へ
COM研究室へ
総合トップへ