2006 年 11 月 13 日 21 時 53 分

クライアントから見た IUnknown


このアーカイブは同期化されません。 mixi の日記が更新されても、このアーカイブには反映されません。


IUnknown の定義を再度引用しておこう。

class IUnknown {

public:

    virtual HRESULT STDMETHODCALLTYPE QueryInterface(
            /* [in] */ REFIID riid,
            /* [out] */ void** ppvObject) = 0;

    virtual ULONG STDMETHODCALLTYPE AddRef() = 0;

    virtual ULONG STDMETHODCALLTYPE Release() = 0;

};

念のため書いておくが、これはインタフェースである。
C++ の構文上の制約で、上のような書き方になっている。

IUnknown は、ルートインタフェースであり、
あらゆる COM インタフェースがこれを継承し、
あらゆる COM クラスがこれを実装することが要求される。
そのため、あらゆるインタフェースポインタから
上記メソッドを呼び出し可能なことが保証される。

IUnknown は、インスタンスの管理に必要な、
最小限のメソッドを定義している。

クライアントは、インタフェースポインタを通じて、
サーバにあるインスタンスを操作する。
クライアントは、インタフェースポインタをコピーしたり、
他のメソッドに渡したりすることができる。

サーバは、参照されているポインタの数を管理しており、
インスタンスを指すポインタがなくなれば、
一般的にはインスタンスを破棄する処理を取る。

この規則を満たすために存在するのが、
IUnknown というインタフェースである。

AddRef や Release は、インスタンスに対して、
インタフェースポインタのコピーを行ったこと、
インタフェースポインタの利用を終了したことを通知する。
これは、サーバの管理している参照カウンタを増減する。

このメソッドは絶対に成功するように実装されており、
戻り値は参照カウンタの現在値である。
値が 0 ならば、インスタンスは破棄されたことになる。
基本的にこの値はデバッグ目的以外に使わない。

QueryInterface は、インスタンスに対して、
別のインタフェースを実装しているかどうかを問い合わせる。
riid は、インタフェースの ID(IID)であり、
ppvObject には、ポインタを受け取る変数を渡す。

インスタンスが指定したインタフェースを実装していた場合、
サーバは指定したインタフェース型のポインタを割り当て、
ppvObject 引数を経由してクライアントに返す。

これは、高水準言語の実行時キャストに相当する機能だ。
クライアント側には実装の詳細が隠されているため、
言語機能で型キャストすることはできず、
必ず QueryInterface を呼び出す必要がある。
(クライアントには、キャストの妥当性を調べる方法がない)

言葉で説明しても分かりにくいと思うので、
クライアントから見た IUnknown の使い方の例を示す。

SetObject 関数は引数で任意のオブジェクトを受け取り、
それをグローバル変数 _object に格納する。
もしオブジェクトが IFoo インタフェースを実装していれば、
その Bar というメソッドを呼び出すこととする。

IUnknown* _object = NULL;

HRESULT SetObject(/* [in] */ IUnknown* object) {

    // NULL なら例外とする
    if (object == NULL) return E_POINTER;

    IFoo* foo;
    HRESULT ret;

    // IFoo にキャストできるか試す
    ret = object->QueryInterface(IID_IFoo, (void**)&foo);

    if (SUCCEEDED(ret)) {

        // キャストに成功した場合……

        // IFoo#Bar の呼び出し
        ret = foo->Bar();

        // 使い終わったポインタは必ず解放
        foo->Release();

        // Bar が失敗したら関数も失敗
        if (FAILED(ret)) return ret;
    }

    // 値を _object に代入

    // 既存の _object が有効なポインタを持っていれば解放
    if (_object != NULL) {
        _object->Release();
        _object = NULL; // 省略してもいい
    }

    // ポインタを複製する場合、必ずサーバに通知
    _object = object;
    object->AddRef();

    // 成功を意味する値を返す
    return S_OK; 
}

QueryInterface は別のインタフェースへのキャストを行う。
QueryInterface が成功した場合、
新しいインタフェースポインタを作成して返すので、
使い終わったら Release で解放する事が肝要である。

_object に代入する操作においても、
既存の _object 値が有効な値を指している場合、
その値を上書きする前に元のポインタを解放している。

object 引数は、呼び出し元から受け取ったものなので、
これは勝手に解放してはいけない。

このように、クライアントはインスタンスの寿命を気にせず、
自分で取得したポインタを全て Release で解放すれば良い。

COM のポインタを扱うには手間が多いことが分かるが、
VBA や VBScript、.NET では、IUnknown の操作や、
HRESULT の処理は言語機能に隠蔽されており、
より直感的にコードを書けるようになっている。

VC++ でも、_com_ptr_t というスマートポインタを用いると、
AddRef/Release/QueryInterface を裏に隠蔽し、
HRESULT を C++ 例外処理に置き換えてくれるので
コードを見やすく書くことは可能である。
まあ、これは今回の本筋から離れるので扱わない。

明日は、サーバ側の IUnknown 実装を考えてみよう。



Copyright (c) 1994-2007 Project Loafer. All rights reserved.