単純な 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 コンポーネントとクラスファクトリのヘッダーファイル
Table. MIDL で生成されないのでこれから作成するファイル

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) です。


図. 関数呼び出しの流れ
この図の意味はおよそ次のようになります。
  1. Client: クラスファクトリオブジェクトの作成を要求。
  2. COM Library: レジストリから起動すべき DLL を Look up した後、その DLL をロード。
  3. COM Library: DLL が Export している DllGetClassLibrary を呼び出す。
  4. DLL: DllGetClassLibrary はクラスファクトリオブジェクト (ここでは CTestComFactory) を作成。
  5. COM Library: 作成されたクラスファクトリオブジェクトの IClassFactory インターフェイスポインタをクライアントへ返す。
  6. Client: CreateInstance は返された IClassFactory ポインタを用いて、目的のオブジェクト (ここでは CTestCom) の作成を要求。
  7. DLL: クラスファクトリオブジェクトが目的のオブジェクトを作成。
  8. DLL: 作成されたオブジェクト(CTestCom) の要求されたインターフェイス (IA) へのポインタを返す。
  9. 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』(アスキー出版局)

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Web/DB プログラミング徹底解説