2006 年 11 月 15 日 23 時 46 分

サーバから見た IUnknown(後編)


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


保留にしていた QueryInterface の実装を考えて見よう。
IUnknown の最大の山場が QueryInterface である。

クライアント側がインタフェースを利用する際に呼び出され、
インスタンスがインタフェースを実装しているか確認し、
実装していれば新しいインタフェースポインタを返す。

==================================================
HRESULT Object::QueryInterface(
        /* [in] */ REFIID riid,
        /* [out] */ void** ppvObject) {

    // ppvObject が NULL なら例外
    if (ppvObject == NULL) return E_POINTER;

    // ppvObject は out なので、
    // ポインタが指す元の値は無視できる
    *ppvObject = NULL;

    // インタフェース型へのキャスト
    if (IsEqualIID(riid, IID_IUnknown)) {
        *ppvObject = static_cast<IUnknown*>(this);
    } else {
        return E_NOINTERFACE;
    }

    // ポインタを作成した場合カウンタを増加
    static_cast<IUnknown*>(*ppvObject)->AddRef();

    return S_OK;
}
==================================================

riid 引数でインタフェースの ID が渡されるため、
自分が実装しているインタフェースかどうか調べ、
一致した場合はそのインタフェース型のポインタを作成する。

実装クラス自身がキャスト演算を使うことは特に問題ない。
static_cast<型> は、C++ のキャスト演算子である。
厳密なインタフェースポインタ型にキャストするためには、
静的な static_cast 演算子を使うことが推奨される。

意味的には、クライアントから見た動的なキャストなのだが、
サーバの実装では静的なキャストで良い。
それは、クラスの提供をしているサーバ側は、
当然ながらクラスの実装の詳細を知っており、
既知のインタフェースのみを返却すれば十分であるからだ。

もし、ここで C 形式のキャスト等を使ってしまうと、
コーディングによっては問題が起こる可能性がある。

C++ と COM では多重継承の問題により、
キャストした後のインタフェース型のポインタと、
クラス自身の this ポインタの指し示すアドレスが
一致しないことがよくあるため、
アドレスを強制的にキャストすることは危険なのである。

さて、ポインタを作成した場合は、
必ず AddRef を呼び出して参照を加算する必要がある。
これは、QueryInteface が AddRef を兼ねているからだ。

要求されたインタフェース型のポインタで、
AddRef を呼び出す必要があるので、
本来は if 文の中で個別に書く必要があるのだが、
ここでは if 文の外に出して呼び出している。

これは、複数のインタフェースを実装した場合、
何箇所も AddRef を書く手間を防ぐためである。
void * から IUnknown * へのキャストは一般的に危険だが、
ここでは問題ない。これはあらゆるインタフェースが
IUnknown を継承することが保証されているからである。

本来は ppvObject は、void** ではなく、
IUnknown** の方が意味論的にもはっきりするのだが、
歴史的な理由により、void** が使われているので、
このキャストはどうしても必要である。
回避しようと思うと、今度は別の変数が必要になる。

あと、今まで触れていなかったのだが、
引数には、わざと in, out などのコメントをつけていた。
これは、COM の引数の方向属性というものであり、
インタフェースのあらゆるメソッドの引数に定義されている。

C++ においてはコメントなので何の意味もないが、
実装者はこの方向と役割を常に意識しておく必要がある。

COM は、クライアントサーバシステムであるため、
RPC でネットワーク越しに引数を転送しなければならない。
その効率を上げるために、引数の「値の方向」を定めている。

値というのは引数の内容(実引数)のことで、
ポインタ型の場合、ポインタの指し示す値のことを意味する。

in 属性は、呼び出し側がメソッド側に値を渡し、
メソッド側から呼び出し側には何も返さないことを示す。
これは、読み取り専用の引数ということになる。
RPC ではメソッドから戻る際に、
サーバからクライアントへの値の書き戻しを抑制する。

これは、一般的な値渡しの概念であり、
ポインタ以外の型の場合は、原則 in である。
(通常型の引数を変更しても呼び出し元に全く関係ないため)

ポインタ型の場合は、呼び出し側が意味のある値を渡し、
メソッド側がポインタの指す値を変更しない事を意味する。
これは、ポインタの指すインスタンスの
メソッドを呼んではいけないと言うことではない。

out は in の逆で、呼び出し側はメソッド側に何も渡さず、
メソッド側から呼び出し側には値を返すことを示す。
これは、書き込み専用の引数ということになる。
RPC ではクライアントからサーバへの値の転送を抑制する。

out はポインタ型に指定しなければ意味がない。
これは、呼び出し側は意味のある値を渡さず、
入れ物としての単なるバッファのポインタだけを渡し、
メソッド側がそこに値を書き込んで返すことを意味する。

また、in と out 両方の属性を併せ持つ引数も存在する。
これは、読み書き両用の引数ということになる。

さて、QueryInterface の第 2 引数の ppvObject には、
メソッドの定義によって out 属性が付与されている。
*ppvObject(ポインタの内容)は書き込み専用であるため、
このポインタの指す値を参照してはいけない。
そのため、元の値のことは気にしなくても良いのである。

昨日、クライアント側のコード片で、
インタフェースポインタ変数に値を代入する際に、
元の値の解放を忘れないようにと言う風に書いた。

もし引数が /* [in, out] */ IUnknown** ppvObject なら、
*ppvObject が NULL 以外の値を指していれば、
先に、*ppvObject->Release() が必要となるのだ。

しかし、/* [out] */ IUnknown** ppvObject なら、
*ppvObject が NULL でなくとも解放の必要はない。

方向属性は、COM にとって非常に重要なのである。



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