全般 †
なぜCOMが必要なのか †
COM通信は大きく分けて以下の3つが挙げられる。
- 同一プロセス内でのCOM通信。DLL形式のCOMサーバを用いる。
- 異なるプロセス間でのCOM通信。マーシャリングという仕組みによって実現される。EXE形式のCOMサーバを用いる。
- 異なるマシン間でのCOM通信。ネットワークを介して通信される。DCOMと呼ばれる。
このうち、2番目と3番目の通信方法を実現するためにはCOMは有効である。しかし、1番目の同一プロセス内での通信は、COMではない通常のDLLでも実現可能である。
それでもCOM通信を用いる理由は、通常のDLLは使用する際に以下の制約があるからである。
- DLLを使用するアプリケーション側とDLL側で、関数の呼出規約を合わせる必要がある。
しかし、言語によっては呼出規約を自由に決められない場合がある。
- DLLを使用するアプリケーション側と、DLL側で、関数のマネージド/アンマネージドを合わせる必要がある。
しかし、言語によっては.NETフレームワークを使用できず、マネージドコードを作れない場合がある。
関連用語 †
呼称 | 説明 |
COM (Component Object Model) | アプリケーションの機能の異なる言語間での連携、再利用を行う技術。 |
COMサーバ | 決められたインタフェースでCOMクライアントに機能を提供するCOMコンポーネント。 |
COMクライアント | COMサーバの提供する機能を利用するCOMコンポーネント。 |
カスタムインタフェース | COMがアーリーバインディングでインタフェースを提供するための手段。 IUnknownを継承している必要がある。 |
ディスパッチインタフェース (またはオートメーション) | COMがレイトバインディングでインタフェースを提供するための手段。 IDispathを継承している必要がある。(必然的にIUnknownも継承される) 引数は全てVARIANT型で授受されるため、VBやJSなどをクライアントにすることが可能となる。 |
デュアルインタフェース | カスタムインタフェースとディスパッチインタフェースの両方を持つこと。 (全てのCOMコンポーネントはカスタムインタフェースを提供するため、ディスパッチインタフェースを提供するCOMコンポーネントはデュアルインタフェースである) |
ATL (Active Template Library) | COMオブジェクトの開発を支援するための、マイクロソフトによるクラスライブラリ。 MFCを用いるより軽量なコンポーネントを作成することを目的としている。 |
ActiveX | COMを利用した、自己登録可能なコントロールのこと。 MFC付属のコントロールは全てActiveXコントロール。 |
OLE (Object Linking and Embedding) | オブジェクトの埋め込み技術。 OLEコントロールとはActiveXコントロールの前身の存在で、名称が異なるだけで同じもの。 |
OCX (OLE-based Control eXtension) | 1つ以上のActiveXコントロールが登録、保存されたファイル。 ファイルの拡張子はOCXだが、中身はDLL。 |
UUID (Universally Unique Identifier) | 汎用一意識別子。128ビットの数値で表わされるユニークな識別子。 |
GUID (Globally Unique Identifier) | グローバル一意識別子。マイクロソフトにより実装されたUUIDの一種。 |
LIBID | ライブラリID。COMコンポーネントのライブラリを識別するためのGUID。 |
CLSID | クラスID。COMコンポーネントのコクラスを識別するためのGUID。 |
IID | インタフェースID。COMコンポーネントのインタフェースを識別するためのGUID。 |
IDL (Interface Definition Language) | インタフェース記述言語。 言語に依存しないオブジェクトのインタフェースを記述する言語。 IDLファイル(COMクライアント側)、ODLファイル(COMサーバ側)などで使用される。OMG IDL。 |
参照カウント | COMコンポーネントが複数のクライアントから同時に使用されることを考慮して生存期間を管理するための手段。 あるCOMコンポーネントがクライアントから使用開始されたときにインクリメントされ、そのクラインアントが使用終了したときにデクリメントされる。 参照カウントが0→1になるときにCOMコンポーネントのプロセスが起動し、1→0になるときにプロセスが終了する。 |
アパートメント | 異なるプロセス/スレッド間であってもそれを意識せずにCOM通信を行うためのCOM特有の考え方。STAとMTAが存在する。 STAのCOMサーバーは、必ずシングルスレッドで動作し、MTAの場合はマルチスレッドで動作する。 COMを使用するスレッドは、サーバー側クライアント側を問わず、CoInitializeEx()によって必ずいずれかのアパートメントに属する。 |
マーシャリング | 異なるアパートメント間(つまり、異なるプロセスやスレッド間)であってもCOM通信を関数コールのように実現するための方法。 プロキシ/スタブDLLという仲介役によって実現される。 |
接続ポイント | |
参考リンク †
http://msdn.microsoft.com/ja-jp/vstudio/ffd7bcba-8a24-41c8-8f86-92c55c65fbde.aspx
http://program.station.ez-net.jp/special/vc/atl-com/variant.asp
http://www.tomosan.org/dev/vs/activex.html
http://www.ops.dti.ne.jp/~allergy/com/com.html
http://atata.sakura.ne.jp/
注意事項 †
- ATLによるCOMのレジストリ登録で、パスに2バイト文字が使われていると正常に登録できない。→リンク
データ型 †
Variant型 †
様々なデータ型を格納するために、保持するデータ型のIDと実際のデータを共用体で格納する。
関連する関数等。
種類 | 名称 | 処理 |
関数 | VariantInit() | Variant型変数の初期化 |
VariantClear() | Variant型変数のクリア |
VariantCopy() | Variant型変数のコピー |
VariantCopyInd() | ??? |
VariantChangeType() | Variant型の保持するデータ型の変換 |
VariantChangeTypeEx() | ??? |
変数型 | VARTYPE | VARENUM列挙体の値を格納してVariant型の種類を表す |
構造体 | VARIANT | Variant型を扱うための構造体 |
列挙体 | VARENUM | 保持できるデータ型の列挙体 |
MFC クラス | COleVariant | VARIANT構造体のラッパクラス |
ATL クラス | CComVariant | VARIANT構造体のラッパクラス |
その他 クラス | _variant_t | VARIANT構造体のラッパクラス |
※...\VC98\Include\OAIDL.h(338) を参照。
(C++からVARIANT型の変数を渡す場合)
- VARIANT型変数を、VariantInit()で初期化。
- VARIANT::vt に VARENUM列挙体 からデータ型を代入。
- データ型に合った共用体のメンバ変数に値を代入。
- 値を渡す。
- VARIANT型変数を、VariantClear()でクリア。
(C++でVARIANT型の変数を受け取る場合)
- VARIANT型変数を、VariantInit()で初期化。
- 値を受け取る。
- データ型に合った共用体のメンバ変数から直接値をコピーしたり、VariantChangeType()でデータを変換することで操作可能。
BSTR型 †
実体は wchar_t*型のUnicode文字列。ただし、通常の文字列とは異なる仕様を持つ。
- 先頭から前4バイト分が文字数を表す。
- 文字列中にNULL文字を埋め込むことができる。逆にいえば、NULL文字が文字列の終端を表さない。
関連する関数等。
種類 | 名称 | 処理 |
関数 | SysAllocString() | 引数で指定した文字数のBSTRを作成する |
SysFreeString() | 引数で指定したBSTRを削除する |
SysStringLen() | 引数で指定したBSTRの文字数を返す |
SysStringByteLen() | 引数で指定したBSTRのバイト数を返す |
SysAllocStringByteLen() | 引数で指定した8ビット文字列を指定したバイト数コピーしたBSTRを作成する |
SysAllocStringLen() | 引数で指定した16ビット文字列を指定した文字数コピーしたBSTRを作成する |
MFC | CString::AllocSysString | 格納されている文字列を元にBSTRを作成する |
ATL クラス | CComBSTR | 文字数を表す4バイトのプリフィックスを内包したBSTR型のラッパクラス |
その他 クラス | _bstr_t | BSTR型のラッパクラス |
メモリの扱いについて。→参考
- 入力値(引数)にBSTR型を持つメソッド
BSTRのメモリ確保、解放はクライアント(呼び出し)側が行う。
- 出力値(引数、戻り値)にBSTR型を持つメソッド
BSTRのメモリ確保はサーバ(メソッド実装)側が、解放はクライアント(呼び出し)側が行う。(クライアントがVBであれば、string型を使うことでスコープを抜けるときに自動的に解放されるはず?)
BSTR型を扱うときは、メモリリークしないように注意する。
以下は、_bstr_tクラスの扱いによるメモリリークの有無の例。
- メモリリークしない例
1
2
3
4
5
6
7
8
9
| | BSTR bstr = NULL;
m_pTestCom->GetString(&bstr);
_bstr_t bstrt(bstr, false);
|
- メモリリークする例
1
2
3
4
5
6
7
8
9
10
11
| | BSTR bstr = NULL;
_bstr_t bstrt;
m_pTestCom->GetString(&bstr);
bstrt = bstr;
|
配列型 †
次元数や要素数と、データを指すポインタを持つ構造体。
関連する関数等。
種類 | 名称 | 処理 |
構造体 | SAFEARRAY | COMの配列型を管理する構造体 |
MFC クラス | COleSafeArray | VARIANT構造体のラッパクラス |
ATL クラス | CComSafeArray | VARIANT構造体のラッパクラス VC++6.0では扱えない |
HRESULT型 †
VARIANT_BOOL型 †
COMで使用する真理値型。C言語で使用するBOOL型やbool型とは値が異なり、VBの真理値型と同じ値となる。
- 真(VARIANT_TRUE) : -1
- 偽(VARIANT_FALSE) : 0
スマートポインタ †
COMオブジェクトは使い終わったら必ず解放しなければならないが、スマートポインタを使用するとスコープを抜けるときに自動的にCOMオブジェクトの解放を行ってくれる。
関連する関数等。
種類 | 名称 | 処理 |
ATL クラス | CComPtr<T> | スマートポインタを実現するためのCOMオブジェクト管理用のテンプレートクラス |
その他 クラス | _com_ptr_t<T> | スマートポインタを実現するためのCOMオブジェクト管理用のテンプレートクラス |
オブジェクトの生成 †
CComPtr<T> はテンプレートクラスとして定義されているため、生成したいオブジェクトでポインタを定義する。
1
| | CComPtr<ISampleObject> pSampleObj;
|
実際の生成には、CComPtr<T>::CoCreateInstance() を用いる。
オブジェクトの代入 †
CComPtr<T>型の変数を定義し、既に生成済みのCOMオブジェクトを代入するときには、「=」演算子を使用してはいけない。「=」演算子はオーバロードされており、COMオブジェクトの参照カウントがインクリメントされてしまう。
代入には、基底クラスのCComPtrBase<T>::Attach() を用いる
1
2
3
| | pISampleObj = pIFooObj; pISampleObj.Attach(pIFooObj);
|
また、CComPtr<T>オブジェクトとして生成済みのCOMオブジェクトのアドレスを別の変数や関数に渡すときは、メンバ変数 CComPtr<T>::p を用いる。
オブジェクトの関数の呼び出し †
オーバロードされた演算子「->」を用いる。つまり、スマートポインタであることを意識せずに関数呼び出しを行うことができる。
オブジェクトの解放 †
何もする必要はない。デストラクタで解放を行ってくれるため、スコープを抜けるときに自動的にCOMオブジェクトが解放される。
もし明示的にCOMオブジェクトの解放を行う場合は、内部のオブジェクトを直接解放するのではなく、CComPtr<T>::Release() を用いる。「->」演算子を使用してはいけないことに注意。
1
2
| | pISampleObj->Release(); pISampleObj.Release();
|
また、スコープを抜けるときに自動的に解放されたくない場合は、基底クラスのCComPtrBase<T>::Detach() を用いてCOMオブジェクトをスマートポインタから切り離す。
COM識別子(GUID) †
COMサーバを識別するために、GUIDという128ビットの値を用いる。
GUIDは5グループの16進文字列で表わされる。それぞれ、8, 4, 4, 4, 12文字。
1
2
3
4
5
6
7
8
| | AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE
GUID::Data1 (DWORD) GUID::Data2 (WORD) GUID::Data3 (WORD) GUID::Data4[8] (BYTE)
|
ProgID †
COMサーバを識別するための文字列。VB系言語で CreateObject() に引数として渡す文字列。内部的には対応するCLSIDに変換される。
CLSID †
COMクラスの識別子で、GUIDで表わされる。
GUID型にtypedefされている。また、「REFCLSID」という識別子が、CLSID型の参照型として定義されている。(C言語ではポインタ型)
IID †
COMクラス内のインタフェースの識別子で、GUIDで表わされる。
GUID型にtypedefされている。また、「REFIID」という識別子が、CLSID型の参照型として定義されている。(C言語ではポインタ型)
その他 †
インタフェース定義ファイルの生成の流れ †
サーバもクライアントも、使用するインタフェースの定義はIDLファイルやタイプライブラリを用いる。通常、インタフェースの作成者が用意して、サーバもクライアントも同じ定義を使用する。
C++で実装を行う場合、インタフェースの定義をヘッダファイルやソースファイルに置き換える必要がある。
以下にファイルの生成の流れを示す。
File not found: "ref001.PNG" at page "技術資料/COM"[添付]
デバッグのヒント †
- atlbase.hをインクルードする前に_ATL_DEBUG_INTERFACESマクロを定義しておくことで、COMの参照カウントの増減をトレースできる。(デバッグウィンドウに表示される。)
1
2
3
| // ATLの使用開始をウィザードで行った場合、stdafx.hでatlbase.hがインクルードされているはず
#define _ATL_DEBUG_INTERFACES
#include <atlbase.h>
|
#importディレクティブのオプション †
オプション | 説明 |
named_guids | このオプションを使用すると、LIBID,CLSID,IID,DIID等の定義がGUID構造体型の変数定義として追加される。 (これらの変数は、::CoCreateInstance 関数の第1引数,第4引数に用いることができる。) |
raw_interfaces_only | デフォルトでは下記の動作。 ・非プロパティのメソッドに対してラッパ関数の定義と実装が作成される。 ・もとのメソッドは"raw_"を冠する名称に変更される。 このオプションを使用すると、上記の処理が抑制される。→tliファイルが作成されない。 |
raw_native_types | デフォルトでは下記の動作。 ・ラッパ関数に対して、COM特有の型であるBSTR型などの替わりにラッパクラスを使用した定義が作成される。 このオプションを使用すると、上記の処理が抑制される。 ※raw_interfaces_only オプションと併用する場合はそもそもラッパ関数が作成されないため、効果が表れない。 |
COMクライアント †
COMコンポーネントをバインドする2種類の方法 †
アーリーバインディングとレイトバインディングの2種類の方法が存在する。
| アーリーバインディング | レイトバインディング |
別名 | 事前バインディング | 実行時バインディング、遅延バインディング |
概要 | 必要なインタフェースのインスタンスを取得し、直接メソッド等をコールする方法。 | IDispatchインタフェースのインスタンスを取得し、間接的にメソッド等をコールする方法。 |
COMコンポ―ネント側で実装する必要があるインタフェース | カスタムインタフェース (IUnknownインタフェース) | デュアルインタフェース (IDispatchインタフェース) |
処理速度 | 速い | 遅い |
COMコンポ―ネントの仕様変更時の影響 | 影響を受けにくい | 影響を受けやすい |
各言語別の使用方法 | C++ | 直接メソッド等をコールする ラッパクラスを作ることが多い | IDispatch::GetIdsOfNames()でメソッド名からdispIDを取得し、IDispatch::Invoke()を介してメソッド等をコールする |
VB | 参照設定で予めプロジェクトに取り込む 変数はそのインタフェース自体の型で宣言して使用する。 ※VBSでは使用不可 | 変数をObject型で定義しておき、CreateObject()で実行時にインタフェースを取得する |
C# | ??? | リフレクションを用いる? |
Java | JAVA-COMブリッジという技術を用いる。JCOM等のツールを使用?詳細不明。 |
実装手順 †
- COMの使用開始
スレッド内でCOMを使う前に、::CoInitialize() をコールする。
- COMオブジェクトの定義、生成
- COMオブジェクトの使用
- COMオブジェクトの解放
- COMの使用終了
スレッド内でCOMを使い終えたら、::CoUninitialize() をコールする。
使用したインタフェースの解放時には参照カウントがデクリメントされるだけで、実際にプロセスが削除されるのはこのタイミング?
COMサーバ(COMコンポーネント) †
レジストリ登録 †
COMコンポーネントは、レジストリに自分のパスとGUIDを登録しておく必要がある。
DLL形式のCOMサーバ †
- 登録
regsvr32 xxx.dll
- 解除
regsvr32 /u xxx.dll
EXE形式のCOMサーバ †
- 登録
xxx.exe /RegServer
- 解除
xxx.exe /UnregServer
タイプライブラリのバイナリへの埋め込み †
VisualStudioでCOMサーバを作成すると、リソースにCLSIDをレジストリ登録するためのレジストリスクリプトが埋め込まれるが、それだけではIIDの登録は行われない。IIDの登録には、COMサーバのビルド時に作られるtlbファイルが必要になる。
tlbファイルは、レジストリ登録時にCOMサーバと同じ場所に置いておくと認識される。また、リソースにtlbファイルを指定することでexeやdllにtlb情報を埋め込むこともできる。その場合はtlbファイルは必要ない。(DLL形式のCOMサーバの場合は、自動的にtlbを埋め込むようにリソースが作られているはず)
リソースへtlbファイルを埋め込むには、rcファイルの「3 TEXTINCLUDE DISCARDABLE」ブロックの末尾に相対パスでtibファイルを指定するコードを追加する。
3 TEXTINCLUDE DISCARDABLE
BEGIN
:
"#ifdef _DEBUG\r\n"
"1 TYPELIB ""Debug\\xxx.tlb""\r\n"
"#else\r\n"
"1 TYPELIB ""Release\\xxx.tlb""\r\n"
"#endif // _DEBUG\r\n"
"\0"
END
※tlbファイルは相対パスを指定する必要があるが、EXE形式のCOMサーバの場合はDebug/Releaseそれぞれのフォルダでtlbファイルが作成されるため、条件分岐させた。
参考リンク1
参考リンク2
VBアプリケーションによる自動登録 †
VBで作成されたアプリケーションは、自身が参照するOCXがレジストリに見つからない場合、起動時にエラーとなる場合がある。しかし、このときDLL/OCXの検索順序?に従ってCOMコンポーネントを検索して見つかると、自動的にレジストリ登録(regsvr32)するようである。そのため、次回からは起動時にエラーとならずアプリケーションを使用することができる。
実装すべきインタフェース †
実装すべき機能 †
レジストリへの自己登録機能など、必要な機能を実装する。
DLL形式のCOMサーバ †
以下の4つの関数をエクスポートする必要がある。
EXE形式のCOMサーバ †
以下のコマンドラインオプションに対応する必要がある。
- /RegServer
- /UnRegServer
- /Embedding
- /Automation
不具合/注意点 †
COMサーバ(EXE形式)にウィザードによって追加されるコード †
VC++6.0でウィザードによって追加されるコードが誤っている箇所があるので注意。
COMサーバ(EXE形式)にウィザードによって追加されるコード
STAでの再入 †
COMサーバーをSTAにしておくことで、マルチスレッドで動作することはなくなる。しかし、別のCOM呼び出しをしている最中に処理が再入されることがあるので注意が必要。
(STAではシングルスレッドで動作させるために、COM呼び出しされたことをウィンドウメッセージとしてキューに溜めている。自身が別のCOM呼び出しを行っている間はメッセージループが回るため、処理が再入される。もちろん、通常のウィンドウメッセージの動作も再入されるということになる。)
参考:http://blogs.wankuma.com/melt/archive/2007/12/13/112869.aspx
SendMessage中のCOM呼び出し †
SendMessageによって別スレッドから送信されたメッセージを処理している間は、EXE(アウトプロセス)のCOMサーバーに対してのCOM呼び出しは失敗して例外が発生する。(デッドロックを防ぐため?)COM呼び出しを行う前に、::InSendMessage() でチェックをすることで防げる。
参考:http://support.microsoft.com/kb/131056/en-us