CX5 SOFTWARE

COM (Component Object Model)

2010.3.22


目次

1. はじめに
2. COMの概念
3. Visual Studio 2008で新規プロジェクトの作成
4. COM IDLの追加と編集
5. クラスの作成
6. COMクラスの実装
6.1. IUnknownインターフェイスの実装
6.2. ISampleCOMインターフェイスの実装
7. クラスファクトリの実装
7.1. クラスファクトリのためのクラスの宣言
7.2. クラスファクトリとDLLの関係
7.3. DLL関連の関数の実装
7.4. クラスファクトリの実装
8. COMのレジストリへの登録
9. モジュール定義ファイル(def)の定義
10. クライアントの作成とテスト
10.1. クライアントの作成
10.2. テスト

第1章 はじめに

古いが未だに使われ続けるCOM (Component Object Model)ですが、今の最新の環境(Visual Studio 2008)で開発しようとすると、世の中にある情報が古く結構混乱します。また、COMの作成の仕方としてもATLを使う方法とMFCを使う方法、さらにDLL形式とEXE形式がありますが、これもさらに混乱する原因になっています。

ですので、ここでは、ATLやMFCを使わずに、Visual Studio 2008上でDLL形式のCOMを作成する方法について説明します。

第2章 COMの概念

まず、COMの全体像を下図に示します。

もし、以前にCORBA、EJB、RMIやWebサービス等を開発したことがあるのなら、COMの構造はそれほど難しくありません。

まず、COM IDLですが、COMのインターフェイスを定義するテキストファイル(拡張子.idl)です。CORBAですと同じようにIDLがあります。EJBやRMIの場合、リモートインターフェイスと呼ばれるinterfaceがありますが、それらがIDLと同じ役割をします。Webサービスの場合はWSDLがCOM IDLと同じ役割を持ちます。

次に、COM本体とクラスファクトリがDLL内に格納されます。COMとクラスファクトリはC++のクラスとなります。COMは、実際のCOMの機能を提供するC++のクラスですので、理解しやすいでしょう。対して、クラスファクトリですが、もしEJBを開発したことがあればホームインターフェイスであると考えればよいです。クラスファクトリとは、いわゆるデザインパターンにおけるファクトリパターンのように、COMインスタンスを生成するためのファクトリとなるC++のクラスです。

最後に、レジストリですが、これはWindowsのレジストリそのものです。CORBA、EJB、RMIではネームサービスを提供する機能がありますが、それと同等の機能を提供します(EJBでのJNDI、RMIでのRMIレジストリ)。クライアントがCOMを呼び出すためには、DLLのパスを知る必要があります。でも、クライアント内にDLLのパスを埋め込んだのでは、DLLのパスが異なる環境で動作しなくなってしまいます。このため、COMにCLSIDというIDを割り当て、そのCLSIDとDLLのパスの関係をレジストリに登録しておきます。クライアントは、CLSIDを指定してDLLを取得し、さらにCOMを取得します。

COM IDLとCOMの関係は、インターフェイスと実装の関係ですからわかりやすいです。レジストリを使っているところは、レジストリを破壊しないか心配という点と、登録と登録解除の方法を考えなければいけないところを除けばそれほど難しくありません。少しわかりにくいのが、クラスファクトリです。この点は、後ほど説明します。

第3章 Visual Studio 2008で新規プロジェクトの作成

COMはDLLに格納しますので、DLLプロジェクトをVisual Studio 2008で作成します。

Visual Studio 2008を起動し、「ファイル(F)」-「新規作成(N)」-「プロジェクト(P)...」を選択します。表示されたダイアログで、「Visual C++」-「Win32」の「Win32コンソールアプリケーション」を選択します。さらに、プロジェクト名を「COM_SampleDLL」、ソリューション名を「COM_Sample」と入力し、「OK」ボタンを押します。

「次へ」を押します。

「DLL」を選択し、「完了」を押します。

結果として、以下のようなプロジェクトが作成されます。

第4章 COM IDLの追加と編集

プロジェクト「COM_SampleDLL」を選択後、右クリックし「追加(D)」-「新しい項目(W)...」を選択します。

MIDLファイルを選択し、ファイル名として「ISample_COM.idl」を指定します。「追加」ボタンを押します。

新しく作成された「ISample_COM.idl」を開きます。以下のような内容のはずです。

import "oaidl.idl";
import "ocidl.idl";

「ISample_COM.idl」の内容を以下のように変更します。

import "oaidl.idl";
import "ocidl.idl";
import "unknwn.idl";

[uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), object] ← xxx...の部分は変更してください
interface ISampleCOM : IUnknown
{
    [helpstring("Print a message")] HRESULT PrintMessage(void);

    [propget, 
     helpstring("Get property1.")] HRESULT Property1(
         [out, retval] int* ReturnVal); 

    [propput, 
     helpstring("Set property1.")] HRESULT Property1(
         [in] int Value);
}

[uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)] ← xxx...の部分は変更してください
coclass SampleCOM
{
    interface ISampleCOM;
}

ここで注意するのがuuidです。上記では「xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx」となっていますが、これをguidgenというツールを使って、本当の値を生成し置き換える必要があります。guidgenを起動するには、スタートメニューのVisual Studio 2008より「Visual Studio 2008コマンドプロンプト」を選択し、コマンドプロンプトを起動します。次に、guidgenと入力してツールを起動します。ツールが起動したら、「4.Registry Format」を選択し、「New GUID」を押した後に「Copy」を押し「ISampleCOM.idl」へペーストします。uuidは2箇所にありますので、それぞれで別の値を指定します。

IDLの内容ですが、最初のimportはCやC++における#includeと同じで、COMで必要となる定義を読み込んでいます。常にこのように書くと思えばよいです。

import "oaidl.idl";
import "ocidl.idl";
import "unknwn.idl";

次にインターフェイスISampleCOMの定義をしています。ISampleCOMはIUnknownというインターフェイスを継承しています。

interface ISampleCOM : IUnknown
{
    ... 
}

IUnknownは、COMインターフェイスに必須な3つのメソッドを定義しています。COMインターフェイスは必ずIUnknownを実装しなければなりません。IUnknownはunknwn.idlで以下のように定義されています。QueryInterface、AddRef、ReleaseはCOMでは非常に重要なメソッドです。後で、実際に実装します。

interface IUnknown
{
    HRESULT QueryInterface(
        [in] REFIID riid,
        [out, iid_is(riid)] void **ppvObject);
    ULONG AddRef();
    ULONG Release();
}

次に、1つのメソッドと1つのプロパティを定義しています。メソッドPrintMessageは以下のように定義しています。まずhelpstringですが、これはこのメソッドの説明です。必須ではありません。次に、実際のメソッドを定義していますが、メソッドの戻り値は常にHRESULTにする必要があります。HRESULTには、処理が成功したかどうかが返されます。

     [helpstring("Print a message")] HRESULT PrintMessage(void);

次に、1つのプロパティProperty1を定義するために2つのメソッドを定義しています。

     [propget, 
     helpstring("Get property1.")] HRESULT Property1(
         [out, retval] int* ReturnVal); 

    [propput, 
     helpstring("Set property1.")] HRESULT Property1(
         [in] int Value);

最後に、SampleCOMというコクラス(coclass)を定義しています。コクラスとは、あるCOMインターフェイスを実装したクラスとなります。C#やJavaにおける、インターフェイスとクラスの関係と思えばよいでしょう。このようにインターフェイスとコクラスと分けているのは、あるインターフェイスを複数のコクラスで実装できるようにするためです。つまり、Aというインターフェイスを定義して、それを実装したA1とA2というコクラスを作成することができます。ただし、今回の場合は1つのインターフェイスと1つのコクラスしかありませんので、あまりこの後意識する必要はありません。

coclass SampleCOM
{
    interface ISampleCOM;
}

ここで、一度ビルドを実行してください。その後、下図の「すべてのファイルを表示」を押し、プロジェクトフォルダ内のすべてのファイルを表示させてみてください。

すると、以下の4つのファイルが新たに作成されているのがわかります。これは、ISampleCOM.idlから生成されたものです。ただし、この中で今回使うのはISampleCOM_h.hとISampleCOM_i.cの2つです。

  • dlldata.c

  • ISampleCOM_h.h

  • ISampleCOM_i.c

  • ISampleCOM_p.c

ISampleCOM_h.hを開くと、以下のようにISampleCOMがC++のクラスとして定義されているのが確認できます。すべてのメソッドが純粋仮想関数として定義されているのがわかります。

    ISampleCOM : public IUnknown
    {
    public:
        virtual /* [helpstring] */ HRESULT STDMETHODCALLTYPE PrintMessage( void) = 0;
        
        virtual /* [helpstring][propget] */ HRESULT STDMETHODCALLTYPE get_Property1( 
            /* [retval][out] */ int *ReturnVal) = 0;
        
        virtual /* [helpstring][propput] */ HRESULT STDMETHODCALLTYPE put_Property1( 
            /* [in] */ int Value) = 0;
        
    };

ISampleCOM_i.cは主にGUIDの定義が行われています。さきほどCOM IDLに設定した二つのGUIDが以下のように定義されています。IID_ISampleCOMは、インターフェイスID(Interface ID)を定義しています。CLSID_SampleCOMは、クラスIDを定義しています。実装において、この値を参照しますので、ISampleCOM_i.cをどこか一箇所でインクルードする必要があります。

MIDL_DEFINE_GUID(IID, IID_ISampleCOM,0x6F6B320A,...);
MIDL_DEFINE_GUID(CLSID, CLSID_SampleCOM,0x11B9861D,...);

第5章 クラスの作成

ISampleCOM_h.hに定義されていたISampleCOMを継承したCSampleCOMというクラスを定義します。SampleCOM.hというファイルを作成し、以下のようにCSampleCOMというクラスを定義します。

#pragma once

#include "ISampleCOM_h.h"

class CSampleCOM : public ISampleCOM
{
protected:
    LONG m_cRef;
    int property1;
public:
    CSampleCOM(void);
    ~CSampleCOM(void);

    STDMETHODIMP QueryInterface(REFIID, void**);
    STDMETHODIMP_(ULONG) AddRef();
    STDMETHODIMP_(ULONG) Release();

    STDMETHODIMP PrintMessage(void);
    STDMETHODIMP get_Property1(int *ReturnVal);
    STDMETHODIMP put_Property1(int Value);
};

最初に、IDLから生成されたISampleCOM_h.hをインクルードしています。そしてその中で定義されているISampleCOMを継承したCSampleCOMを定義しています。

class CSampleCOM : public ISampleCOM
{
    ...
};

コンストラクタ、デストラクタに続いてIUnknownで定義されていた3つのメソッド、そしてIDL内のISampleCOMで定義した3つのメソッドを定義しています。ここでSTDMETHODIMPというマクロを使っています。これは、COMのメソッドが仮想関数かつHRESULTを戻りとして返すということで、それを定義したマクロです。

第6章 COMクラスの実装

6.1. IUnknownインターフェイスの実装

さて、いよいよCOMを実装します。行うことは、さきほど定義したCSampleCOMクラスのメソッドを一つずつ実装していきます。SampleCOM.cppを作成してください。

まずは、コンストラクタです。CSampleCOMクラス定義にて定義したm_cRefを初期化しています。このm_cRefはこのCOMオブジェクトの参照を管理する、参照カウンタです。

CSampleCOM::CSampleCOM(void)
    :m_cRef(0L)
{
}

次にデストラクタですが、こちらは特に何もしません。

CSampleCOM::~CSampleCOM(void)
{
}

IUnknownで定義されているAddRefを実装します。このAddRefとReleaseはペアになって、COMへの参照を管理します。つまり、COMクライアント(COMを使う側)は、COMの参照を取得する度にAddRefを呼び出し、参照を削除した場合にReleaseを呼ばなければならないというルールになっています。ですので、AddRefは単純に以下のように先程の参照カウンタm_cRefを+1します。

STDMETHODIMP_(ULONG) CSampleCOM::AddRef()
{
    return ++m_cRef;
}

ただ、この実装には問題があります。それはマルチスレッドの場合に正しく動作しません。そこで、Win32 APIのInterlockedIncrementという関数を用いて+1します。

STDMETHODIMP_(ULONG) CSampleCOM::AddRef()
{
    return InterlockedIncrement(&m_cRef);
}

次にAddRefの対となるReleaseを実装します。AddRefと同じようにInterlockedDecrementを用いて-1しますが、加えて、参照カウンタが0になった場合、つまり誰にも参照されていなかった場合、deleteで削除します。deleteで削除していることから、COMオブジェクトを作成するときは必ずnewを用いる必要があります。

STDMETHODIMP_(ULONG) CSampleCOM::Release()
{
    if(InterlockedDecrement(&m_cRef)) {
        return m_cRef;
    }
    delete this;
    return 0L;
}

次に、IUnknownで定義されている最後のメソッドQueryInterfaceを実装しましょう。このQueryInterfaceですが、COMオブジェクトを特定のインターフェイスにキャストしていると思えばよいでしょう。引数にIID(インターフェイスID)とCOMオブジェクトを渡し、そのCOMインターフェイスを実装しているかを知ることができます。

でも、なぜこのような面倒なことをするのでしょうか。普通ならばCOMオブジェクトを生成する際にどのインターフェイスを実装しているかはわかっているのではないでしょうか。であれば、C++のキャストでキャストしてはまずいのでしょうか。これはCOMがC++と異なり、オブジェクト(バイナリ)レベルのコンポーネントモデルであることから来ています。つまり、COMの世界では、COMオブジェクトがどのインターフェイスを実装しているかは、ソースコードから得られる知識から勝手に想定してはいけません。そのCOMオブジェクトにQueryInterfaceで確認する必要があります。

QueryInterfaceの実装は、以下のようになります。まず、引数で渡されたインターフェイスID(IID)と、ISampleCOMのIIDと比較しています。もし等しい場合は、ISampleCOMにキャストしています。次に、AddRefを呼び出しています。これは、QueryInterfaceに成功した場合、ISampleCOMの参照を渡すことになるので、参照カウンタを+1にするためです。

STDMETHODIMP CSampleCOM::QueryInterface(REFIID riid, void** ppv)
{
    *ppv = NULL;

    if(IsEqualIID(riid, IID_ISampleCOM) || IsEqualIID(riid, IID_IUnknown)) {
        *ppv = (ISampleCOM*)this;
    }

    if(*ppv) {
        AddRef();
        return S_OK;
    }
    return E_NOINTERFACE;
}

ここで、ISampleCOMに変換できた場合は、戻り値としてS_OKを返し、失敗した場合はE_NOINTERFACEを返しています。QueryInterfaceはHRESULT型を返します。そして、S_OKとE_NOINTERFACEは、あらかじめ定義されているHRESULT型の値です。

6.2. ISampleCOMインターフェイスの実装

では、今回のCOMの機能を提供するメソッドを実装しましょう。まずは、PrintMessageメソッドです。こちらは非常に単純で、Helloとメッセージをコンソールに出力して、S_OKを返します。

STDMETHODIMP CSampleCOM::PrintMessage()
{
    _tprintf(_T("Hello\n"));
    return S_OK;
}

次に、プロパティProperty1のgetterです。引数で渡された戻り値のポインタ(ReturnVal)に現在の値(property1)を設定しています。

STDMETHODIMP CSampleCOM::get_Property1(int *ReturnVal)
{
	*ReturnVal = property1;
	return S_OK;
}

プロパティProperty1のsetterは以下のように定義します。引数(Value)で与えられた新しい値を現在の値(property1)に設定します。

STDMETHODIMP CSampleCOM::put_Property1(int Value)
{
	property1 = Value;
	return S_OK;
}

第7章 クラスファクトリの実装

7.1. クラスファクトリのためのクラスの宣言

さきほども説明したようにクラスファクトリはCOMオブジェクトを生成するためのファクトリです。クラスファクトリはIClassFactoryという特別なクラスを継承しなければなりません。IClassFactoryはUnknwn.hで以下のように定義されています。

IClassFactory : public IUnknown
{
public:
    virtual HRESULT STDMETHODCALLTYPE CreateInstance( 
        IUnknown *pUnkOuter,
        REFIID riid,
        void **ppvObject) = 0;
        
    virtual HRESULT STDMETHODCALLTYPE LockServer( 
        BOOL fLock) = 0;
};

CreateInstanceとLockServerというメソッドが定義されています。さらに、IClassFactoryはIUnknownを継承していますので、AddRef、Release、QueryInterfaceも実装する必要があります。

IClassFactoryとIUnknownで定義される5つのメソッドを定義したクラスファクトリCSampleCOMFactoryを以下のように定義します。以下の定義を、さきほど作成したSampleCOM.hに追加してください。

class CSampleCOMFactory : public IClassFactory
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppv);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();
	STDMETHODIMP CreateInstance(IUnknown *punkOuter, REFIID riid, void **ppv);
	STDMETHODIMP LockServer(BOOL fLock);
};

7.2. クラスファクトリとDLLの関係

これから実際にクラスファクトリを実装しますが、その前に、クラスファクトリとDLLの関係を説明します。まず、クライアントはどのようにしてクラスファクトリを取得するかを考えてみましょう。クライアントがどのようにしてCOMオブジェクトを取得するかを示したのが以下の図です。

DLLの探索、クラスファクトリの生成、COMオブジェクトの生成という3つの手順で行われますが、ここではクラスファクトリの生成に関係する③と④に注目しましょう。DLLが見つかるとDllGetClassObjectが呼び出され、その中でクラスファクトリが生成されて返されます。このため、われわれはDllGetClassObjectを実装し、その中でCSampleCOMFactoryのインスタンスを生成する必要があります。このような仕様から、二つの点に注意する必要があります。

一つ目は、クラスファクトリの生成と削除にはクライアントは関わらないということです。このことは、参照カウンタは不要で、AddRef/Releaseは単純な実装でよいことになります。

二つ目は、DLLとクラスファクトリは密接に連携する必要があり、このため、DLLがアンロード可能かどうかを返すDllCanUnloadNowも実装する必要があります。このために、DLLの参照カウンタを管理し、0の場合にのみDLLがアンロードされるようにDllCanUnloadNowを実装する必要があります。

7.3. DLL関連の関数の実装

最初にプロジェクトを作成した際に「DLLプロジェクト」を選択しているため、Visual Studioは自動的にdllmain.cppという以下の内容のファイルを自動作成しています。

// dllmain.cpp : DLL アプリケーションのエントリ ポイントを定義します。
#include "stdafx.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
					 )
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}

DllMainは以下のイベントが発生した場合に呼び出されます。

  • DLL_PROCESS_ATTACH - プロセスがDLLをロードした

  • DLL_THREAD_ATTACH - DLLをロードしたプロセスがスレッドを作成しようとしている

  • DLL_THREAD_DETACH - DLLをロードしたプロセスがスレッドを終了しようとしている

  • DLL_PROCESS_DETACH - プロセスがDLLをアンロードした

今回の場合、後でDLLのパスを取得するために使うために、DLL_PROCESS_ATTACHにおいてDLLモジュールのハンドルを念のため変数に保存しておきます。このため、以下のようにDLLモジュールのハンドルを格納する変数g_hmodThisDllの定義の追加と、その変数へ引数で渡されたハンドルを格納する処理をDllMainに追加します。

// dllmain.cpp : DLL アプリケーションのエントリ ポイントを定義します。
#include "stdafx.h"

HMODULE g_hmodThisDll = NULL;

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        g_hmodThisDll = hModule;
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

次に、DLLの参照カウンタとそれに関連した関数を実装します。最初に参照カウンタg_cObjsを宣言します。次に、参照カウンタを+1するDllAddRefと-1するDllReleaseを実装しています。最後にDllCanUnloadNowを実装しています。これはその名の通り、今、DLLをアンロード可能かどうかを返します。そのため、参照カウンタが0かどうかで判定しています。

LONG g_cObjs = 0;

void DllAddRef()
{
	InterlockedIncrement(&g_cObjs);
}

void DllRelease()
{
	InterlockedDecrement(&g_cObjs);
}

STDAPI DllCanUnloadNow(void)
{
    return (g_cObjs == 0 ? S_OK : S_FALSE);
}

7.4. クラスファクトリの実装

クラスファクトリの実装ですが、dllmain.cppに記述することとします。というのも、さきほど定義した関数を呼び出す必要があるからです。このため、最初に、dllmain.cppの先頭にSampleCOM.hの#includeを追加しましょう。加えて、ISampleCOM_i.cの#includeも追加します。これはGUIDが格納された変数が定義されています。

// dllmain.cpp : DLL アプリケーションのエントリ ポイントを定義します。
#include "stdafx.h"
#include "SampleCOM.h"
#include "ISampleCOM_i.c"

QueryInterfaceは、ほぼCOMクラスと同じですが、引数で渡されたインターフェイスID(REFIID)がIID_IUnknownかIID_IClassFactoryのどちらかと等しければIClassFactoryにキャストして返します。

STDMETHODIMP CSampleCOMFactory::QueryInterface(REFIID riid, void **ppv)
{
    *ppv = NULL;
    if(IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IClassFactory)) {
        *ppv = (IClassFactory*)this;
        AddRef();
        return S_OK;
    }
    return E_NOINTERFACE;
}

AddRef、Releaseはクライアントから呼ばれることはないため、以下のような実装とします。

STDMETHODIMP_(ULONG) CSampleCOMFactory::AddRef()
{
    return 2;
}

STDMETHODIMP_(ULONG) CSampleCOMFactory::Release()
{
    return 1;
}

ここまでがIUnknownで定義されていたメソッドです。

次にIClassFactoryで実装されているCreateInstanceとLockServerを実装します。CreateInstanceですが、その名前のとおりインスタンスを生成するためのメソッドです。第一引数pUnkOuterはアグリゲーションという機能のために使うためのものですが、今回はサポートしませんのでCLASS_E_NOAGGREGATIONを常に返しています。その後はCSampleCOMをnew演算子で生成後、QueryInterfaceでキャストしています。

STDMETHODIMP CSampleCOMFactory::CreateInstance(IUnknown* pUnkOuter, REFIID riid, void **ppvObj)
{
    *ppvObj = NULL;
    if(pUnkOuter) {
        return CLASS_E_NOAGGREGATION;
    }
    CSampleCOM *pSampleCOM = new CSampleCOM();
    if(NULL == pSampleCOM) {
        return E_OUTOFMEMORY;
    }
    return pSampleCOM->QueryInterface(riid, ppvObj);
}

LockServerですが、クラスファクトリが開放されないようにロックしたり、ロックを解除したりするためのものです。ですのでDLLの参照カウンタを増減するように実装します。

STDMETHODIMP CSampleCOMFactory::LockServer(BOOL fLock)
{
    if(fLock) {
        DllAddRef();
    }
    else {
        DllRelease();
    }
    return S_OK;
}

第8章 COMのレジストリへの登録

すでに説明したようにCOMはレジストリにて管理されています。ここでは、レジストリへの設定と解除のための処理を実装します。COMの登録と解除のための関数としてDllRegisterServerとDllUnregisterServerを用意すると、regsvr32というコマンドでCOMの登録と解除が行えるようになります。それぞれの実装は以下の通りとなります。

BOOL SetRegKeyValue(LPTSTR pszKey, LPTSTR pszSubkey, LPTSTR pszName, LPTSTR pszValue)
{
    BOOL bOK = FALSE;
    TCHAR szKey[255];

    lstrcpy(szKey, pszKey);

    if(NULL != pszSubkey) {
        lstrcat(szKey, TEXT("\\"));
        lstrcat(szKey, pszSubkey);
    }

    HKEY hKey;
    LONG ec = RegCreateKeyEx(
        HKEY_CLASSES_ROOT, szKey, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &hKey, NULL);
    if(ERROR_SUCCESS == ec) {
        if(NULL != pszValue) {
            ec = RegSetValueEx(
                hKey, pszName, 0, REG_SZ, (BYTE *)pszValue, (lstrlen(pszValue) + 1) * sizeof(TCHAR));
        }
        if(ERROR_SUCCESS == ec) {
            bOK = TRUE;
        }
        RegCloseKey(hKey);
    }

    return bOK;
}

STDAPI DllRegisterServer(void)
{
    TCHAR szModulePath[MAX_PATH];

    GetModuleFileName(
        g_hmodThisDll, szModulePath, sizeof(szModulePath) / sizeof(TCHAR));
    SetRegKeyValue(
        TEXT("COM_Sample.SampleCOM"), 
        NULL, NULL, TEXT("COM_Sample"));
    SetRegKeyValue(
        TEXT("COM_Sample.SampleCOM"), 
        TEXT("CLSID"), NULL, TEXT("{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}"));
    SetRegKeyValue(
        TEXT("CLSID\\{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}"), 
        NULL, NULL, TEXT("COM_Sample"));
    SetRegKeyValue(
        TEXT("CLSID\\{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}"), 
        TEXT("InprocServer32"), NULL, szModulePath);
    SetRegKeyValue(
        TEXT("CLSID\\{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}\\InprocServer32"), 
        NULL, TEXT("ThreadingModel"), TEXT("Apartment"));
    SetRegKeyValue(
        TEXT("CLSID\\{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}"), 
        TEXT("ProgID"), NULL, TEXT("COM_Sample.SampleCOM"));
    return S_OK;
}

STDAPI DllUnregisterServer(void)
{
    RegDeleteKey(HKEY_CLASSES_ROOT, TEXT("COM_Sample.SampleCOM\\CLSID"));
    RegDeleteKey(HKEY_CLASSES_ROOT, TEXT("COM_Sample.SampleCOM"));
    RegDeleteKey(HKEY_CLASSES_ROOT, TEXT("CLSID\\{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}\\InprocServer32"));
    RegDeleteKey(HKEY_CLASSES_ROOT, TEXT("CLSID\\{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}\\ProgID"));
    RegDeleteKey(HKEY_CLASSES_ROOT, TEXT("CLSID\\{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}"));
    return S_OK;
}

xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxの部分は、guidgenで生成しCOM IDLファイル内でコクラスSampleCOMに設定した値を指定してください。注意として、インターフェイスID(ISampleCOM)ではなく、コクラスのクラスID(SampleCOM)となります。

DllRegisterServerの最初でGetModuleFileNameを用いてDLLへのパスを取得し、そのパスをレジストリに登録しています。

第9章 モジュール定義ファイル(def)の定義

dllmain.cpp内で定義した関数のいくつかは外部へ公開する必要があります。この定義を行うのがモジュール定義ファイル(def)です。Visual Studioの「新しい項目の追加」でモジュール定義ファイルを"COM_SampleDLL.def"というファイル名で生成してください。そして、COM_SampleDLL.defの内容を以下のように書き換えてください。

LIBRARY	"COM_SampleDLL"

EXPORTS
    DllCanUnloadNow	@1
    DllGetClassObject	@2
    DllRegisterServer	@4
    DllUnregisterServer	@5

以上でCOMとしての実装は完了です。

第10章 クライアントの作成とテスト

10.1. クライアントの作成

クライアントを作成するためVisual StudioでWin32コンソールアプリケーションプロジェクトを同じソリューションの下に作成します。生成されたcppファイルを以下のように変更します。

#include "stdafx.h"
#include "ISampleCOM_h.h"
#include "ISampleCOM_i.c"

int _tmain(int argc, _TCHAR* argv[])
{
    HRESULT hr = CoInitialize(NULL);

    ISampleCOM *sampleCOM;
    hr = CoCreateInstance(CLSID_SampleCOM, NULL, CLSCTX_ALL, IID_ISampleCOM, 
         reinterpret_cast<void**>(static_cast<ISampleCOM**>(&sampleCOM)));
    if(SUCCEEDED(hr)) {
        // メソッドのテスト
        sampleCOM->PrintMessage();

        // プロパティのテスト
        sampleCOM->put_Property1(100);
        int value = 0;
        sampleCOM->get_Property1(&value);
        _tprintf(_T("Property1 = %d\n"), value);

        sampleCOM->Release();
    }
    CoUninitialize();
    return 0;
}

注意として、上記のソースコードの先頭で"ISampleCOM_h.h"と"ISampleCOM_i.c"をインクルードしています。しかし、これらのファイルはCOMのソースが格納されたCOM_SampleDLLプロジェクトにあります。このため、このままではファイルが見つからないためにエラーとなってしまいます。これを解消するために、プロジェクトのプロパティにて、「C/C++」-「全般」-「追加のインクルードディレクトリ」にCOM_SampleDLLプロジェクトを追加しています。

最初のCoInitializeは、COMを使用する前に呼び出すCOM環境を初期化するWindows APIです。同様に、最後のCoUninitializeはCOMの使用を終了する際に呼び出すWindows APIです。

最初にCoCreateInstanceを呼び出しCOMオブジェクトを取得しています。CoCreateInstanceには、クラスID(CLSID_SampleCOM)、インターフェイスID(IID_ISampleCOM)を渡し、最後の引数(ISampleCOMのポインタ)に取得されたCOMオブジェクトが返されます。

COMオブジェクトの利用が終わったらReleaseメソッドを呼び出します。

10.2. テスト

では実際に動作させてみましょう。まず最初に、COMをレジストリに登録する必要があります。これにはregsvr32というコマンドを用います。まず、コマンドプロンプトを起動してください。この際、Vista/Windows 7の場合は、管理者権限で起動してください。というのも、レジストリへのアクセスは管理者権限が必要となります。コマンドプロンプトを起動したら、COM_SampleDLL.dllがあるフォルダへ移動し以下のようにregsvr32を起動します。

> regsvr32 COM_SampleDLL.dll

正しく登録されれば、その旨のダイアログが表示されるはずです。この状態から、クライアントのWin32コンソールアプリケーションを起動してください。画面上に以下のように表示されれば成功です。

> COM_SampleDLL.exe
Hello
Property1 = 100

最後に、COMの解除は以下のようにregsvr32を起動します。

> regsvr32 /u COM_SampleDLL.dll

これで、レジストリよりCOMの情報が削除されます。


CX5 SOFTWARE, 2010