CAtlExeModuleT<> を使用したプロジェクトでの初期化

VS2005 で COM の exe サーバのプロジェクトを作ると、

class CHogeModule : public CAtlExeModuleT< CHogeModule >
{
public :
    DECLARE_LIBID(LIBID_HogeLib)
    DECLARE_REGISTRY_APPID_RESOURCEID(IDR_HOGE, "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}")
};

CHogeModule _AtlModule;

//
extern "C" int WINAPI _tWinMain(HINSTANCE /*hInstance*/, HINSTANCE /*hPrevInstance*/, 
                                LPTSTR /*lpCmdLine*/, int nShowCmd)
{
    return _AtlModule.WinMain(nShowCmd);
}

こんな感じのファイルが出来ます。
このプログラム自体はもちろん何の問題も無いのですが、プログラム全体の初期化処理と終了処理をどこに書けばいいか、というのを考えてみます。

OOP 的に考えると、_AtlModule は唯一のグローバル変数と考えられます。
そしてそれは、このプログラム全体を総括する役割と責任を持っていると考えられるので、初期化処理と終了処理はこいつが行うことにしましょう。

class CHogeModule : public CAtlExeModuleT< CHogeModule >
{
public :
    DECLARE_LIBID(LIBID_HogeLib)
    DECLARE_REGISTRY_APPID_RESOURCEID(IDR_HOGE, "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}")
public:
    CHogeModule()
    {
        // 初期化処理
    }
    virtual ~CHogeModule()
    {
        // 終了処理
    }
};

これでいいと思うかもしれないですが、初期化処理に失敗したら起動させない、という処理を行いたい場合、CHogeModule はエラーを返すか、メンバにエラーが起こったかどうかを確認するためのフラグを持たせておく必要があります。
また、OOP 的に考えると CHogeModule 以外にグローバル変数があってはならない訳ですが、実際にはいろいろな場面で使われるでしょう。
グローバル変数のコンストラクタが呼ばれる順番は不定なので、_AtlModule の初期化部分で他のグローバル変数を参照することは非常に危険になってきます。


なので、初期化メソッドは別に用意して、_tWinMain() からそれを呼んでやることにしましょう。

class CHogeModule : public CAtlExeModuleT< CHogeModule >
{
public :
    DECLARE_LIBID(LIBID_HogeLib)
    DECLARE_REGISTRY_APPID_RESOURCEID(IDR_HOGE, "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}")
public:
    HRESULT Initialize()
    {
        // 初期化処理
    }
    void Destroy()
    {
        // 終了処理
    }
};

//
extern "C" int WINAPI _tWinMain(HINSTANCE /*hInstance*/, HINSTANCE /*hPrevInstance*/, 
                                LPTSTR /*lpCmdLine*/, int nShowCmd)
{
    HRESULT result = _AtlModule.Initialize();
    if (FAILED(result))
    {
        return result;
    }
    int ret = _AtlModule.WinMain(nShowCmd);
    _AtlModule.Destroy();
    return ret;
}

これならグローバル変数は既に初期化されていることが保証されているし、エラーが起こった場合は起動せずにエラーを返すという処理も出来ています。


ここまではどんなプログラムでも普通にやっていることだと思うけど、COM の exe サーバを作るときにこうするのはあまり良くないです。


というのも、_tWinMain() はメインのプログラムを実行する以外の処理もあったりするからです。


少し話はそれますが、CoCreateInstance() で COM の exe サーバを作るためには、あらかじめ準備をしておく必要があります。
なぜなら、CoCreateInstance() は要求された exe サーバ(もしくは dll)を起動しなければならないのに、その exe が置いてあるファイルのフルパスが分からなければ起動することが出来ないからです。
なので、レジストリに GUID とか exe へのフルパスを書き込んでおくか、CoCreateInstance() をする前に exe サーバを直接起動しておく必要があります。
普通は前者の方法が使われます。
DLL の COM なら

 regsvr32.exe Hoge.dll

これで登録できます。が、exe はそれで登録することは出来ません。
exe サーバは、

 Hoge.exe /RegServer

こうやって、引数に /RegServer を指定すると、その exe はレジストリに登録されます。
VS2005 では、ビルド後のイベントの中で上記の登録を行っているので、デバッグ時には何も考えずに CoCreateInstance() をするだけで exe サーバを起動することが出来るということです。


話を戻すと、exe サーバは当然、引数を確認して、/RegServer であることが分かったら、レジストリに自分の GUID やらフルパスやらの情報を書き込み、終了するわけですが、これは誰がやっているのでしょうか。
当然 Hoge.exe がやっているのは自明ですが、自分の書いたプログラムではそんな処理は行っていません。
これは、CAtlExeModuleT<> が WinMain() メソッドの中で行っているのです。


ということは、_tWinMain() は /RegServer(それと登録を解除する /UnregServer)が指定されている場合でも通るわけで、その場合に初期化処理・終了処理を行ってしまうと、おかしな挙動をしてしまうことがあります。
例えば初期化の段階でファイルを生成したり書き込んだりする場合は、コンパイル時に毎回それをしてしまいます。
なので、/RegServer・/UnregServerの途中では初期化処理・終了処理は行わずに _AtlModule.WinMain() を呼び出す必要があるわけです。


長くなってきたのでこれをどうやって呼び出すのかは次回に持ち越し。