QueryInterfaceでCOMインターフェースを取得する方法
Windows APIでCOMオブジェクトを扱うとき、QueryInterfaceは実装済みのインターフェースを調べ、必要な機能へ安全に切り替えるための基本メソッドです。COMでは、生成したオブジェクトの具体的なクラス型を直接操作するのではなく、IIDで識別されたインターフェースを通じて機能を利用します。
たとえば、IUnknownポインターから特定のインターフェースを取得したり、オブジェクトが任意の機能をサポートしているか確認したりする場面で使います。戻り値のHRESULT、参照カウント、IIDの指定方法を正しく理解すると、アクセス違反やメモリリークを避けやすくなります。
QueryInterfaceの役割とCOMの設計
COMインターフェースは、関連するメソッドの集合を表す契約です。各インターフェースには一意な識別子であるIIDが割り当てられ、クライアントはそのIIDを渡して目的のインターフェースを要求します。要求に対応していればポインターが返され、未対応ならE_NOINTERFACEが返ります。
基本的な宣言は次の形です。
HRESULT QueryInterface(
REFIID riid,
void** ppvObject
);
riidには取得したいインターフェースのIIDを指定し、ppvObjectには結果を受け取るポインターのアドレスを渡します。COMオブジェクトがIUnknownを実装している限り、少なくともIUnknown自身への問い合わせは可能です。
C++でインターフェースを取得する
従来の書き方では、取得先をvoid*として用意し、成功後に目的の型へ変換します。
IMyInterface* myInterface = nullptr;
HRESULT hr = unknown->QueryInterface(
IID_IMyInterface,
reinterpret_cast<void**>(&myInterface)
);
if (SUCCEEDED(hr)) {
myInterface->DoSomething();
myInterface->Release();
}
ただし、reinterpret_cast<void**>は型の扱いを誤りやすいため、現代的なC++では__uuidofとスマートポインターを組み合わせる方法が便利です。インターフェース定義にuuid属性があれば、次のようにIIDを自動的に取得できます。
Microsoft::WRL::ComPtr<IMyInterface> myInterface;
HRESULT hr = unknown.As(&myInterface);
if (SUCCEEDED(hr)) {
myInterface->DoSomething();
}
ATLを利用する場合はCComPtr、Windows Runtime LibraryではMicrosoft::WRL::ComPtrが代表的です。これらはスコープ終了時にReleaseを呼び出すため、例外や途中のエラーが発生しても後処理を漏らしにくくなります。
HRESULTと失敗時の処理
QueryInterfaceの成否は、戻り値のHRESULTで判定します。SUCCEEDED(hr)やFAILED(hr)を使うと、成功コードと失敗コードを符号の判定だけで扱えます。特定のインターフェースが存在するかだけを調べる場合は、hr == E_NOINTERFACEを確認する実装も有効です。
void* result = nullptr;
HRESULT hr = unknown->QueryInterface(
IID_IOptionalFeature,
&result
);
if (hr == E_NOINTERFACE) {
// このオブジェクトは機能をサポートしていない
} else if (SUCCEEDED(hr)) {
auto* feature = static_cast<IOptionalFeature*>(result);
feature->UseFeature();
feature->Release();
}
成功した場合、返されたインターフェースポインターには通常、呼び出し側が所有する参照が追加されています。そのため、利用が終わったら必ずReleaseが必要です。QueryInterfaceが失敗したときは、出力ポインターを利用してはいけません。
IID、継承、インターフェースの互換性
インターフェースの取得では、C++の継承関係とCOM上のIIDを混同しないことが重要です。あるインターフェースが別のインターフェースを継承していても、目的のIIDを使って問い合わせる必要があります。単純なC++キャストだけでは、COMオブジェクトが実装している別インターフェースへ移動できません。
IUnknownにはQueryInterface、AddRef、Releaseが定義されています。派生インターフェースもこの3メソッドを備えますが、インターフェースごとにメソッドテーブル上の構成が異なるため、無関係な型へのキャストは危険です。IIDを使った問い合わせが、実装の有無を確認する正式な手段になります。
APIの宣言やWindows開発の補足情報を調べる際は、Windows API資料のような技術リファレンスも、関数の周辺概念を確認する手がかりになります。実際のSDKヘッダーとMicrosoftの公式ドキュメントを照合しながら使うと、古いサンプルコードの差異にも対応できます。
参照カウントと安全な所有権管理
QueryInterfaceが成功すると、通常は取得したポインターの参照カウントが1つ増えます。元のポインターを解放しても、問い合わせで得たポインターが有効であるのは、この独立した参照があるためです。反対に、使い終わったポインターを解放しなければ、COMサーバーや関連リソースが残り続けます。
| 処理方法 | 利点 | 注意点 |
|---|---|---|
生ポインターとRelease |
COMの仕組みを理解しやすい | 解放漏れや二重解放が起きやすい |
CComPtr |
ATL環境で扱いやすい | ATLへの依存が生じる |
ComPtr |
WRLで自動管理できる | Resetやアドレス取得方法を理解する必要がある |
_com_ptr_t |
例外ベースのコードと相性がよい | 使用するコンパイラ設定に注意が必要 |
スマートポインターへ既存の生ポインターを渡す場合は、所有権を移すのか、参照を追加するのかを確認してください。たとえばAttachは既存の参照をそのまま引き受けるため、同じポインターを別の管理対象にも登録すると二重解放につながる可能性があります。
実装時に確認したいポイント
QueryInterfaceを組み込む前に、対象オブジェクトがどのインターフェースを公開しているかをSDKの仕様で確認します。生成元の関数が返す型だけを見て判断せず、必要な機能のIID、呼び出し可能なスレッドモデル、初期化条件まで確認すると実行時エラーを減らせます。
実装時には次の点を意識すると、取得処理を安定させられます。
- 出力ポインターを呼び出し前に
nullptrで初期化する - 戻り値を確認してから取得先のメソッドを呼び出す
- 成功したインターフェースを必ず
Releaseまたはスマートポインターで管理する E_NOINTERFACEを機能未対応として適切に処理する- COMの初期化と終了をスレッド単位で正しく行う
COMオブジェクトによっては、同じIIDへの問い合わせで常に同一のポインターが返るとは限りません。また、集約されたオブジェクトでは、外側のIUnknownと内部オブジェクトの関係が通常の生成物と異なる場合があります。仕様にないポインター比較や内部実装への依存は避けるべきです。
問い合わせ処理を設計へ組み込む
QueryInterfaceは、単なる型変換ではなく、実行時に機能の有無を確認する仕組みです。オプション機能を持つ複数のCOMコンポーネントへ対応する場合、まず基本インターフェースを取得し、追加機能のIIDを個別に問い合わせる設計が適しています。
生ポインターを使う場合の最小形は次のようになります。
IMyInterface* p = nullptr;
HRESULT hr = object->QueryInterface(__uuidof(IMyInterface),
reinterpret_cast<void**>(&p));
if (SUCCEEDED(hr)) {
p->DoSomething();
p->Release();
p = nullptr;
}
この処理を共通関数やRAIIラッパーへまとめれば、エラー判定と後始末を各所に重複して記述せずに済みます。実務では、IIDの指定、HRESULTの確認、所有権の管理を一組の処理として扱うことが、堅牢なCOMプログラムへの近道です。