C++でCOMサーバ

テキストエディタでCOMサーバを作って色々苦労したので、メモなどを残してみます。多分、間違っているところもあると思いますが、何かの参考になれば。

まず、必要なインタフェース分だけGUIDを作成します。

$ guidgen

IDLの作成(hello.idl)

[object, uuid(5137F758-8590-439d-AE71-98D8BDB88961)]
interface IHello : IUnknown {
    import "unknwn.idl";
    HRESULT Say ();
}

[object, uuid(126B0F62-1920-4222-92DA-23015FC076ED)]
interface IHelloClass : IUnknown {
    import "unknwn.idl";
    HRESULT Create ([out, retval] IHello **ppHello);
} 

IHelloを使ってHello,Worldを表示させます。IHelloClassはIHelloを実装したオブジェクトを生成するクラスオブジェクトのインタフェースです。

idlをコンパイルします。

$ midl hello.idl

すると、以下のファイルが作成されます。

  • hello.h
  • hello_i.c
  • hello_p.c
  • dlldata.c

今回使うのは、hello.hとhello_i.cです。

次に、IHelloとIHelloClassを実装します。(hello.cpp)

#include "hello.h"
#include <stdio.h>

const CLSID& CLSID_HelloClass = IID_IHelloClass;

class Hello : public IHello {
    LONG m_cRef;
protected:
    virtual ~Hello () {}
public:
    Hello () : m_cRef(0) {}

    STDMETHODIMP QueryInterface(REFIID riid, void **ppv) {
        if (riid == IID_IHello)
            *ppv = static_cast<IHello*>(this);
        else if (riid == IID_IUnknown)
            *ppv = static_cast<IHello*>(this);
        else {
            *ppv = 0;
            return E_NOINTERFACE;
        }
        reinterpret_cast<IUnknown*>(*ppv)->AddRef ();
        return S_OK;
    }

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

    STDMETHODIMP_(ULONG) Release () {
        LONG res = InterlockedDecrement (&m_cRef);
        if (res == 0)
            delete this;
        return res;
    }

    STDMETHODIMP Say () {
        printf ("Hello, World\n");
        return S_OK;
    }
};

class HelloClass : public IHelloClass {
public:
    virtual ~HelloClass () {}
    STDMETHODIMP QueryInterface(REFIID riid, void **ppv) {
        if (riid == IID_IHelloClass)
            *ppv = static_cast<IHelloClass*>(this);
        else if (riid == IID_IUnknown)
            *ppv = static_cast<IHelloClass*>(this);
        else {
            *ppv = 0;
            return E_NOINTERFACE;
        }
        reinterpret_cast<IUnknown*>(*ppv)->AddRef ();
        return S_OK;
    }

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

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

    STDMETHODIMP Create (IHello **ppHello) {
        if ((*ppHello = new Hello) == 0)
            return E_OUTOFMEMORY;
        (*ppHello)->AddRef ();
        return S_OK;
    }
};

STDAPI DllGetClassObject (REFCLSID rclsid, REFIID riid, void **ppv) {
    static HelloClass g_helloClass;

    if (rclsid == CLSID_HelloClass)
        return g_helloClass.QueryInterface (riid, ppv);
    *ppv = 0;
    return CLASS_E_CLASSNOTAVAILABLE;
}

また、公開する関数のためにdefファイルを作成します。(hello.def)

LIBRARY HELLO
EXPORTS
    DllRegisterServer private
    DllUnregisterServer private
    DllGetClassObject private

これで材料は揃いましたのでコンパイルします。

$ cl /LD hello.cpp hello_i.c hello_dll.cpp hello.def /link advapi32.lib

これでCOMサーバの完成です。早速、レジストリに登録してみましょう。

$ regsvr32 hello.dll

折角、サーバが出来たのでこれを呼び出すクライアントを用意してテストしてみます。

#include "hello.h"
#include <stdio.h>
#include <objbase.h>

int main (int argc, char* argv[]) {
    CoInitializeEx (NULL, COINIT_APARTMENTTHREADED);

    IHelloClass *phc = 0;

    HRESULT hr = CoGetClassObject (IID_IHelloClass, CLSCTX_ALL, 0, IID_IHelloClass, (void**)&phc);
    if (SUCCEEDED (hr)) {
        IHello *pHello = 0;
        hr = phc->Create (&pHello);
        if (SUCCEEDED (hr)) {
            hr = pHello->Say ();
            pHello->Release ();
        }
        phc->Release ();
    }
    else {
        printf("CoGetClassObject failed.\n");
    }
    
    CoUninitialize ();
    return 0;
}

これをhello_test.cppとして保存し、コンパイル

$ cl hello_test.cpp hello_i.c /link ole32.lib

実行すると、

$ hello_test.exe
Hello, World

と、なりました。

現状の怪しいところ。クライアント作成するのにhello.hとhello_i.cを使いましたが、hello.dllを配布するときに、これらも一緒に渡すわけがないので、このコンパイル方法は間違いなはず。.tlbを作成とかかしら?

Essential COMを3章の途中まで読んだ知識だと、まだこれだけ。(^^;