CoCreateInstanceでCOMオブジェクトを作成する方法
Windows APIのCOM(Component Object Model)では、実装クラスを直接生成する代わりに、登録されたクラスを識別子で指定してオブジェクトを取得します。その中心となる関数がCoCreateInstanceです。ファイル操作、XML解析、画像処理、音声処理など、Windowsや各種ミドルウェアが提供するCOMコンポーネントを利用するときに使われます。
呼び出しには、COMの初期化、CLSID、取得したいインターフェースのIID、サーバーの実行方式、戻り値の解放という複数の要素が関係します。どれか一つを省略すると、作成に失敗したり、処理終了時にメモリやCOM参照が残ったりするため、基本的な流れを整理しておくことが重要です。
COMオブジェクト生成の基本構造
CoCreateInstanceは、クラス識別子(CLSID)で指定したCOMクラスのインスタンスを作り、要求したインターフェースへのポインターを返します。呼び出し側は実装クラスの内部構造を知る必要がなく、公開されたインターフェースだけを通じて機能を利用できます。
使用前には通常、CoInitializeExまたはCoInitializeを呼び出して、現在のスレッドをCOM環境へ参加させます。スレッドごとに初期化が必要であり、処理が終わったら同じスレッド上でCoUninitializeを呼び出します。GUIではSTA、並列処理ではMTAを選ぶことが多いものの、対象コンポーネントの仕様も確認が必要です。
CLSIDとIIDを指定する
CLSIDは作成するCOMクラスを表し、IIDは利用するインターフェースを表します。たとえば、クラスの識別子が同じでも、取得するインターフェースによって利用できるメソッドが変わります。標準インターフェースではIUnknownやIDispatch、個別コンポーネントでは独自のインターフェースIDを指定します。
文字列形式のGUIDを使う場合はCLSIDFromStringやIIDFromStringで変換できます。ヘッダーファイルに定義済みの識別子がある場合は、DEFINE_GUIDやuuidofを使う方が入力ミスを抑えられます。C++では、型からIIDを取得できる__uuidof(IMyInterface)を利用できる場合があります。
レジストリにはCLSID、InprocServer32、LocalServer32などの情報が登録されています。DLL型のインプロセスサーバーは呼び出し元プロセスに読み込まれ、EXE型のローカルサーバーは別プロセスで動作します。登録状態やビット数が一致しないと、REGDB_E_CLASSNOTREGなどのエラーになります。
CoCreateInstanceの引数と呼び出し例
基本的な宣言は次のような形です。
HRESULT CoCreateInstance(
REFCLSID rclsid,
LPUNKNOWN pUnkOuter,
DWORD dwClsContext,
REFIID riid,
LPVOID* ppv
);
単純な作成では、集約を使わないため第2引数にnullptrを指定し、実行方式にCLSCTX_INPROC_SERVERを指定します。成功した場合はS_OKが返り、最後の引数にインターフェースポインターが格納されます。
#include <windows.h>
#include <objbase.h>
#include <wrl/client.h>
using Microsoft::WRL::ComPtr;
HRESULT CreateComponent()
{
HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
if (FAILED(hr)) {
return hr;
}
{
ComPtr<IMyInterface> object;
hr = CoCreateInstance(
CLSID_MyComponent,
nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&object)
);
if (SUCCEEDED(hr)) {
object->Execute();
}
}
CoUninitialize();
return hr;
}
IID_PPV_ARGSは、受け取るポインター型とIIDの不一致を防ぎやすい記述です。生のポインターを使う場合は、成功時にReleaseを必ず呼びます。スマートポインターのComPtrやATLのCComPtrを使うと、スコープ終了時の参照カウント管理を自動化できます。
サーバー実行方式を使い分ける
dwClsContextには、COMサーバーをどこで実行するかを指定します。インプロセスDLLは呼び出しが速い反面、サーバーの不具合がアプリケーション全体に影響します。ローカルサーバーはプロセス境界で分離されますが、起動やRPC通信のコストが発生します。
| 実行方式 | 主な対象 | 特徴 | 注意点 |
|---|---|---|---|
| CLSCTX_INPROC_SERVER | DLLサーバー | 高速で呼び出しが軽い | DLLのビット数と登録を合わせる |
| CLSCTX_LOCAL_SERVER | EXEサーバー | 別プロセスで動作する | 起動時間とRPC遅延がある |
| CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER | 複数候補 | 利用可能な方式を探せる | 選択結果を環境に依存する |
| CLSCTX_ALL | 多くの登録方式 | 記述が簡単 | 実行場所を明確にしにくい |
実行方式を広く許可するCLSCTX_ALLは検証時には便利ですが、セキュリティや性能を考えると必要な方式だけに絞る方が管理しやすくなります。サービスや権限の異なるプロセスから利用する場合は、DCOMの構成、ユーザー権限、セッション分離も関係します。
HRESULTと参照カウントを確認する
COM関数の戻り値はHRESULTで判定します。SUCCEEDEDとFAILEDを使えば、成功値や失敗値の詳細な数値に依存せず処理を分岐できます。作成に失敗した場合は、CLSIDの登録、IID、COM初期化、DLLの依存関係を順に調べると原因を絞りやすくなります。
REGDB_E_CLASSNOTREGはクラス登録や対象ビット数を確認するE_NOINTERFACEは要求したIIDが実装されているか確認するCO_E_NOTINITIALIZEDはスレッドごとのCOM初期化を確認する0x80040154などの登録エラーはレジストリとインストール状態を確認する- 作成後はインターフェースのRelease漏れがないか確認する
COMインターフェースは参照カウント方式で管理されます。QueryInterfaceで別のインターフェースを取得すると、通常は新しい参照が加算されます。取得したポインターを長期間保持する場合は所有期間を明確にし、循環参照が起きる設計では弱参照や明示的な切断処理を検討します。
スレッドモデルと周辺APIの境界
COMオブジェクトの作成は、単に関数を呼ぶだけで完了するとは限りません。STAで作成したオブジェクトを別スレッドから直接操作できない場合があり、必要ならマーシャリング、メッセージループ、または対象コンポーネントが定める呼び出し方法を使います。スレッド生成後のCOM初期化も忘れないようにします。
Windowsのスレッドを作成して処理を分離する場合は、スレッド関数の開始方法と終了待ちも重要です。CRTを使うアプリケーションでは、スレッド開始方法の注意点も確認すると、ランタイム初期化や終了処理に関する問題を避けやすくなります。
ウィンドウを持つCOMクライアントでは、ウィンドウプロシージャや状態保存領域を変更する処理が同じスレッドモデルと絡むことがあります。SetWindowLongを使ったウィンドウ拡張スタイルの変更も、対象ウィンドウのスレッドとライフサイクルを意識して実装します。
実装時に分けて考える処理
CoCreateInstanceを呼ぶコードは、初期化、生成、利用、解放の四段階に分けると読みやすくなります。エラー処理を各段階に置けば、どの操作で失敗したかをログに残せます。ログにはHRESULT、CLSID、IID、選択した実行方式を記録すると、利用環境の差を調査しやすくなります。
COMコンポーネントがデバイス制御やファイルハンドルを扱う場合、COMの生成成功だけで処理全体が保証されるわけではありません。取得したインターフェースの初期化結果や、個別メソッドのHRESULTも検証します。デバイスとの低レベル通信を組み合わせるときは、DeviceIoControlの通信方法のようなWindows API側の仕様も切り分けて確認します。
実装を終了するときは、利用中のインターフェースを先に解放し、その後でCOMの初期化を解除します。スコープ管理を採用していても、別スレッドにポインターを渡した箇所やコールバックを登録した箇所では、終了順序を個別に確認する必要があります。
CoCreateInstanceで大切なのは、CLSIDでクラスを選び、IIDで契約するインターフェースを選び、適切なCOM初期化と実行コンテキストを組み合わせることです。HRESULTの確認、参照カウントの解放、スレッドモデルの整合性まで含めて管理すれば、COMオブジェクト生成を安定したWindows API利用へつなげられます。