ATL の CAtlServiceModuleT を使ってサービスを作る

Win32 でサービス開発すると、ServiceMainHandler の登録処理などが煩雑で、毎回サンプルコードを探して検索して時間を無駄に浪費してしまう。そこで、ATL に定義されている CAtlServiceModuleT クラスを使って簡単にサービスを作る方法を確立してみた。

今回のコードは atlbase.h 内の CAtlServiceModuleT 周辺のソースコードを追いつつ、Visual C++ 2005 と Visual C++ 2008 で動作することを確認している。ただし、Microsoft の意図とは違う使い方をしているので、実戦投入する場合は十分に注意してほしい。

ソースコード

さっそくソースコード。

#define _ATL_NO_COM_SUPPORT
#include <atlbase.h>
#include <tchar.h>
#define SERVICE_NAME TEXT("ServiceName")

class CMyServiceModule : public CAtlServiceModuleT<CMyServiceModule>
{
public:
    HANDLE m_hStopEvent;

    CMyServiceModule()
    {
        m_hStopEvent = NULL;
        _tcscpy_s(m_szServiceName, _countof(m_szServiceName), SERVICE_NAME);
    }

    bool ParseCommandLine(LPCTSTR lpCmdLine, HRESULT* pnRetCode) throw()
    {
        *pnRetCode = S_OK;
        return true;
    }

    HRESULT Start(int nShowCmd) throw()
    {
        SERVICE_TABLE_ENTRY st[] =
        {
            { m_szServiceName, _ServiceMain },
            { NULL, NULL }
        };
        if (::StartServiceCtrlDispatcher(st) == 0)
            m_status.dwWin32ExitCode = GetLastError();
        return m_status.dwWin32ExitCode;
    }

    HRESULT Run(int nShowCmd = SW_HIDE) throw()
    {
        m_hStopEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
        if (m_hStopEvent == NULL)
        {
            return AtlHresultFromLastError();
        }

        LogEvent(_T("Service started"));
        SetServiceStatus(SERVICE_RUNNING);

        // ここでワーカー スレッドを作るなり、何らかの処理を実装する

        WaitForSingleObject(m_hStopEvent, INFINITE);
        CloseHandle(m_hStopEvent);

        return S_OK;
    }

    void OnStop() throw()
    {
        SetServiceStatus(SERVICE_STOP_PENDING);
        SetEvent(m_hStopEvent);
    }
};

int WINAPI _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int nShowCmd)
{
    CMyServiceModule module;

    //if (!module.IsInstalled())
    //    module.Install();

    return module.WinMain(nShowCmd);
}

使い方

サービス登録するには、sc create コマンドを使うか、_tWinMain() のコメントアウトしてある部分を消して一度実行するとよい。

登録したサービスを [開始] すると、サービスが開始する。現在の実装では何もせずに停止されるのを待機するだけのものとなっている。[停止] を実行すると無事に停止することを確認できる。

解説

軽く解説していく。atlbase.h で定義されている CAtlServiceModuleT のソースと、上に掲載している CMyServiceModule のソースを見比べると理解できるはず…。

#include 処理

まずは、CAtlServiceModuleT が定義されている atlbase.h を include する。

CAtlServiceModuleT は ATL で定義される CAtlExeModuleT を継承したクラスだ。

ATL の XXXXModule クラスは COM に必要な初期化などをやってくれるが、今回は COM は使わないので _ATL_NO_COM_SUPPORT を事前に define しておく。

#define _ATL_NO_COM_SUPPORT
#include <atlbase.h>
#include <tchar.h>

クラス定義

CAtlServiceModuleT は ATL の CComObject クラスなどと同じように、クラス名をテンプレート引数に渡して継承してやる。

class CMyServiceModule : public CAtlServiceModuleT<CMyServiceModule>
{
public:
    // 停止用のイベント
    HANDLE m_hStopEvent;

// ...
};

CAtlServiceModuleT には WinMain メソッドや、その他のサービス実装に便利なメソッドが定義されている。

ここでは、追加でサービス停止時にシグナル化される m_hStopEvent を追加で定義してあげた。public なのは気持ち悪いが、CAtlServiceModuleT クラスの他のフィールドも public なのでそれに合わせる形とした。

コンストラクタ

    CMyServiceModule()
    {
        // 停止用のイベントを初期化
        m_hStopEvent = NULL;

        // サービス名を設定
        _tcscpy_s(m_szServiceName, _countof(m_szServiceName), SERVICE_NAME);
    }

m_szServiceName にサービス名を設定している。

CAtlServiceModuleT のコンストラクタには、リソースからサービス名を取得する実装があるのだが、リソースに依存するのは不便だったので、自力でサービス名を設定するようにしておいた。これでリソースがなくても、m_szServiceName にサービス名が設定される。ServiceName のところは自分で作るサービス名に適宜置き換えるべし。

エントリポイント

ここでアプリケーションのエントリポイントを見ておく。

int WINAPI _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int nShowCmd)
{
    // サービス モジュールのインスタンスを作成
    CMyServiceModule module;

    // サービスをインストールするには次のようにする
    //if (!module.IsInstalled())
    //    module.Install();

    // モジュールの WinMain に処理を委譲すうr
    return module.WinMain(nShowCmd);
}

単純に処理を CAtlServiceModuleT::WinMain に委譲している。呼ばれた CAtlServiceModuleT::WinMain() は、「CAtlServiceModuleT::GetCommandLine() を実行し、成功すれば CAtlServiceModuleT::Start() を呼ぶ」という実装になっている。

コメントアウトされてる部分はサービスの登録処理だ。必要に応じて活用してもよいだろう。

ParseCommandLine メソッド

CAtlServiceModuleT::GetCommandLine() は COM サーバーとして登録するための処理を実装しているので、次のように何もしない処理でオーバーライドした。

    bool ParseCommandLine(LPCTSTR lpCmdLine, HRESULT* pnRetCode) throw()
    {
        *pnRetCode = S_OK;
        return true;
    }

Start メソッド

CAtlServiceModuleT::Start() は、「サービスの登録を行う」以外にも、「レジストリの HKCR\AppID から COM サーバーの情報を読み出してサービスかどうか判定する」という処理が入っている。

そこで、「サービスを登録を行う」部分だけを抜き出してオーバーライドしている。

    HRESULT Start(int nShowCmd) throw()
    {
        SERVICE_TABLE_ENTRY st[] =
        {
            { m_szServiceName, _ServiceMain },
            { NULL, NULL }
        };
        if (::StartServiceCtrlDispatcher(st) == 0)
            m_status.dwWin32ExitCode = GetLastError();
        return m_status.dwWin32ExitCode;
    }

この部分で StartServiceCtrlDispatcher() API を使って _ServiceMain を登録する。

CAtlServiceModuleT::_ServiceMain() は Handler を登録して、成功すれば Run() を呼び出すようになっている。次は Run() の実装を見ていく。

Run メソッド

CAtlServiceModuleT::Run() ではメッセージ ループを使った実装があるが、ここではシンプルな独自定義を行うことにした。

    HRESULT Run(int nShowCmd = SW_HIDE) throw()
    {
        // イベントを作成
        m_hStopEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
        if (m_hStopEvent == NULL)
        {
            return AtlHresultFromLastError();
        }

        // サービス起動成功
        LogEvent(_T("Service started"));
        SetServiceStatus(SERVICE_RUNNING);

        // ここでワーカー スレッドを作るなり、何らかの処理を実装する

        // イベントがシグナル化するまで待機する
        WaitForSingleObject(m_hStopEvent, INFINITE);
        CloseHandle(m_hStopEvent);

        return S_OK;
    }

OnStop メソッド

サービス停止が要求されると、OnStop() メソッドが呼ばれる。このあたりの流れは、CAtlServiceModuleT::Handler() の実装を確認するとよい。

ここでは OnStop() のデフォルト実装をオーバーライドし、停止通知用のイベントをシグナル化している。

    void OnStop() throw()
    {
        SetServiceStatus(SERVICE_STOP_PENDING);
        SetEvent(m_hStopEvent);
    }

その他

LogEvent() ではデフォルトでイベントログに出力するが、これもオーバーライドしてもよいかもしれない。

まとめ

繰り返しになるが、CAtlServiceModuleT クラスの内部実装に依存した形で活用しているので注意してもらいたい。メンテナンス性を考えると独自ラッパーを作ったほうが安心かもしれない。その場合にも CAtlServiceModuleT の実装はかなり参考になるはずだ。

追記

この記事を書いたあとに知ったのですが、Microsoft が公開するサンプルコード集、All-In-One Code Framework に C++ で実装するサービスのサンプル「CppWindowsService」が含まれています。ATL と似た CServiceBase クラスと、それを継承した CSampleService クラスのコードがあるので参照してみるとよいでしょう。