単純な COM コンポーネントの作成
このページはかなり昔に書いた資料です。今 COM コンポーネントを作るとしたらこの方法では行わないでしょうが、 基本的な動作を理解するためには役に立つかもしれないと思い、ここに掲載しています。
このページの目標
ここでは非常に単純なコンポーネントの例を調べることを通して、コンポーネントの作成およびコンポーネントクライアントのプログラミングを理解しましょう。テスト用に作成するコンポーネントは、下図のコンポーネントです。
Fig1. ここで作成する COM コンポーネント
IDL によるインターフェイスの定義
COM コンポーネントは IDL (Interface Difinition Language) で始まります。IDL を作成する作業では、リンカは必要ないので Visual C++ 6.0 プロジェクトとしては、 Utility Project を選択して新規作成します。
このプロジェクト内に、テキストファイルを新規作成します。ファイル名: TestCom.IDL TestCom.IDL は次のようになります。
UUIDGEN: 次のようにして雛型を作ります。
uuidgen -i -oTestCom.IDL
cpp_quote("//TestCom") import "unknwn.idl"; //IA interface [ object, uuid(6E345EE3-FAB6-44d6-8C3B-1E25820D8254), helpstring("IA interface"), pointer_default(unique) ] interface IA: IUnknown { HRESULT About(void); } //IB interface [ object, uuid(49A9BF77-0ED9-4ca6-92EC-87C2AD585C26), helpstring("IB interface"), pointer_default(unique) ] interface IB: IUnknown { HRESULT Sum([in] double x, [in] double y, [out, retval] double *s); } //Typelibrary [ uuid(122623DE-24B9-4645-9CB4-075276D21DE9), version(2.0), helpstring("TestCom Library") ] library TestComLibrary { importlib("stdole32.tlb"); [ uuid(BA7BBC17-5DBF-4093-835E-FE1130924951) ] coclass TestCom { [default] interface IA; interface IB; }; };
ビルドします。成功すると、以下のファイルが生成されます。
dlldata.c | プロキシとスタブのコードを含む DLL を実装する C ファイル。 |
---|---|
TestCom.h | インターフェイス宣言を含む C/C++ ヘッダーファイル |
TestCom_i.c | GUID を定義する C ファイル |
TestCom_p.c | プロキシとスタブのコードを実装する C ファイル |
TestCom.tlb | タイプライブラリ |
COM コンポーネントの実装
さて、次は COM コンポーネントの実装です。もう一度、図1 を見てください。
Fig1. ここで作成する COM コンポーネント
また、インターフェイス IA, IB は IDL にて以下のように定義しました。
... interface IA: IUnknown { HRESULT About(void); } ... interface IB: IUnknown { HRESULT Sum([in] double x, [in] double y, [out, retval] double *s); } ...
インターフェイスを定義する C/C++ ヘッダーファイルに関しては、MIDL が自動生成する TestCom.h をそのまま使います。 また、IID、CLSID は同様に MIDL が自動生成する TestCom_i.c を使えます。 ですから、新規に作成しなければならないのは以下のファイルとなります。
TestComImpl.cpp | TestCom コンポーネントの実装。その他、このコンポーネントのクラスファクトリ (コンポーネントを生成するコンポーネント) 、 DLL のエクスポート関数、およびレジストリへの登録用関数 |
---|---|
TestComImpl.h | TestCom コンポーネントとクラスファクトリのヘッダーファイル |
TestCom コンポーネントの実装
定義
クラスの定義ですから、TestComImpl.h ファイル内に記述します。
class TestCom : public IA, public IB { public: //IUnknown メソッド STDMETHODIMP QueryInterface(REFIID riid, LPVOID *ppv); STDMETHODIMP_(ULONG) AddRef(VOID); STDMETHODIMP_(ULONG) Release(VOID); //IA メソッド STDMETHODIMP About(VOID); //IB メソッド STDMETHODIMP Sum(double x, double y, double *s); //コンストラクタ TestCom(); //デストラクタ ~TestCom(); private: //参照カウンタ LONG m_cRef; };
IUnknown
//IUnknown メソッド STDMETHODIMP TestCom::QueryInterface(REFIID riid, LPVOID *ppv) { *ppv = NULL; if((IID_IUnknown == riid) || (IID_IA == riid)) { *ppv = static_cast<IA*>(this); } else if(IID_IB == riid) { *ppv = static_cast<IB*>(this); } else { return E_NOINTERFACE; } reinterpret_cast<IUnknown*>(*ppv)->AddRef(); return S_OK; } STDMETHODIMP_(ULONG) TestCom::AddRef(void) { LockModule(); return InterlockedIncrement(&m_cRef); } STDMETHODIMP_(ULONG) TestCom::Release(void) { LONG res = InterlockedDecrement(&m_cRef); if(0 == res) { UnlockModule(); delete this; } return res; }
IA
//IA メソッド STDMETHODIMP TestCom::About(VOID) { cout << "Copyright(C)1999 K.Oyama" << endl; return S_OK; }
IB
//IB メソッド STDMETHODIMP TestCom::Sum(double x, double y, double *s) { *s = x + y; return S_OK; }
コンストラクタ、およびデストラクタは次のとおり。
//コンストラクタ TestCom::TestCom() : m_cRef(1) { } //デストラクタ TestCom::~TestCom() { }
クラスファクトリの実装
さて、下図をご覧ください。 図の中では、大きく 3
つのモジュールが描かれています。 1つは左上の Client
(コンポーネントを利用する EXE ファイル) であり、2
つ目は左下の COM Library (Windows
システム内に存在)、3つ目は コンポーネントを格納している
DLL (Dynamic Link Library) です。
図. 関数呼び出しの流れ
- Client: クラスファクトリオブジェクトの作成を要求。
- COM Library: レジストリから起動すべき DLL を Look up した後、その DLL をロード。
- COM Library: DLL が Export している DllGetClassLibrary を呼び出す。
- DLL: DllGetClassLibrary はクラスファクトリオブジェクト (ここでは CTestComFactory) を作成。
- COM Library: 作成されたクラスファクトリオブジェクトの IClassFactory インターフェイスポインタをクライアントへ返す。
- Client: CreateInstance は返された IClassFactory ポインタを用いて、目的のオブジェクト (ここでは CTestCom) の作成を要求。
- DLL: クラスファクトリオブジェクトが目的のオブジェクトを作成。
- DLL: 作成されたオブジェクト(CTestCom) の要求されたインターフェイス (IA) へのポインタを返す。
- Client: 取得したインターフェイスポインタを利用する。
定義
class TestComFactory : public IClassFactory { public: //IUnknown メソッド STDMETHODIMP QueryInterface(REFIID riid, LPVOID *ppv); STDMETHODIMP_(ULONG) AddRef(VOID); STDMETHODIMP_(ULONG) Release(VOID); //IClassFactory メソッド STDMETHODIMP CreateInstance(IUnknown* pUnkOuter, REFIID riid, LPVOID *ppv); STDMETHODIMP LockServer(BOOL bLock); //コンストラクタ TestComFactory(); //デストラクタ ~TestComFactory(); private: //参照カウンタ LONG m_cRef; };
IUnknown
STDMETHODIMP TestComFactory::QueryInterface(REFIID riid, LPVOID *ppv) { *ppv = NULL; if((IID_IUnknown == riid) || (IID_IClassFactory == riid)) { *ppv = static_cast<IClassFactory*>(this); } else { return E_NOINTERFACE; } reinterpret_cast<IUnknown*>(*ppv)->AddRef(); return S_OK; } STDMETHODIMP_(ULONG) TestComFactory::AddRef(VOID) { return InterlockedIncrement(&m_cRef); } STDMETHODIMP_(ULONG) TestComFactory::Release(VOID) { return InterlockedDecrement(&m_cRef); }
IClassFactory
IClassFactory::CreateInstance にて、実際に new で、C++ インスタンスを生成しています。
//IClassFactory メソッド
STDMETHODIMP TestComFactory::CreateInstance(LPUNKNOWN pUnkOuter,
REFIID riid,
LPVOID *ppv)
{
if(NULL != pUnkOuter)
{
return CLASS_E_NOAGGREGATION;
}
TestCom* pTestCom = new TestCom;
if(NULL == pTestCom)
{
return E_OUTOFMEMORY;
}
HRESULT hr = pTestCom->QueryInterface(riid, ppv);
pTestCom->Release();
return hr;
}
STDMETHODIMP TestComFactory::LockServer(BOOL bLock)
{
if(bLock)
{
LockModule();
}
else
{
UnlockModule();
}
return S_OK;
}
TestComFactory::TestComFactory() : m_cRef(1)
{
}
TestComFactory::~TestComFactory()
{
}
以上のように IClassFactory を実装
STDAPI DllGetClassObject(REFCLSID clsid, REFIID iid, LPVOID *ppv) { if(CLSID_TestCom != clsid) { return CLASS_E_CLASSNOTAVAILABLE; } TestComFactory* pFactory = new TestComFactory; if(NULL == pFactory) { return E_OUTOFMEMORY; } //必要なインターフェイスを取得 HRESULT hr = pFactory->QueryInterface(iid, ppv); pFactory->Release(); return hr; }
レジストリへの登録
static HINSTANCE g_hModule = NULL; const char *g_RegTable[][3] = { {"CLSID\\{BA7BBC17-5DBF-4093-835E-FE1130924951}", 0, "TestCom"}, {"CLSID\\{BA7BBC17-5DBF-4093-835E-FE1130924951}\\InprocServer32", 0, (const char*)-1}, {"CLSID\\{BA7BBC17-5DBF-4093-835E-FE1130924951}\\ProgID", 0, "OyamaCom.TestCom.1"}, {"OyamaCom.TestCom.1", 0, "TestCom"}, {"OyamaCom.TestCom.1\\CLSID", 0, "{BA7BBC17-5DBF-4093-835E-FE1130924951}"} }; BOOL APIENTRY DllMain(HINSTANCE hModule, DWORD dwReason, void* lpReserved) { if(DLL_PROCESS_ATTACH == dwReason) { g_hModule = hModule; } return TRUE; } STDAPI DllUnregisterServer(void) { HRESULT hr = S_OK; int nEntries = sizeof(g_RegTable)/sizeof(*g_RegTable); for(int i=nEntries-1;i>=0;i--) { const char *pszKeyName = g_RegTable[i][0]; long err = RegDeleteKeyA(HKEY_CLASSES_ROOT, pszKeyName); if(ERROR_SUCCESS!=err) { hr = S_FALSE; } } return hr; } STDAPI DllRegisterServer(void) { HRESULT hr = S_OK; char szFileName[MAX_PATH]; GetModuleFileNameA(g_hModule, szFileName, MAX_PATH); int nEntries = sizeof(g_RegTable)/sizeof(*g_RegTable); for(int i=0; SUCCEEDED(hr) && i<nEntries; i++) { const char *pszKeyName = g_RegTable[i][0]; const char *pszValueName = g_RegTable[i][1]; const char *pszValue = g_RegTable[i][2]; if(pszValue == (const char*)-1) { pszValue = szFileName; } HKEY hkey; long err = RegCreateKeyA(HKEY_CLASSES_ROOT, pszKeyName, &hkey); if(ERROR_SUCCESS == err) { err = RegSetValueExA(hkey, pszValueName, 0, REG_SZ, (const BYTE*)pszValue, (strlen(pszValue) + 1)); RegCloseKey(hkey); } if(ERROR_SUCCESS != err) { DllUnregisterServer(); hr = SELFREG_E_CLASS; } } return hr; }
サーバーの寿命管理
DLL に COM+ Server を実装する場合は、2 つの寿命管理が必要です。 一つは、COM+ オブジェクト (インスタンス) をいつアンロードすべきか、という管理と、 もう一つは、いつ DLL をアンロードすべきか、というものです。
COM+ オブジェクトの管理は、COM+ サーバーが持つ参照カウントと、IUnknown::AddRef と IUnknown::Release で行います。 一方、DLL のアンロードの方はひと工夫必要です。
//グローバルスコープのモジュールロックカウンタ static LONG g_cLocks = 0; //*********************************************** // ロックカウンタ関数 //*********************************************** void LockModule(void) { InterlockedIncrement(&g_cLocks); } void UnlockModule(void) { InterlockedDecrement(&g_cLocks); }
このような LockModule() と UnlockModule() を用意しておいて、STDMETHODIMP_(ULONG) TestCom::AddRef(void) と STDMETHODIMP_(ULONG) TestCom::Release(void) で呼び出しています。
/* 不要なDLLをアンロードしたいクライアントは、COM 関数 CoFreeUnusedLibraries を呼び出します。この関数は、DLL のDllCanUnloadNow を呼び出すことで、アン ロード可能かどうかを問い合わせます。 */ STDAPI DllCanUnloadNow(void) { return 0 == g_cLocks ? S_OK : S_FALSE; }
クライアントの実装例
#include <iostream.h> #include <objbase.h> #include "TestComImpl.h" int main() { HRESULT hr; hr = CoInitialize(NULL); //他のスレッドが入れないプライベートアパートを作成しアパートに入る //= CoInitializeEx(0, COINIT_APARTMENTTHREADED); if(FAILED(hr)) { return 1; } IA* pIA = NULL; hr = CoCreateInstance(CLSID_TestCom, NULL, CLSCTX_INPROC_SERVER, IID_IA, (void**)&pIA); if(SUCCEEDED(hr)) { pIA->About(); IB* pIB = NULL; hr = pIA->QueryInterface(IID_IB, (void**)&pIB); if(SUCCEEDED(hr)) { double s; pIB->Sum(5., 10., &s); cout << "IB::Sum = " << s << endl; pIB->Release(); } pIA->Release(); } else { cout << "ERROR: CoCreateInstance()" << endl; } CoUninitialize(); // アパートから出る return 0; }
参考資料
- Don Box 著, 長尾 高弘訳『Essential COM』(アスキー出版局)
- Don Box, Keith Brown, Tim Ewald, Chris Sells著, (株)ロングテール/長尾 高弘訳『Effective COM』
- Dale Rogerson 著, バウン グローバル(株)訳『Inside COM』(アスキー出版局)