Win32API SRWLock

Windows VistaからSRWLock(Slim Reader/Writer Lock)が導入された。


目的はクリティカルセクションと同じだが、共有ロックと排他ロックを別々に適用することで、パフォーマンスが向上を図ることができるところがメリットになる。




SRWLockに関する主な関数には、以下の5つがある。

InitializeSRWLock
AcquireSRWLockExclusive
ReleaseSRWLockExclusive
AcquireSRWLockShared
ReleaseSRWLockShared

SRWLockは自動的に解放されるため、クリティカルセクションのDeleteCriticalSectionのようにSRWLockを解放するための関数は存在しない。





InitializeSRWLock関数は、SRWLOCKオブジェクトを初期化する。この関数で初期化されたSRWLOCKオブジェクトを用いて、オブジェクトに対して共有ロックや排他ロックを行う。


InitializeSRWLockのプロトタイプ

VOID WINAPI InitializeSRWLock(
  _Out_  PSRWLOCK SRWLock //SRWLOCK構造体へのポインタ
);




AcquireSRWLockExclusive関数は、排他ロックを獲得する。
AcquireSRWLockExclusive関数のプロトタイプ

VOID WINAPI AcquireSRWLockExclusive(
  _Inout_  PSRWLOCK SRWLock
);




ReleaseSRWLockExclusive関数は、排他ロックを解放する。
ReleaseSRWLockExclusiveのプロトタイプ

VOID WINAPI ReleaseSRWLockExclusive(
  _Inout_  PSRWLOCK SRWLock
);





AcquireSRWLockShared関数は、共有ロックを獲得する。
AcquireSRWLockSharedのプロトタイプ

VOID WINAPI AcquireSRWLockShared(
  _Inout_  PSRWLOCK SRWLock
);




ReleaseSRWLockShared関数は、共有ロックを解放する。
ReleaseSRWLockSharedのプロトタイプ

VOID WINAPI ReleaseSRWLockShared(
  _Inout_  PSRWLOCK SRWLock
);




SRWLockで排他ロックを行うサンプルプログラム
複数のスレッドを用いて、1つの整数オブジェクトをインクリメントする。

#include <windows.h>
#include <stdio.h>

static PTP_WORK g_pWorkItem;
static SRWLOCK g_SRWLock;
static CRITICAL_SECTION g_Crit;
static __int64 g_nCount = 0;


void NTAPI WorkCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work)
{
    for (int i = 0; i < 10000; i++) {
        AcquireSRWLockExclusive(&g_SRWLock);
        printf("g_nCount = %d\n", g_nCount++);
        ReleaseSRWLockExclusive(&g_SRWLock);
    }
}

int main()
{
    InitializeSRWLock(&g_SRWLock);
    g_pWorkItem = ::CreateThreadpoolWork(WorkCallback, NULL, NULL);

    for (int i = 0; i < 10; i++) {
        SubmitThreadpoolWork(g_pWorkItem);
    }

    WaitForThreadpoolWorkCallbacks(g_pWorkItem, FALSE);

    CloseThreadpoolWork(g_pWorkItem);

    return 0;
}




次に、共有ロックと排他ロックを用いたサンプルプログラムを示す。

1つの整数オブジェクトをWriterThread関数とReaderThread関数で共有している。

WriterThread関数では、排他ロックを用いて整数オブジェクトをインクリメントし、

ReaderThread関数では共有ロックを用いて整数オブジェクトの値を読み取って表示している。

双方のスレッドで整数オブジェクトの値がMAXCOUNTに達したら関数を終了させる。

#include <Windows.h>
#include <stdio.h>

static PTP_WORK g_pWorkItemReader;
static PTP_WORK g_pWorkItemWriter;
static SRWLOCK g_SRWLock;
static __int64 g_nCount = 0;
static const __int64 MAXCOUNT = 1000000;

//共有ロックを用いて g_nCount の値を読み取り、コンソールに表示する
void NTAPI ReaderThread(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work)
{
    while (1) {
        __try {
            AcquireSRWLockShared(&g_SRWLock);
            
            printf("g_nCount = %ld\n", g_nCount);
            if (g_nCount >= MAXCOUNT)
            {
                break;
            }

        } __finally {
            ReleaseSRWLockShared(&g_SRWLock);
        }
    }
}

//排他ロックを用いて g_nCount の値をインクリメントする
void NTAPI WriterThread(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work)
{
    while (1) {
        __try {
            AcquireSRWLockExclusive(&g_SRWLock);
            
            if (g_nCount >= MAXCOUNT) {
                break;
            }
            g_nCount++;

        } __finally {
            ReleaseSRWLockExclusive(&g_SRWLock);
        }
    }
}

int main()
{
    //SRQLOCK構造体オブジェクトを初期化
    ::InitializeSRWLock(&g_SRWLock);
    
    //WriterThread関数, ReaderThread関数のスレッドプールを生成
    g_pWorkItemWriter = ::CreateThreadpoolWork(WriterThread, NULL, NULL);
    g_pWorkItemReader = ::CreateThreadpoolWork(ReaderThread, NULL, NULL);
    
   
    //WriterThread関数とReaderThread関数によるスレッドを共に100個生成する
    for (int i = 0; i < 100; i++) {
        SubmitThreadpoolWork(g_pWorkItemWriter);
        SubmitThreadpoolWork(g_pWorkItemReader);
    }
    
    //双方のスレッドの完了を待機する
    WaitForThreadpoolWorkCallbacks(g_pWorkItemWriter, FALSE);
    WaitForThreadpoolWorkCallbacks(g_pWorkItemReader, FALSE);

    printf("Complete! - g_nCount = %d\n", g_nCount);

    //スレッドプールオブジェクトを解放
    CloseThreadpoolWork(g_pWorkItemWriter);
    CloseThreadpoolWork(g_pWorkItemReader);

    return 0;
}






参考

InitializeSRWLock http://msdn.microsoft.com/ja-jp/library/windows/desktop/ms683483(v=vs.85).aspx
AcquireSRWLockExclusive http://msdn.microsoft.com/en-us/library/windows/desktop/ms681930(v=vs.85).aspx
ReleaseSRWLockExclusive http://msdn.microsoft.com/en-us/library/windows/desktop/ms685076(v=vs.85).aspx
AcquireSRWLockShared http://msdn.microsoft.com/en-us/library/windows/desktop/ms681934(v=vs.85).aspx
ReleaseSRWLockShared http://msdn.microsoft.com/en-us/library/windows/desktop/ms685080(v=vs.85).aspx

Win32API 新しいスレッドプール タイマーオブジェクトのAPI

Windows Vistaで新たに追加されたスレッドプールのタイマーオブジェクトのAPIを使う。

タイマーオブジェクトは、指定した期限に達したときにコールバック関数を呼び出すために使用する。


まず、CreateThreadpoolTimer関数でスレッドプールタイマーオブジェクトを取得する

PTP_TIMER WINAPI CreateThreadpoolTimer(
  _In_         PTP_TIMER_CALLBACK pfnti, //コールバック関数
  _Inout_opt_  PVOID pv, //コールバック関数へ渡す引数
  _In_opt_     PTP_CALLBACK_ENVIRON pcbe //TP_CALLBACK_ENVIRON構造体へのポインタ
);



次に、SetThreadpoolTimer関数でスレッドプールタイマーオブジェクトをセットする。

VOID WINAPI SetThreadpoolTimer(
  _Inout_   PTP_TIMER pti, //スレッドプールタイマーオブジェクトのハンドル
  _In_opt_  PFILETIME pftDueTime, //タイマーが期限に達するまでの時間
  _In_      DWORD msPeriod, //コールバック関数が呼び出される間隔
  _In_opt_  DWORD msWindowLength //システムがタイマー呼び出しで遅延を起こす時間の最大値
);



このSetThreadpoolTimer関数で注意が必要なのは、第2引数の pftDueTime である。この引数では、SetThreadpoolTimer関数が呼び出されてから、最初にコールバック関数が呼び出されるまでの時間を指定する。指定方法には2種類あって、正の数かゼロを指定すると、1601年1月1日(UTC)からの絶対時間を示すことになる。負の数を指定すると、現在の時刻からの相対的な時間を示すことになる。私の感覚では、現在の時刻から相対的な時間を扱うほうが多いのではないかと思うので、負の数を指定するケースの方が多いのではないかと思う。また、100ナノ秒単位で指定するということにも注意が必要である。



最後に、CloseThreadpoolTimer関数でスレッドプールタイマーオブジェクトを解放する。

VOID WINAPI CloseThreadpoolTimer(
  _Inout_  PTP_TIMER pti //スレッドプールタイマーオブジェクト(CreateThreadpoolTimer関数の戻り値)
);





以下に、新しいスレッドプールによるスレッドプールタイマーを用いたサンプルを示す。
プログラム実行直後から3秒後に初めてコールバック関数が呼ばれるようにしている。コールバック関数は2秒間隔で呼び出されるようにしており、コールバック関数の中では現在の時刻をコンソールに表示している。10秒間経過すると、スレッドプールタイマーオブジェクトを解放し、プログラムは終了する。

#include <windows.h>
#include <stdio.h>

//現在の時刻をコンソールに表示するコールバック関数
VOID NTAPI TimerCallback(PTP_CALLBACK_INSTANCE Instance,
    PVOID Context,
    PTP_TIMER Timer)
{
    SYSTEMTIME st;

    GetLocalTime(&st);

    printf("%02d:%02d:%02d\n",
        st.wHour,
        st.wMinute,
        st.wSecond);
}

int main()
{
    //スレッドプールタイマーオブジェクトを生成
    PTP_TIMER lpTimer =
        CreateThreadpoolTimer(TimerCallback, NULL, NULL);


    //SetThreadpoolTimer関数の呼び出し後、3秒後にコールバック関数が呼ばれるようにする
    ULARGE_INTEGER ulRelativeStartTime;
    ulRelativeStartTime.QuadPart = (LONGLONG)(-30000000);//100nano秒単位で指定する
    
    FILETIME ftStartTime;
    ftStartTime.dwHighDateTime = ulRelativeStartTime.HighPart;
    ftStartTime.dwLowDateTime = ulRelativeStartTime.LowPart;

    //タイマーを登録し、一定間隔でコールバック関数が呼ばれるようにする
    //2秒間隔でコールバック関数が呼ばれるように指定している。
    SetThreadpoolTimer(lpTimer,
        &ftStartTime,
        2000,
        0);

    //10秒待機
    Sleep(10 * 1000);

    //スレッドプールタイマーオブジェクトを解放する
    CloseThreadpoolTimer(lpTimer);

    return 0;
}


実行結果

12:55:12
12:55:14
12:55:16
12:55:18





スレッドプールタイマーオブジェクトは、WaitForThreadpoolTimerCallbacks関数を用いることでコールバック関数が終了するのを待機することができる。

VOID WINAPI WaitForThreadpoolTimerCallbacks(
  _Inout_  PTP_TIMER pti, //スレッドプールタイマーオブジェクト
  _In_     BOOL fCancelPendingCallbacks //キューに入れられたコールバック関数をキャンセルするかしないか
);



以下のプログラムで、WaitForThreadpoolTimerCallbacks関数の動作を検証する。
SetThreadpoolTimerが呼び出されたあとすぐにコールバック関数が呼び出される。このコールバック関数は5秒間スリープする。その間、WaitForThreadpoolTimerCallbacks関数がコールバック関数の終了を待機する。5秒経過後コールバック関数が終了すると、WaitForThreadpoolTimerCallbacks関数から制御が戻りプログラムが終了する。

#include <windows.h>
#include <stdio.h>

VOID NTAPI TimerCallback(PTP_CALLBACK_INSTANCE Instance,
    PVOID Context,
    PTP_TIMER Timer)
{
    puts("コールバック関数 開始");
    Sleep(5000);
    puts("コールバック関数 終了");
}

int main()
{
    PTP_TIMER lpTimer =
        CreateThreadpoolTimer(TimerCallback, NULL, NULL);


    ULARGE_INTEGER ulRelativeStartTime;
    ulRelativeStartTime.QuadPart = (LONGLONG)(0);
    
    FILETIME ftStartTime;
    ftStartTime.dwHighDateTime = ulRelativeStartTime.HighPart;
    ftStartTime.dwLowDateTime = ulRelativeStartTime.LowPart;

    SetThreadpoolTimer(lpTimer,
        &ftStartTime,
        10*1000,
        0);

    puts("WaitForThreadpoolTimerCallbacks開始");

    WaitForThreadpoolTimerCallbacks(lpTimer, FALSE);

    puts("WaitForThreadpoolTimerCallbacks終了");

    CloseThreadpoolTimer(lpTimer);

    return 0;
}


実行結果

WaitForThreadpoolTimerCallbacks開始
コールバック関数 開始
コールバック関数 終了
WaitForThreadpoolTimerCallbacks終了



TimerCallback関数で5秒間スリープしている間、WaitForThreadpoolTimerCallbacks関数がTimerCallback関数の終了を待機しているのがわかる。TimerCallback関数終了後に、WaitForThreadpoolTimerCallbacks関数から制御が戻っている。








参考

CreateThreadpoolTimer: http://msdn.microsoft.com/ja-jp/library/windows/desktop/ms682466(v=vs.85).aspx

SetThreadpoolTimer: http://msdn.microsoft.com/ja-jp/library/windows/desktop/ms686271(v=vs.85).aspx

CloseThreadpoolTimer: http://msdn.microsoft.com/ja-jp/library/windows/desktop/ms682040(v=vs.85).aspx

WaitForThreadpoolTimerCallbacks: http://msdn.microsoft.com/ja-jp/library/windows/desktop/ms687042(v=vs.85).aspx

Win32API 例外ハンドラ SetUnhandledExceptionFilter

SetUnhandledExceptionFilter関数で例外ハンドラを登録しておくと、プログラムでAccessViolationなどの例外が発生したときに、登録した例外ハンドラが呼び出される。




使用例
main関数の最初に、SetUnhandledExceptionFilterで例外ハンドラを登録する。その直後に、strcpyでAccessViolationを発生させる。ここで、登録された例外ハンドラが呼び出される。例外ハンドラであるTopLevelExceptionFilter関数は、引数で渡されてくる例外に関する情報をメッセージボックスで表示する。

#include <windows.h>
#include <stdio.h>
#include <strsafe.h>
#include <string.h>

LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
    TCHAR szText[1024] =  {TEXT('\0')};

    StringCchPrintf(szText,
        sizeof(szText)/sizeof(szText[0]),
        TEXT("ExceptionAddress=%X, ExceptionCode=%X, ExceptionFlags=%X, ExceptionInformation=%X, NumberParameters=%X"),
        ExceptionInfo->ExceptionRecord->ExceptionAddress,
        ExceptionInfo->ExceptionRecord->ExceptionCode,
        ExceptionInfo->ExceptionRecord->ExceptionFlags,
        ExceptionInfo->ExceptionRecord->ExceptionInformation,
        ExceptionInfo->ExceptionRecord->NumberParameters);

    MessageBox(NULL, szText, TEXT("ExceptionFilter"), MB_OK);
    
    return 0;
}

int main()
{
    SetUnhandledExceptionFilter(TopLevelExceptionFilter);
    
    strcpy(NULL, NULL);

    return 0;
}



実際には、デバッグ中にクラッシュさせても例外ハンドラは呼び出されない。このソースコードをビルドして生成された実行ファイルを実行すると例外ハンドラが呼び出されて、メッセージボックスが起動して例外に関する情報が表示される。

メッセージボックスを閉じると、「xxxxx.exeの動作は停止しました」というメッセージボックスが起動し、プログラムが終了する。



参考
http://msdn.microsoft.com/ja-jp/library/cc428992.aspx


Win32API ダブルクリック時間を取得、設定する

ダブルクリック時間を取得するにはGetDoubleClickTime関数を、設定するにはSetDoubleClickTime関数を用いる。


GetDoubleClickTime関数とSetDoubleClickTime関数の使用例は、以下の通り
このプログラムは、まず現在設定されているダブルクリック時間を取得したあとで、ダブルクリック時間を3秒間に設定している。そして、10秒経過後に元の設定に直すという処理になっている。プログラムがスリープ中に、アイコン等に対して長い間隔でクリックを2回行うとダブルクリックとみなされてしまうことが確認できる。

#include <windows.h>
#include <stdio.h>

int main()
{
    UINT uiDoubleClickTime;
   
    uiDoubleClickTime = GetDoubleClickTime();

    if (SetDoubleClickTime(3 * 1000)) {
        puts("Double click time is 3 seconds.");
        Sleep(10 * 1000);
        SetDoubleClickTime(uiDoubleClickTime);
        puts("Double click time was reset.");
    } else {
        puts("SetDoubleClickTime failed");
    }

    return 0;
}


もし、プログラムがスリープ中にプログラムを停止させてしまった場合は、ダブルクリック時間が3秒に設定されたままになってしまうので、SetDoubleClickTime関数を呼び出すか、Windowsのマウスの設定等で元に戻さないと不便である。恐らくダブルクリック時間の既定は、500ミリ秒程度なので、500ミリ秒に設定すれば、それほど違和感がなく使用できるようになる。




参考

SetDoubleClickTime http://msdn.microsoft.com/ja-jp/library/cc411070.aspx

GetDoubleClickTime http://msdn.microsoft.com/ja-jp/library/cc364628.aspx

Win32API ディスプレイモニターの情報を取得する

ディスプレイモニターの情報を取得するには、GetMonitorInfo関数を用いる。


GetMonitorInfo関数のプロトタイプは、以下の通り

BOOL GetMonitorInfo(
  _In_   HMONITOR hMonitor,
  _Out_  LPMONITORINFO lpmi
);


第一引数の hMonitor はディスプレイモニターのハンドルで、MonitorFromPoint関数を用いて取得する。

第二引数の lpmi は、MONITORINFO構造体またはMONITORINFOEX構造体へのポインタで、GetMonitorInfo関数からモニター情報を受け取る。


MONITORINFOEX構造体は、以下のように定義されている。

typedef struct tagMONITORINFOEX {
  DWORD cbSize; //構造体のサイズ。つまり、sizeof(MONITORINFOEX)
  RECT  rcMonitor;//ディスプレイモニターの矩形を表すRECT構造体
  RECT  rcWork;//仮想スクリーン座標の作業領域の矩形を表すRECT構造体
  DWORD dwFlags;//ディスプレイモニターの属性
  TCHAR szDevice[CCHDEVICENAME];//使用中のモニター名
} MONITORINFOEX, *LPMONITORINFOEX;





GetMonitorInfoの使用例
MonitorFromPoint関数で、座標が(100, 100)の位置のモニターのハンドルを取得する。このハンドルをetMonitorInfo関数の第一引数に指定することで、モニター情報を取得する。シングルモニターの場合とデュアルモニターの場合は、MonitorFromPoint関数に指定する座標次第で結果が異なってくるであろう。

#include <windows.h>
#include <stdio.h>

int main()
{
    HMONITOR hMonitor;
    MONITORINFOEX  MonitorInfoEx;
    POINT pt = { 100, 100 };

    //ptで指定した部分のモニターのハンドルを取得する
    hMonitor = MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST);
    
    //モニター情報を取得する
    MonitorInfoEx.cbSize = sizeof(MonitorInfoEx);
    GetMonitorInfo(hMonitor, &MonitorInfoEx);
    
    //モニター情報を表示する
    printf("rcMonitor.bottom = %d\n", MonitorInfoEx.rcMonitor.bottom);
    printf("rcMonitor.left = %d\n", MonitorInfoEx.rcMonitor.left);
    printf("rcMonitor.right = %d\n", MonitorInfoEx.rcMonitor.right);
    printf("rcMonitor.top = %d\n", MonitorInfoEx.rcMonitor.top);

    printf("rcWork.bottom = %d\n", MonitorInfoEx.rcWork.bottom);
    printf("rcWork.left = %d\n", MonitorInfoEx.rcWork.left);
    printf("rcWork.right = %d\n", MonitorInfoEx.rcWork.right);
    printf("rcWork.top = %d\n", MonitorInfoEx.rcWork.top);

    if (MonitorInfoEx.dwFlags == MONITORINFOF_PRIMARY) {
        puts("This is Primary Monitor");
    } else {
        puts("This is not Primary Monitor");
    }

    _tprintf(TEXT("szDevice = %s\n"), MonitorInfoEx.szDevice);

    return 0;
}



実行結果

rcMonitor.bottom = 1050
rcMonitor.left = 0
rcMonitor.right = 1680
rcMonitor.top = 0
rcWork.bottom = 1010
rcWork.left = 0
rcWork.right = 1680
rcWork.top = 0
This is Primary Monitor
szDevice = \\.\DISPLAY1





参考

MonitorFromPoint http://msdn.microsoft.com/ja-jp/library/cc410472.aspx

GetMonitorInfo http://msdn.microsoft.com/en-us/library/windows/desktop/dd144901(v=vs.85).aspx

MONITORINFOEX http://msdn.microsoft.com/en-us/library/windows/desktop/dd145066(v=vs.85).aspx

Win32API Windowsサービスアプリケーションの開発

Windowsサービスアプリケーションの開発を行う。

まず、アプリケーションをサービス制御マネージャのデータベースに登録するところから行う。

使用する主なAPIは、OpenSCManager関数とCreateService関数である。


アプリケーションをサービス制御マネージャのデータベースに登録を行うソースコードは、以下のようになる。ここでは、Win32アプリケーション(コンソールアプリケーションではない)で作成した。
WinMain関数で、InstallService関数を呼び出している。InstallService関数の中で、Windowsサービスを作成し、サービス制御マネージャのデータベースに追加している。

#include "stdafx.h"
#include <Windows.h>
#include <strsafe.h>

#define SERVICE_NAME TEXT("SampleServiceApp")
#define DISPLAY_NAME TEXT("SampleServiceApp")
#define DESCRIPTION TEXT("Sample Service Application")

BOOL InstallService()
{
    SC_HANDLE hSCM = NULL;
    SC_HANDLE hService = NULL;
    TCHAR szBinaryPathName[MAX_PATH];
    SERVICE_DESCRIPTION sd;
    BOOL bRet = FALSE;

    //サービス制御マネージャのデータベースのハンドルを取得する
    //第3引数に 「SC_MANAGER_CREATE_SERVICE」 を指定することで、
    //サービスを作成しサービス制御マネージャのデータベースに追加できるようにしている
    //つまり、CreateService関数の呼び出しをできるようにしているということになる。
    hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
    if (hSCM == NULL) {
        goto EXIT;
    }

    //自分自身の実行ファイルのフルパスを取得する
    GetModuleFileName(NULL, 
        szBinaryPathName, 
        sizeof(szBinaryPathName)/sizeof(szBinaryPathName[0]));


    //サービスを作成し、サービス制御マネージャのデータベースに追加する。
    hService = CreateService(hSCM, //サービス制御マネージャのハンドル
        SERVICE_NAME, //サービス名
        DISPLAY_NAME, //表示名
        SERVICE_CHANGE_CONFIG,//サービスの構成を変更できるようにする
        SERVICE_WIN32_OWN_PROCESS, //サービスアプリケーションが専用のプロセス内で動作
        SERVICE_DEMAND_START,//StartService関数が呼び出されたときに、サービスが開始される
        SERVICE_ERROR_IGNORE,//エラー発生時に、サービス開始操作を続行する
        szBinaryPathName, //サービスアプリケーションの実行ファイルパス
        NULL,
        NULL,
        NULL,
        NULL,
        NULL);

    if (hService == NULL) {
        goto EXIT;
    }

    sd.lpDescription = DESCRIPTION;
    
    ChangeServiceConfig2(hService, SERVICE_CONFIG_DESCRIPTION, &sd);

    bRet = TRUE;


EXIT:
    //クリーンアップ
    CloseServiceHandle(hService);
    CloseServiceHandle(hSCM);

    return bRet;
}


int APIENTRY _tWinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,
                     int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    if (InstallService()) {
        MessageBox(NULL, TEXT("成功"), TEXT("成功"), MB_OK);
    } else {
        TCHAR szMsg[64];
        StringCchPrintf(szMsg, 
            sizeof(szMsg)/sizeof(szMsg[0]), 
            TEXT("失敗 - ErrorCode=%d"), GetLastError());
        MessageBox(NULL, szMsg, TEXT("失敗"), MB_OK);
    }

	return 0;
}


このコードをビルドし、生成された実行ファイルを管理者権限で実行すると、サービスがサービス制御マネージャのデータベースに登録される。登録されているかどうかは、


[スタート]->[コントロールパネル]->[システムとセキュリティ]->[管理ツール]->[サービス]


から確認できる。


以下の画像が、登録されたサービスである。









次に、実際のサービスアプリケーションを実装する。

先程、サービス制御マネージャのデータベースに登録した実行ファイルをサービスアプリケーションとして利用するため、先程のソースコードにサービスアプリケーションのコードを追加および修正していく。



ここでやるべきことは、2つのグローバル変数と3つの関数を追加し、WinMain関数の修正を行う。

追加する2つのグローバル変数とは、以下のSERVICE_STATUS構造体オブジェクトとSERVICE_STATUS_HANDLE型オブジェクトである。

SERVICE_STATUS g_ServiceStatus;
SERVICE_STATUS_HANDLE g_ServiceStatusHandle;




追加する3つの関数は、「ServiceEntry関数」、「ServiceThread関数」,「HandlerEx関数」である。ServiceEntry関数は、サービスアプリケーションのエントリポイントのような位置づけの関数である。ここでサービス制御ハンドラーであるHandlerExを登録し、ServiceThread関数をスレッドとして起動する。このServiceThread関数で、サービスアプリケーションで実現すべき処理を実装する。サービス制御ハンドラーHandlerEx関数は、実行中のサービスの状態が変更されたときに呼び出されるコールバック関数である。例えば、サービスの状態が「開始」から「停止」や「一時停止」などの状態に変更されたときに呼び出される。


WinMain関数では、StartServiceCtrlDispatcher関数(Win32API)をServiceEntry関数を指定して呼び出すように修正する。これにより、WinMain → ServiceEntry → ServiceThread という順番に呼び出される。



以下に、実装例を示す。

#include "stdafx.h"
#include <Windows.h>
#include <strsafe.h>
#include <process.h>
#include <stdio.h>

#include <shlwapi.h>
#pragma comment(lib, "shlwapi.lib")

#define SERVICE_NAME TEXT("SampleServiceApp")
#define DISPLAY_NAME TEXT("SampleServiceApp")
#define DESCRIPTION TEXT("Sample Service Application")

SERVICE_STATUS g_ServiceStatus;
SERVICE_STATUS_HANDLE g_ServiceStatusHandle;

//サービス制御ハンドラー
DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, 
                       PVOID pvEventData, PVOID pvContext)
{
    switch (dwControl) {
    
        case SERVICE_CONTROL_STOP:
            g_ServiceStatus.dwCurrentState = SERVICE_STOPPED;
            break;

        case SERVICE_CONTROL_SHUTDOWN:
            g_ServiceStatus.dwCurrentState = SERVICE_STOPPED;
            break;

        case SERVICE_CONTROL_PAUSE:
            g_ServiceStatus.dwCurrentState = SERVICE_PAUSED;
            break;

        case SERVICE_CONTROL_CONTINUE:
            g_ServiceStatus.dwCurrentState = SERVICE_RUNNING;
            break;

        default:
            break;
    }

    SetServiceStatus(g_ServiceStatusHandle, &g_ServiceStatus);

    return ERROR_CALL_NOT_IMPLEMENTED;
}


//サービスアプリケーション自身の実行ファイルパスと同じ場所にあるファイルに
//現在の時刻を書き続けるだけの処理を実装する
unsigned int WINAPI ServiceThread(LPVOID arg)
{
    char szFileName[MAX_PATH] = {'\0'};
    SYSTEMTIME localTime;

    GetModuleFileNameA(NULL, szFileName, sizeof(szFileName)/sizeof(szFileName[0]));
    PathRenameExtensionA(szFileName, ".txt");

    for (;;) {
        FILE *fp = fopen(szFileName, "a");
        if (fp != NULL) {
            GetLocalTime(&localTime);
            fprintf(fp, "%02d:%02d:%02d\n", 
                localTime.wHour, localTime.wMinute, localTime.wSecond);
            fclose(fp);
        }
        Sleep(1000);
    }

    return 0;
}

void WINAPI ServiceEntry(DWORD dwArgs, LPTSTR *pszArgv)
{
    unsigned int uiThreadID;
    HANDLE hThread;

    //サービススレッドを起動する
    hThread = (HANDLE)_beginthreadex(NULL,
        0,
        ServiceThread,
        NULL,
        0,
        &uiThreadID);

    
    //サービス制御ハンドラーを登録する
    g_ServiceStatusHandle = RegisterServiceCtrlHandlerEx(SERVICE_NAME, HandlerEx, NULL);
    
    

    //サービスの状態を設定する
    memset(&g_ServiceStatus, 0, sizeof(g_ServiceStatus));
    g_ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
    g_ServiceStatus.dwCurrentState = SERVICE_RUNNING;//サービス実行状態
    g_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE;//サービスの停止と、一時停止+続行を受け入れる
    g_ServiceStatus.dwWin32ExitCode = NO_ERROR;
    g_ServiceStatus.dwServiceSpecificExitCode = 0;
    g_ServiceStatus.dwCheckPoint = 0;
    g_ServiceStatus.dwWaitHint = 2000;
    
    SetServiceStatus(g_ServiceStatusHandle, &g_ServiceStatus);
}



BOOL InstallService()
{
    SC_HANDLE hSCM = NULL;
    SC_HANDLE hService = NULL;
    TCHAR szBinaryPathName[MAX_PATH];
    SERVICE_DESCRIPTION sd;
    BOOL bRet = FALSE;

    //サービス制御マネージャのデータベースのハンドルを取得する
    //第3引数に 「SC_MANAGER_CREATE_SERVICE」 を指定することで、
    //サービスを作成しサービス制御マネージャのデータベースに追加できるようにしている
    //つまり、CreateService関数の呼び出しをできるようにしているということになる。
    hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
    if (hSCM == NULL) {
        goto EXIT;
    }

    //自分自身の実行ファイルのフルパスを取得する
    GetModuleFileName(NULL, 
        szBinaryPathName, 
        sizeof(szBinaryPathName)/sizeof(szBinaryPathName[0]));


    //サービスを作成し、サービス制御マネージャのデータベースに追加する。
    hService = CreateService(hSCM, //サービス制御マネージャのハンドル
        SERVICE_NAME, //サービス名
        DISPLAY_NAME, //表示名
        SERVICE_CHANGE_CONFIG,//サービスの構成を変更できるようにする
        SERVICE_WIN32_OWN_PROCESS, //サービスアプリケーションが専用のプロセス内で動作
        SERVICE_DEMAND_START,//StartService関数が呼び出されたときに、サービスが開始される
        SERVICE_ERROR_IGNORE,//エラー発生時に、サービス開始操作を続行する
        szBinaryPathName, //サービスアプリケーションの実行ファイルパス
        NULL,
        NULL,
        NULL,
        NULL,
        NULL);

    if (hService == NULL) {
        goto EXIT;
    }

    sd.lpDescription = DESCRIPTION;
    
    ChangeServiceConfig2(hService, SERVICE_CONFIG_DESCRIPTION, &sd);

    bRet = TRUE;


EXIT:
    //クリーンアップ
    CloseServiceHandle(hService);
    CloseServiceHandle(hSCM);

    return bRet;
}


int APIENTRY _tWinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,
                     int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);


    SERVICE_TABLE_ENTRY ServiceTableEntry[] = {
        {SERVICE_NAME, ServiceEntry},
        {NULL, NULL}
    };

    StartServiceCtrlDispatcher(ServiceTableEntry);

    return 0;
}




このソースコードをビルドして、サービスアプリケーションの実行ファイルを生成する。
そして、管理ツールの「サービス」を管理者権限で起動し、[開始]ボタンを押し、サービスを開始する。



サービスアプリケーションが開始すると、サービスアプリケーションの実行ファイルと同じ場所にテキストファイルが作成され、1秒毎に現在の時刻が追記されていくことを確認することができる。








最後に、サービスのアンインストールを行う。

このサービスのアンインストールの実装も上記のファイルに追加および修正していく。


サービスのアンインストールをするには、サービス制御マネージャのデータベースに登録されたサービスを削除することになる。そのために使用する主なWin32APIは、DeleteService関数である。サービスをアンインストールする手順は以下のようになる。


1.OpenSCManager関数で、サービス制御マネージャに接続する

2.OpenService関数で、サービスのハンドルを取得する

3.DeleteService関数で、サービス制御マネージャのデータベースに登録されたサービスを削除する




注意点は、OpenService関数の第3引数のアクセス権に「DELETE」を指定して、DeleteServiceの呼び出しを有効にする必要があるということ。


実装例は、以下のようになる。UninstallService関数の中でサービスをアンインストールしている。そして、WinMain関数からUninstallService関数を呼び出すようにする。

#include "stdafx.h"
#include <Windows.h>
#include <strsafe.h>
#include <process.h>
#include <stdio.h>

#include <shlwapi.h>
#pragma comment(lib, "shlwapi.lib")

#define SERVICE_NAME TEXT("SampleServiceApp")
#define DISPLAY_NAME TEXT("SampleServiceApp")
#define DESCRIPTION TEXT("Sample Service Application")

SERVICE_STATUS g_ServiceStatus;
SERVICE_STATUS_HANDLE g_ServiceStatusHandle;

//サービス制御ハンドラー
DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, 
                       PVOID pvEventData, PVOID pvContext)
{
    switch (dwControl) {
    
        case SERVICE_CONTROL_STOP:
            g_ServiceStatus.dwCurrentState = SERVICE_STOPPED;
            break;

        case SERVICE_CONTROL_SHUTDOWN:
            g_ServiceStatus.dwCurrentState = SERVICE_STOPPED;
            break;

        case SERVICE_CONTROL_PAUSE:
            g_ServiceStatus.dwCurrentState = SERVICE_PAUSED;
            break;

        case SERVICE_CONTROL_CONTINUE:
            g_ServiceStatus.dwCurrentState = SERVICE_RUNNING;
            break;

        default:
            break;
    }

    SetServiceStatus(g_ServiceStatusHandle, &g_ServiceStatus);

    return ERROR_CALL_NOT_IMPLEMENTED;
}


//サービスアプリケーション自身の実行ファイルパスと同じ場所にあるファイルに
//現在の時刻を書き続けるだけの処理を実装する
unsigned int WINAPI ServiceThread(LPVOID arg)
{
    char szFileName[MAX_PATH] = {'\0'};
    SYSTEMTIME localTime;

    GetModuleFileNameA(NULL, szFileName, sizeof(szFileName)/sizeof(szFileName[0]));
    PathRenameExtensionA(szFileName, ".txt");

    for (;;) {
        FILE *fp = fopen(szFileName, "a");
        if (fp != NULL) {
            GetLocalTime(&localTime);
            fprintf(fp, "%02d:%02d:%02d\n", 
                localTime.wHour, localTime.wMinute, localTime.wSecond);
            fclose(fp);
        }
        Sleep(1000);
    }

    return 0;
}

void WINAPI ServiceEntry(DWORD dwArgs, LPTSTR *pszArgv)
{
    unsigned int uiThreadID;
    HANDLE hThread;

    //サービススレッドを起動する
    hThread = (HANDLE)_beginthreadex(NULL,
        0,
        ServiceThread,
        NULL,
        0,
        &uiThreadID);

    
    //サービス制御ハンドラーを登録する
    g_ServiceStatusHandle = RegisterServiceCtrlHandlerEx(SERVICE_NAME, HandlerEx, NULL);
    
    

    //サービスの状態を設定する
    memset(&g_ServiceStatus, 0, sizeof(g_ServiceStatus));
    g_ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
    g_ServiceStatus.dwCurrentState = SERVICE_RUNNING;//サービス実行状態
    g_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE;//サービスの停止と、一時停止+続行を受け入れる
    g_ServiceStatus.dwWin32ExitCode = NO_ERROR;
    g_ServiceStatus.dwServiceSpecificExitCode = 0;
    g_ServiceStatus.dwCheckPoint = 0;
    g_ServiceStatus.dwWaitHint = 2000;
    
    SetServiceStatus(g_ServiceStatusHandle, &g_ServiceStatus);
}



BOOL InstallService()
{
    SC_HANDLE hSCM = NULL;
    SC_HANDLE hService = NULL;
    TCHAR szBinaryPathName[MAX_PATH];
    SERVICE_DESCRIPTION sd;
    BOOL bRet = FALSE;

    //サービス制御マネージャのデータベースのハンドルを取得する
    //第3引数に 「SC_MANAGER_CREATE_SERVICE」 を指定することで、
    //サービスを作成しサービス制御マネージャのデータベースに追加できるようにしている
    //つまり、CreateService関数の呼び出しをできるようにしているということになる。
    hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
    if (hSCM == NULL) {
        goto EXIT;
    }

    //自分自身の実行ファイルのフルパスを取得する
    GetModuleFileName(NULL, 
        szBinaryPathName, 
        sizeof(szBinaryPathName)/sizeof(szBinaryPathName[0]));


    //サービスを作成し、サービス制御マネージャのデータベースに追加する。
    hService = CreateService(hSCM, //サービス制御マネージャのハンドル
        SERVICE_NAME, //サービス名
        DISPLAY_NAME, //表示名
        SERVICE_CHANGE_CONFIG,//サービスの構成を変更できるようにする
        SERVICE_WIN32_OWN_PROCESS, //サービスアプリケーションが専用のプロセス内で動作
        SERVICE_DEMAND_START,//StartService関数が呼び出されたときに、サービスが開始される
        SERVICE_ERROR_IGNORE,//エラー発生時に、サービス開始操作を続行する
        szBinaryPathName, //サービスアプリケーションの実行ファイルパス
        NULL,
        NULL,
        NULL,
        NULL,
        NULL);

    if (hService == NULL) {
        goto EXIT;
    }

    sd.lpDescription = DESCRIPTION;
    
    ChangeServiceConfig2(hService, SERVICE_CONFIG_DESCRIPTION, &sd);

    bRet = TRUE;


EXIT:
    //クリーンアップ
    CloseServiceHandle(hService);
    CloseServiceHandle(hSCM);

    return bRet;
}

BOOL UninstallService()
{
    SC_HANDLE hSCM = NULL;
    SC_HANDLE hService = NULL;
    BOOL bRet = FALSE;

    //サービス制御マネージャに接続する
    hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);
    if (hSCM == NULL) {
        goto EXIT;
    }

    //サービスのハンドルを取得する
    //第3引数のアクセス権に DELETE を指定して、DeleteService呼び出しを有効にする
    hService = OpenService(hSCM, SERVICE_NAME, DELETE);
    if (hService == NULL) {
        goto EXIT;
    }

    //サービス制御マネージャのデータベースに登録されたサービスを削除する
    if (DeleteService(hService) == FALSE) {
        goto EXIT;
    }

    bRet = TRUE;


EXIT:
    //クリーンアップ
    CloseServiceHandle(hSCM);
    CloseServiceHandle(hService);
    
    return bRet;
}


int APIENTRY _tWinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,
                     int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    UninstallService();

    return 0;
}


このソースコードをビルドし実行ファイルを生成して、これを管理者権限で実行するとサービス制御マネージャのデータベースからサービスが削除される。管理ツールの「サービス」に表示されるサービスの一覧から、登録してあったサービス名が削除されていれば、サービスのアンインストールは成功したということになる。


あとは、WinMain関数でInstallService関数呼び出し、StartServiceCtrlDispatcher関数の呼び出し、Uninstall関数の呼び出しをそれぞれ実装し、コマンドライン引数でそれぞれの動作を切り替えるようにすれば良い。


以上。

C#でSSL(サーバー証明書の検証あり版)

C#SSL通信を行う。

サーバー証明書の検証も行う。



例として、インターネット上のWebサーバーとSSL通信を行う。


手順は、以下のようになる。

1Webサーバー名からWebサーバーのIPアドレスDNSでルックアップ
2TcpClientを用いて、Webサーバーの443番ポートに接続する
3SslStreamを生成し、でSSL通信を開始
4SslStream#AuthenticateAsClientメソッドで、サーバー証明書の検証を行う
5HTTPリクエストを送信する
6HTTPレスポンスを受信する


PrintCertificateメソッドは、サーバー証明書の内容をコンソールに表示するためのメソッド。
RemoteCertificateValidationCallbackメソッドで、証明書の検証を行なっている。

using System;
using System.Net;
using System.Net.Sockets;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.IO;
using System.Text;

public class Program
{
    //証明書の内容を表示するメソッド
    private static void PrintCertificate(X509Certificate certificate)
    {
        Console.WriteLine("===========================================");
        Console.WriteLine("Subject={0}", certificate.Subject);
        Console.WriteLine("Issuer={0}", certificate.Issuer);
        Console.WriteLine("Format={0}", certificate.GetFormat());
        Console.WriteLine("ExpirationDate={0}", certificate.GetExpirationDateString());
        Console.WriteLine("EffectiveDate={0}", certificate.GetEffectiveDateString());
        Console.WriteLine("KeyAlgorithm={0}", certificate.GetKeyAlgorithm());
        Console.WriteLine("PublicKey={0}", certificate.GetPublicKeyString());
        Console.WriteLine("SerialNumber={0}", certificate.GetSerialNumberString());
        Console.WriteLine("===========================================");
    }

    //サーバー証明書を検証するためのコールバックメソッド
    private static Boolean RemoteCertificateValidationCallback(Object sender, 
        X509Certificate certificate, 
        X509Chain chain, 
        SslPolicyErrors sslPolicyErrors)
    {
        PrintCertificate(certificate);

        if (sslPolicyErrors == SslPolicyErrors.None)
        {
            Console.WriteLine("サーバー証明書の検証に成功しました\n");
            return true;
        }
        else
        {
            //何かサーバー証明書検証エラーが発生している

            //SslPolicyErrors列挙体には、Flags属性があるので、
            //エラーの原因が複数含まれているかもしれない。
            //そのため、&演算子で1つ1つエラーの原因を検出する。
            if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateChainErrors) == 
                SslPolicyErrors.RemoteCertificateChainErrors)
            {
                Console.WriteLine("ChainStatusが、空でない配列を返しました");
            }

            if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNameMismatch) == 
                SslPolicyErrors.RemoteCertificateNameMismatch)
            {
                Console.WriteLine("証明書名が不一致です");
            }

            if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNotAvailable) == 
                SslPolicyErrors.RemoteCertificateNotAvailable)
            {
                Console.WriteLine("証明書が利用できません");
            }

            //検証失敗とする
            return false;
        }
    }

    public static void Main()
    {
        //ホスト名とポート番号を指定
        String hostName = "google.com";
        Int32 port = 443;

        using (TcpClient client = new TcpClient())
        {
            //接続先Webサーバー名からIPアドレスをルックアップ
            IPAddress[] ipAddresses = 
                Dns.GetHostAddresses(hostName);

            //Webサーバーに接続する
            client.Connect(new IPEndPoint(ipAddresses[0], port));
            
            //SSL通信の開始
            using (SslStream sslStream = 
                new SslStream(client.GetStream(), false, RemoteCertificateValidationCallback))
            {
                //サーバーの認証を行う
                //これにより、RemoteCertificateValidationCallbackメソッドが呼ばれる
                sslStream.AuthenticateAsClient(hostName);

                //HTTPリクエストをサーバーに送信する
                Byte[] req = 
                    Encoding.ASCII.GetBytes(String.Format("GET / HTTP/1.0\r\nHost: {0}\r\n\r\n", hostName));
                sslStream.Write(req);
                sslStream.Flush();

                //サーバーから受信したHTTPレスポンスを読み込んで
                //コンソールに表示数
                Byte[] res = new Byte[1024];
                Int32 n;
                while ( (n = sslStream.Read(res, 0, res.Length) ) > 0)
                {
                    String s = Encoding.ASCII.GetString(res, 0, n);
                    Console.WriteLine(s);
                }
            }
        }
    }
}





ソースコードの補足説明

SslStreamクラスコンストラクタの第3引数で指定した、RemoteCertificateValidationCallbackというコールバックメソッドでサーバー証明書の検証を行っている。


RemoteCertificateValidationCallbackは、デリゲートとして以下のように定義されている。

public delegate bool RemoteCertificateValidationCallback(
	Object sender,
	X509Certificate certificate,
	X509Chain chain,
	SslPolicyErrors sslPolicyErrors
)



サーバー証明書の検証を行うためには、このメソッドの第3引数である、sslPolicyErrorsを用いる。
sslPolicyErrorsは、SslPolicyErrors列挙体のオブジェクトであり、SslPolicyErrors列挙体は、以下のように定義されている。

namespace System.Net.Security
{
    // 概要:
    //     SSL (Secure Socket Layer) のポリシー エラーを列挙します。
    [Flags]
    public enum SslPolicyErrors
    {
        // 概要:
        //     SSL のポリシー エラーはありません。
        None = 0,
        //
        // 概要:
        //     証明書が利用できません。
        RemoteCertificateNotAvailable = 1,
        //
        // 概要:
        //     証明書名が不一致です。
        RemoteCertificateNameMismatch = 2,
        //
        // 概要:
        //     System.Security.Cryptography.X509Certificates.X509Chain.ChainStatus が、空でない配列を返しました。
        RemoteCertificateChainErrors = 4,
    }
}



SslPolicyErrors列挙体には Flags属性 が付いているため、サーバー証明書の検証エラーの複合的な原因を、&演算子を用いて詳細に調べることができる





実行結果

===========================================
Subject=CN=*.google.com, O=Google Inc, L=Mountain View, S=California, C=US
Issuer=CN=Google Internet Authority, O=Google Inc, C=US
Format=X509
ExpirationDate=2014/01/01 0:58:50
EffectiveDate=2013/04/11 21:52:18
KeyAlgorithm=1.2.840.10045.2.1
PublicKey=0481072B5CEC82040D3E50F8B7D07A47423C0CB215E0B716AD744E46166782A4729BCA
A1155C1332105EB0A331B721BB1917106E56464312861ED7ADD54464F4B1
SerialNumber=40950B0A00010000839D
===========================================
サーバー証明書の検証に成功しました

HTTP/1.0 301 Moved Permanently
Location: https://www.google.com/
Content-Type: text/html; charset=UTF-8
Date: Tue, 30 Apr 2013 11:28:01 GMT
Expires: Thu, 30 May 2013 11:28:01 GMT
Cache-Control: public, max-age=2592000
Server: gws
Content-Length: 220
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN

<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="https://www.google.com/">here</A>.
</BODY></HTML>



実際には、Main関数のhostNameに不正なサーバー証明書を用いているWebサーバー名を指定しないと、検証エラーを発生させることができません。ですので自前でオレオレ証明書を作成して、自前のWebサーバーにセットして試すのが良いかと思います。