Vista以降では安易にスレッドを作るな!?

LinuxやBSDなどのUNIX互換のOSと違って、Windowsではスレッドを起こすのは普通に行われてることだと思います。で、スレッドを起こすには、CreateThread API もしくは、Cランタイムライブラリの_beginthreadex関数を使うことになります。

が、一方でスレッドを生成するのではなく、スレッドプールを利用する、というケースもあり、パフォーマンス的な事を考えれば、むしろこちらの方が多いかもしれません。

Windows 2000/XPなどのNT5.x系では、QueueUserWorkItem APIというスレッドプールに関するAPIが使えます。。。けど、はっきりいって、このAPI、よほど簡単なロジックでないと使えません。いったんこのAPIでキューに入れてしまうと、もう手が出ません。外からキャンセルできないし、強制終了させようとしてもTerminateThread APIは使っちゃだめ!ですし・・・。
結局、ちょっと凝ったことさせようと思ったら、自前でI/O完了ポートを駆使してスレッドプールを実装する他なかったと思います。

しかし、Windows Vista以降のNT6.x系のWindowsでは、より進化したスレッドプールAPIが追加されています。もはや、Vista以降のWindowsで、CreateThreadや_beginthreadex関数を使ってスレッドを起こすコーディングはダサイといわざる得ません(^^;;;

というか、CPUがデュアルコアは当たり前、マルチコアがデフォルトってな状況の中で、これからはメニーコアだ! っていう時代なので、アプリケーション開発者側が、コア数のことまで考えて組む・・・とかもう無理!そんなのは限られたスーパープログラマーしかできんわ(笑) ってことです。

だから、CreateThread APIや_beginthreadex関数でスレッドを起こす前提のロジックでコーディングしちゃだめ。ってことに。

ただ・・・スレッドを作ってそこにウィンドウを作成する、というようなケースには向きません。とどのつまり、プロセス開始から終了まで生存するようなスレッドの場合、スレッドプールに処理を投げる意味はありません。あくまで、一つのスレッドの生成・削除が頻繁に行われるケースや、スレッドの生存期間が短いケース等でスレッドプールの恩恵を受けることができると思います。

ま、今更ですが(^^;;; この新たに追加されたスレッドプールなAPI群は、スレッドの生成と管理をすべてWindowsが肩代わりしてマシンに積まれているCPUの数(コア数?)と負荷状態に応じて最適な性能を発揮できるようになっているようです(・・・なっているはずです)。

例えば、ちょっとしたユーティリティアプリなんかで、単純な処理を非同期(並行)処理させる場合、TrySubmitThreadpoolCallbackもしくは、CreateThreadpoolWork/SubmitThreadpoolWorkでほとんど事足りると思います。

CreateThreadや_beginthreadexのあの引数の多さにうんざりすることが無くなり、スレッドハンドルを管理するコード、同期のためにイベントオブジェクトを作成して待機するようなコード諸々を実装する手間が大幅に軽減されます。ヒャッホーーー!

/********************************************************
  簡単なスレッドプールサンプル
********************************************************/
#pragma comment(lib,"comsuppw.lib")
#define _WIN32_WINNT 0x0600

#include <windows.h>
#include <tchar.h>
#include <cstdio>
#include <comutil.h>

using namespace std;

#define NUM 10

/********************************************************
クリティカルセクションのラッパークラス
********************************************************/
class CCriticalSection
{
  CRITICAL_SECTION cs;

public:
  CCriticalSection(){ InitializeCriticalSectionAndSpinCount(&cs,4000); }
  ~CCriticalSection(){ DeleteCriticalSection(&cs); }

  void Enter(){ EnterCriticalSection(&cs); }
  void Leave(){ LeaveCriticalSection(&cs); }
};

/********************************************************
 スレッドに渡す何らかのデータ(適当です)
********************************************************/
struct Data : CCriticalSection
{
  int i;
  _bstr_t bs;
};

/********************************************************
  コールバック関数
********************************************************/
VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE pInstance,LPVOID pvParam,PTP_WORK pWork)
{
  Data *pData = (Data *)pvParam;
  DWORD tid = GetCurrentThreadId();

  //なんらかの処理
  pData->Enter();
  {
    _tprintf(TEXT("[%06d][%02d] %s\n"),tid,pData->i++,(PCTSTR)pData->bs);
  }
  pData->Leave();
}

/********************************************************
  エントリポイント
********************************************************/
void main(void)
{
  PTP_WORK pWork;
  Data param;

  param.i = 0;
  param.bs = TEXT("なにがしらのデータ");

  pWork= CreateThreadpoolWork(WorkCallback,(PVOID)&amp;param,NULL);

  //スレッドプールにNUM回作業を投げる。
  for(int i=0;i<NUM;i++)
    {
      /*******************************************************
      スレッドプールのキューに送信

      SubmitThreadpoolWorkで同じPTP_WORKを繰り返す場合、
      コールバック関数に渡されるパラメータはCreateThreadpoolWorkをコールしたときの
      パラメータが渡されるのでその点注意しなければなりません。
      *******************************************************/
      SubmitThreadpoolWork(pWork);
    }

  //投げた作業が終了するまで待つ。
  WaitForThreadpoolWorkCallbacks(pWork,FALSE);
  
  //2013/6/18 追記 閉じるのを忘れてました。
  CloseThreadpoolWork(pWork);
}

当然ですが、上記はXPでは実行できません。

他にも、タイマーオブジェクトを利用した関数の繰り返し処理、jscriptでいうところのsetTimeout / setIntervalメソッドみたいな、一定時間毎にコールバック関数を実行してくれるCreateThreadpoolTimer APIや、カーネルオブジェクトがシグナル状態になったらコールバック関数を実行してくれる CreateThreadpoolWait API、ReadFile/WriteFileなどのI/O非同期処理に利用できるCreateThreadpoolIo API。

一連のスレッドプールに関するAPIは非常に強力で、使い勝手もいい。デフォルトのスレッドプールの動作が気に入らなければ、カスタマイズしたスレッドプールを利用することもできる。

自分的に便利だと思ったのが、ReadFIle/WriteFileでI/O非同期処理に利用できる、CreateThreadpoolIo/StartThreadpoolIo。
今まではI/O完了ポートで通知を受け取って・・・というようなコードを書いてましたが、Vista以降のOSに限定すれば、これらのスレッドプールAPIを使うことでコード量が減ります。

ReadDirectoryChangesW APIを使ったディレクトリへの変更を監視するコードをスレッドプールを使ったコードに強引にリプレースしてみました。

まずは、昔作った、WindowsXPで動作する、IO完了ポートとワーカースレッドを単純に作って利用したバージョン。

#pragma comment(lib,"comsuppw.lib")

#include <windows.h>
#include <process.h>
#include <conio.h>
#include <comutil.h>
#include <cstdio>
#include <cstring>
#include <tchar.h>

using namespace std;

//バッファサイズは適当です。
#define BUFFER_SIZE 1024
#define COMPKEY_QUIT -1
#define COMPKEY_DIR 1

//使用するデータをパックした構造体
typedef struct _IocpData : OVERLAPPED
{
  TCHAR szDir[MAX_PATH];
  HANDLE hDir;
  BYTE pvData[BUFFER_SIZE];  //FILE_NOTIFY_INFORMATION構造体に必要なメモリブロック
} IocpData,*PIOCPDATA;

//プロトタイプ
unsigned int __stdcall WatchDirectory(PVOID pvParam);
unsigned int __stdcall IoCompletionCallback(LPVOID pvParam);

HANDLE g_hQuitEvent = NULL;

int _tmain(int argc,TCHAR**argv)
{
  if(argc < 2)
    {
      _tprintf(TEXT("ディレクトリを指定してください。\n"));
      goto cleanup;
    }

  //待機終了通知のためのイベントオブジェクトを初期化
  g_hQuitEvent = CreateEvent(NULL,TRUE,FALSE,NULL);
  
  HANDLE hThread = (HANDLE)_beginthreadex(NULL,0,WatchDirectory,(PVOID)argv[1],0,NULL);

  if(WAIT_TIMEOUT == WaitForSingleObject(g_hQuitEvent,1000))
    {
      while(TRUE)
        {
          if('q' == _gettch())
            {
              //終了通知を投げる。
              SetEvent(g_hQuitEvent);

              //スレッドが終了するまで待機
              WaitForSingleObject(hThread,INFINITE);

              break;
            }
        }
    }

  CloseHandle(hThread);
  CloseHandle(g_hQuitEvent);

cleanup:
  return 0;
}

/********************************************************
ディレクトリを監視
********************************************************/
unsigned int __stdcall WatchDirectory(PVOID pvParam)
{
  PCTSTR pDir = (PCTSTR)pvParam;

 IocpData ioReq;

  //ワーカースレッドはとりあえず2個。
  HANDLE hThreads[2] = {0};

  //FILE_FLAG_OVERLAPPEDを指定してディレクトリを開く。
  _tcscpy(ioReq.szDir,pDir);
  ioReq.hDir = CreateFile(pDir,
                          FILE_LIST_DIRECTORY,
                          FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                          NULL,
                          OPEN_EXISTING,
                          FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
                          NULL);

  if(ioReq.hDir == INVALID_HANDLE_VALUE)
    {
      _tprintf(TEXT("ディレクトリのオープンに失敗しました。\n"));
      SetEvent(g_hQuitEvent);
      return 0;
    }

  //IO完了ポートの作成とディレクトリハンドルの関連付け
  HANDLE hIocp = CreateIoCompletionPort(ioReq.hDir,NULL,COMPKEY_DIR,0);
  if(hIocp == INVALID_HANDLE_VALUE)
    {
      _tprintf(TEXT("I/O完了ポートの作成に失敗しました。\n"));
      SetEvent(g_hQuitEvent);
      return 0;
    }

  //IO完了ポートを待機するワーカースレッドを起動
  //監視するディレクトリ一つなので、ワーカースレッドを二つも作る必要はないと思いますが・・・
  //IO完了ポートに複数の(ディレクトリの)ハンドルを関連付ける場合に複数のワーカースレッドを作る方がいいかも。
  for(int i=0;i<2;i++)
    hThreads[i] = (HANDLE)_beginthreadex(NULL,0,IoCompletionCallback,(PVOID)hIocp,0,NULL);

  _tprintf(TEXT("監視用タスクが待機状態に入ります。\n"));

  PostQueuedCompletionStatus(hIocp,0,COMPKEY_DIR,(LPOVERLAPPED)&ioReq);

  //終了イベントがシグナル状態になるまで待機する。
  WaitForSingleObject(g_hQuitEvent,INFINITE);

  for(int i=0;i<2;i++)
    PostQueuedCompletionStatus(hIocp,0,COMPKEY_QUIT,NULL);

  //ワーカースレッドが終了するまで待機
  WaitForMultipleObjects(2,hThreads,TRUE,INFINITE);

  for(int i=0;i<2;i++)
    CloseHandle(hThreads[i]);
  
  _tprintf(TEXT("監視用タスクが待機状態から解放され終了します。\n"));

  CloseHandle(hIocp);
  return 1;
}

/*****************************************************************
 IO完了通知を受けるワーカースレッド関数
*****************************************************************/
unsigned int __stdcall IoCompletionCallback(PVOID pvParam)
{
  HANDLE hIocp = (HANDLE)pvParam;

  DWORD dwBytes = 0L;
  ULONG_PTR ulCompKey = 0L;
  LPOVERLAPPED pOverlapped = NULL;
  IocpData *pIoReq = NULL;

  while(TRUE)
    {
      GetQueuedCompletionStatus(hIocp,&dwBytes,&ulCompKey,&pOverlapped,INFINITE);
      PFILE_NOTIFY_INFORMATION pFileNotifyInfo = NULL;
      pIoReq = (IocpData*)pOverlapped;

      if(ulCompKey == (ULONG_PTR)-1)
        break;

      //FILE_NOTIFY_INFORMATION構造体をコピーする。
      if(dwBytes > 0)
        {
          PFILE_NOTIFY_INFORMATION pfni = (PFILE_NOTIFY_INFORMATION)pIoReq->pvData;
          SIZE_T cb = sizeof(FILE_NOTIFY_INFORMATION) + pfni->FileNameLength;
          
          pFileNotifyInfo = (PFILE_NOTIFY_INFORMATION)new BYTE[cb];
          CopyMemory((PVOID)pFileNotifyInfo,(CONST PVOID)pfni,cb);
        }

      //次の監視を開始
      if(pOverlapped)
        ReadDirectoryChangesW(pIoReq->hDir,
                              (PVOID)pIoReq->pvData,
                              BUFFER_SIZE,
                              TRUE,
                              FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME,
                              NULL,
                              pOverlapped,
                              NULL);

      /**************************************************************************************
      コールバック関数に渡すNotifyParams構造体に値をセットしてコールバック関数をコールする。
      
      ※コールバック関数が制御を返さないかぎりスレッドがワーカースレッドがビジーになる。
      **************************************************************************************/
      if(dwBytes > 0)
        {
          _bstr_t filename,fullpath;

          //FILE_NOTIFY_INFORMATION構造体のFileNameメンバの終端をヌル文字で閉じる。
          *(PWSTR)((PBYTE)(pFileNotifyInfo->FileName) + pFileNotifyInfo->FileNameLength) = L'&#092;&#048;';

          //変更のあったファイルのフルパスを得る。
          filename = pFileNotifyInfo->FileName;
          fullpath = pIoReq->szDir;
          fullpath += (_bstr_t(TEXT("\\")) + filename);

          _tprintf(TEXT("[THREAD:%d] [変更]%s\n"),GetCurrentThreadId(),(PCTSTR)fullpath);

          //確保したメモリを解放
          delete [] (PBYTE)pFileNotifyInfo;
        }
    }

  return 1;
}

要点は、スレッドを作成して監視が終了するまで待機。ReadDirectoryChangeW APIの非同期処理が完了するとIO完了ポートのキューにI/O完了パケットが追加され、ワーカースレッドのGetQueuedCompletionStatus APIが制御を戻すことでReadDirectoryChangeW APIの処理結果のデータを得て、再びReadDirectoryChangeW APIをコールし非同期処理を継続します。

続いて、上記をVista以降のスレッドプールのAPIを利用したバージョン。

#pragma comment(lib,"comsuppw.lib")

#include <windows.h>
#include <comutil.h>
#include <cstdio>
#include <cstring>
#include <tchar.h>
#include <conio.h>

using namespace std;

//バッファサイズは適当です。
#define BUFFER_SIZE 1024

//使用するデータをパックした構造体
typedef struct _IocpData : OVERLAPPED
{
  TCHAR szDir[MAX_PATH];
  HANDLE hDir;
  BYTE pvData[BUFFER_SIZE];  //FILE_NOTIFY_INFORMATION構造体に必要なメモリブロック
} IocpData,*PIOCPDATA;

//プロトタイプ
VOID CALLBACK WatchDirectoryEx(PTP_CALLBACK_INSTANCE pInstance,LPVOID pvParam,PTP_WORK pWork);
VOID CALLBACK IoCompletionCallback(PTP_CALLBACK_INSTANCE pInstance,PVOID pvParam,PVOID pOverlapped,ULONG IoResult,ULONG_PTR ulBytes,PTP_IO pio);

HANDLE g_hQuitEvent = NULL;

int _tmain(int argc,TCHAR**argv)
{
  if(argc < 2)
    {
      _tprintf(TEXT("ディレクトリを指定してください。\n"));
      goto cleanup;
    }

  //待機終了通知のためのイベントオブジェクトを初期化
  g_hQuitEvent = CreateEvent(NULL,TRUE,FALSE,NULL);

  //作業を作成
  PTP_WORK pWork = CreateThreadpoolWork(WatchDirectoryEx,(PVOID)argv[1],NULL);

  //スレッドプールに作業を投げる。
  SubmitThreadpoolWork(pWork);

  if(WAIT_TIMEOUT == WaitForSingleObject(g_hQuitEvent,1000))
    {
      while(TRUE)
        {
          if('q' == _gettch())
            {
              //終了通知を投げる。
              SetEvent(g_hQuitEvent);
              
              //投げた作業が終了するまで待つ。
              WaitForThreadpoolWorkCallbacks(pWork,FALSE);
              
              break;
            }
        }
    }

cleanup:
  return 0;
}

/********************************************************
ディレクトリを監視
********************************************************/
VOID CALLBACK WatchDirectoryEx(PTP_CALLBACK_INSTANCE pInstance,LPVOID pvParam,PTP_WORK pWork)
{
  PCTSTR pDir = (PCTSTR)pvParam;
  IocpData ioReq;

  //FILE_FLAG_OVERLAPPEDを指定してディレクトリを開く。
  _tcscpy(ioReq.szDir,pDir);
  ioReq.hDir = CreateFile(pDir,
                          FILE_LIST_DIRECTORY,
                          FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                          NULL,
                          OPEN_EXISTING,
                          FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
                          NULL);

  if(ioReq.hDir == INVALID_HANDLE_VALUE)
    {
      _tprintf(TEXT("ディレクトリのオープンに失敗しました。\n"));
      SetEvent(g_hQuitEvent);
      return;
    }

  PTP_IO pio = CreateThreadpoolIo(ioReq.hDir,IoCompletionCallback,(PVOID)&ioReq,NULL);

  _tprintf(TEXT("監視用タスクが待機状態に入ります。\n"));
  
  //コールバック関数をコールしてディレクトリ監視を開始。
  IoCompletionCallback(NULL,(PVOID)&ioReq,(LPOVERLAPPED)&ioReq,NO_ERROR,0,pio);

  //終了イベントがシグナル状態になるまで待機する。
  WaitForSingleObject(g_hQuitEvent,INFINITE);

  //現在キューに残っている作業をキャンセルする。
  WaitForThreadpoolIoCallbacks(pio,TRUE);

  //ハンドルを閉じる。
  CloseHandle(ioReq.hDir);
  CloseThreadpoolIo(pio);

  _tprintf(TEXT("監視用タスクが待機状態から解放され終了します。\n"));
}

/*****************************************************************
 IO完了コールバック関数
*****************************************************************/
VOID CALLBACK IoCompletionCallback(PTP_CALLBACK_INSTANCE pInstance,PVOID pvParam,PVOID pOverlapped,ULONG IoResult,ULONG_PTR ulBytes,PTP_IO pio)
{
  PFILE_NOTIFY_INFORMATION pFileNotifyInfo;
  IocpData *pIoReq = (IocpData*)pOverlapped;

  //転送されたバイト数が0以上なら、
  //FILE_NOTIFY_INFORMATION構造体のメモリを確保してコピーしておく。
  if(ulBytes > 0)
    {
      //転送されたFILE_NOTIFY_INFORMATION構造体をコピーする。
      PFILE_NOTIFY_INFORMATION pfni = (PFILE_NOTIFY_INFORMATION)pIoReq->pvData;
      SIZE_T cb = sizeof(FILE_NOTIFY_INFORMATION) + pfni->FileNameLength;

      pFileNotifyInfo = (PFILE_NOTIFY_INFORMATION)new BYTE[cb];
      CopyMemory((PVOID)pFileNotifyInfo,(CONST PVOID)pfni,cb);
    }

  //監視続行
  if(pOverlapped)
    {
      //スレッドプールにIO完了オブジェクトを関連付ける
      StartThreadpoolIo(pio);

      //ディレクトリ監視を開始するが、非同期なので制御が直に返る。
      ReadDirectoryChangesW(pIoReq->hDir,
                            pIoReq->pvData,
                            BUFFER_SIZE,
                            TRUE,
                            FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME,
                            NULL,
                            (LPOVERLAPPED)pOverlapped,
                            NULL);
    }

  //転送されたバイト数が0以上なら、何らかの処理をする。
  //ここでは変更のあったファイルを出力する。
  if(ulBytes > 0)
    {
      _bstr_t filename,fullpath;

      //FILE_NOTIFY_INFORMATION構造体のFileNameメンバの終端をヌル文字で閉じる。
      *(PWSTR)((PBYTE)(pFileNotifyInfo->FileName) + pFileNotifyInfo->FileNameLength) = L'&#092;&#048;';

      //変更のあったファイルのフルパスを得る。
      filename = pFileNotifyInfo->FileName;
      fullpath = pIoReq->szDir;
      fullpath += (_bstr_t(TEXT("\\")) + filename);

      _tprintf(TEXT("[THREAD:%d] [変更]%s\n"),GetCurrentThreadId(),(PCTSTR)fullpath);

      //確保したメモリを解放
      delete [] (PBYTE)pFileNotifyInfo;
    }
}

あまり違いがないように思いますが、スレッドを何個作るべき?だとか細々とした調整などが不要になり、何より_beginthread関数などのプリミティブなAPIを使わなくても良くなりました。以前なら、スレッド処理をラップする、なんらかのクラスライブラリが必須だったと思いますが、ちょっとしたツールを書くときはこれらの新しいスレッドプールなAPIを使えば良くなりました。

ま、ちょっとしたツールを作るにはC#を使えば済む話で、わざわざC++を使う必要性があるとは思えませんが・・・。ま、要するに自己満です(^^;;;

_bstr_t は何気に便利だな、とか。

昔書いてたメモ帳から転記。今更感がマックスの、備忘録メモ。

ちょこっとCOMクライアントとかのテストコード書く際に、ワイド文字列(主にWindowsで言うところのUNICODE)と、マルチバイト文字列の相互変換が煩わしいときが多々ある。

まぁ、変換用に関数一個作っとけばいい話なんですが・・・。正直、Win32 APIのMultiByteToWideChar/WideCharToMultibyte は使いたくない。理由はただ一つ。コイツらの使い勝手の悪さときたらどうしようもない。ってとこ。引数が5個も6個もいるし、何より、最低限2回のAPIコールが必要。↓のような感じ。

/*
  マルチバイト文字列 ==> ワイド文字列
*/

#include <windows.h>

//ヒープを使用。
PWSTR AllocString(PCSTR& pStr)
{
  PWSTR pWStr = NULL;
  SIZE_T cchWStr = 0;

  if(pStr == NULL)
    goto cleanup;
  
  cchWStr = MultiByteToWideChar(CP_ACP,MB_PRECOMPOSED,pStr,-1,NULL,0);

  if(cchWStr <= 0)
    goto cleanup;

  if((pWStr = (PWSTR)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,cchWStr*sizeof(WCHAR))) == NULL)
    goto cleanup;

  if(!MultiByteToWideChar(CP_ACP,MB_PRECOMPOSED,pStr,-1,pWStr,cchWStr))
    FreeString(pWStr);

cleanup:
  return pWStr;
}

//ヒープ解放
BOOL FreeString(PWSTR& pWStr)
{
  BOOL bRet = FALSE;

  if(pWStr && (bRet = HeapFree(GetProcessHeap(),0,reinterpret_cast<PVOID>(pWStr))))
    pWStr = NULL;

  return bRet;
}

でも、こんな感じだと、明らかにテストしたいコードより遙かに長いコード行・・・何か腹立つ。
お金の掛からないVC++ Express Edition を使っていて贅沢は言えませんが・・・なんかで代用できないものか・・・。

で、最近MSDNのドキュメント見てて気付いた。
_bstr_t クラスって便利やないかい!、と。

もともとは、COMでの文字列表現、BSTR型のラップするスマートポインタみたいなクラス。BSTR型はちょっと特殊で簡単に言えば文字列の長さとバッファを合わせたような型。BSTR型をPWSTR型と混同しているケースを極たまに見ます。まぁ、そんなことはどうでもよく。

_bstr_t クラスは、PCWSTR,PCSTRの両方をコンストラクタで受け付けてくれます。しかも、キャスト演算子をオーバーロードしてくれてるので、引数にPCWSTRもしくはPCSTRが必要なAPIや関数にそのまま放り込めます。

※注意点
これらのキャスト演算子は内部バッファへのナマのポインタを返すので、やたらめったら使用すると、おそらくどこかでクラッシュするバグを抱えるでせう。

#pragma comment(lib,"user32.lib")
#pragma comment(lib,"comsuppw.lib")

#include <windows.h>
#include <comutil.h>
#include <tchar.h>

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
  _bstr_t bStr("テストでげす");
  
  //ANSI版
  MessageBoxA(NULL,bStr,"ANSI版",MB_OK);

  //UNICODE版
  MessageBoxW(NULL,bStr,L"UNICODE版",MB_OK);
  
  return 0;
}

/*
ANSIバージョンのAPIは、内部でマルチバイト文字<==>ワイド文字の変換を行って
UNICODE版APIをコールしているだけのようなので、このコード自体無意味。
*/

スコープを抜けると自動的にSysFreeStringをコールしてくれるみたいなので、解放処理も必要なし。

万歳!

ネットワークプレースの作り方がわからん

Windows7 では、WebDAVをネットワークドライブとして割り当てることができました。
「ネットワークドライブとWebDAV over SSL」

ですが、WindowsXP(SP3)では失敗してしまいました。
XPでは、ネットワークドライブの割り当てに、WebDAVは使えないようです。

XPではWebフォルダ(ネットワークプレース?)を作成することで解決できるようです・・・が、肝心のネットワークプレースをWindowsAPIを使って作成方法がマイクロソフトからは公開されてないみたいです。

すみません、Windowsのネットワーク関係のことがよく分かってません。ので、まるでトンチンカンなことを書いてるかもしません。備忘録なので、ご注意を。)

ネットワークプレースの仕組み自体はカンタン。。。というか、ネットワークプレース自体がフォルダーリンクなので、

  1. ディレクトリを作成
  2. 作ったディレクトリの中に target という名のショートカットを作成
  3. そのショートカットにリードオンリー属性をつける。
  4. 下記内容のDesktop.ini(要システム・隠し属性)ファイルを作成
    [.ShellClassInfo]
    CLSID2={0AFACED1-E828-11D1-9187-B532F1E9575D}
    Flags=2
    
  5. ディレクトリにリードオンリー属性に。

これだけ。ショートカットはIShellLink/IPersistFile インターフェイスで作れます。

だだ、この方法では WebDAV(https/http)のネットワークプレースは作れません。
理由は至極当然で、上記(2)のWebDAVのアドレスへのショートカットが原因で、具体的には、https/http などのURIリンク先を単純にIShellLink::SetPath()に渡しても、ショートカットファイル自体は作れても、エクスプローラからWebフォルダとしては認識されません。

リンク先が、HTTP/HTTPSのURLではなく、FTP(ftp://server_name/directory/)や共有フォルダ(\\server_name\folder)のURIならうまくいきます。

このHTTP/HTTPSへのショートカットファイルは、普通のショートカットファイルではなく、どうやら特殊な(非公開の)方法で作られる必要があるようです。
もしかしたら公式のMSDNドキュメントに記述されているのかもしれません。が、結構検索かけて探してみたけどわかりませんでした。ご存じの方は教えて欲しいです (--;;;
WebDAV APIというのがあって、DavAddConnectionなどのようなAPIはありますが、Vista以降用のもののようですし、そもそも DavAddConnectionなどのAPIはSSL接続用、というようなことが書かれてますので、ちょっと違うみたいです。

で、もうちょっとグーグル先生に粘って聞いてみると・・・マイクロソフトのユーザーフォーラム?でそれらしき情報がありました。
【How to create web folders programmatically using Windows Script Host】

英文を要約すると、target.lnk そのもののバイト配列を用意してファイルに書き込むという、かなり力技で強引な(^^;;解決方法が提示されてます。
エクスプローラシェルでWebフォルダを作った後、そのtarget.lnkを解析したみたいですね。

この情報を元に、整理してC/C++で書き直して見ました。

#pragma comment(lib,"shell32.lib")

#define URI_LIMIT_SIZE 1024

#include <windows.h>
#include <strsafe.h>
#include <shlobj.h>
/*
 pURI : WebDAVへのhttp/httpsから始まるURLアドレス
 pDir : ネットワークプレースを作成するディレクトリ
 pName : ネットワークプレースの名前
*/
HRESULT CreateURILinkByEmbedByteArray(PCTSTR pURI,PCTSTR pDir,PCTSTR pName)
{
  HRESULT hres = S_OK;

  TCHAR szPath[MAX_PATH] = {0};
  TCHAR szDir[MAX_PATH] = {0};

  WCHAR szName[URI_LIMIT_SIZE] = {0};
  WCHAR szURI[URI_LIMIT_SIZE] = {0};
  
  HANDLE hFile = NULL;
  DWORD dwLen = 0;
  
  BYTE part1[] = 
    { 0x4c,0x00,0x00,0x00,0x01,0x14,0x02,0x00,0x00,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,
      0x00,0x00,0x00,0x46,0x81,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
      0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
      0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,
      0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x00,0x14,0x00,
      0x1f,0x50,0xe0,0x4f,0xd0,0x20,0xea,0x3a,0x69,0x10,0xa2,0xd8,0x08,0x00,0x2b,0x30,
      0x30,0x9d,0x14,0x00,0x2e,0x00,0x00,0xdf,0xea,0xbd,0x65,0xc2,0xd0,0x11,0xbc,0xed,
      0x00,0xa0,0xc9,0x0a,0xb5,0x0f,0xa4,0x00,0x4c,0x50,0x00,0x01,0x42,0x57,0x00,0x00,
      0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10,0x00,
      0x00,0x00};
  BYTE part2[] = { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 };
  BYTE nameLen[] = {0x00, 0x00};
  BYTE uriLen[] = {0x00, 0x00};

  int cbLen = lstrlen(pName);

  part1[0x4d] = part1[0x77] = (cbLen <= 44) ? 0x00 : 0x01;

  nameLen[0] = cbLen % 0x100;
  nameLen[1] = cbLen / 0x100;

//ワイド文字に変換。ユニコードでビルドする場合は、単にコピーするだけ。
#ifdef UNICODE
  StringCchCopy(szURI,URI_LIMIT_SIZE,pURI);
  StringCchCopy(szName,MAX_PATH,pName);
#else
  MultiByteToWideChar(CP_ACP,0,pURI,-1,szURI,URI_LIMIT_SIZE);
  MultiByteToWideChar(CP_ACP,0,pName,-1,szName,MAX_PATH);
#endif

  cbLen = lstrlen(pURI);
  uriLen[0] = cbLen % 0x100;
  uriLen[1] = cbLen / 0x100;
  
  //ディレクトリの作成
  StringCchPrintf(szDir,
                  MAX_PATH,
                  TEXT("%s%s%s"),
                  pDir,
                  (szDir[lstrlen(szDir) - 1] != TEXT('\\')) ? TEXT("\\") : TEXT(""),
                  pName);
  SHCreateDirectoryEx(NULL,szDir,NULL);

  //target.lnkのパスを生成  
  StringCchPrintf(szPath,MAX_PATH,TEXT("%s\\%s"),szDir,TEXT("target.lnk"));

  //シェルリンク・ファイルへの書込み
  hFile = CreateFile(szPath,
                     GENERIC_WRITE,
                     0,
                     NULL,
                     CREATE_NEW,
                     FILE_ATTRIBUTE_NORMAL,
                     NULL);

  if(hFile == INVALID_HANDLE_VALUE || GetLastError() == ERROR_ALREADY_EXISTS)
    {
      hres = E_FAIL;
      goto cleanup;
    }

  //埋め込み
  WriteFile(hFile,reinterpret_cast<PVOID>(part1),sizeof(part1),&dwLen,NULL);
  WriteFile(hFile,reinterpret_cast<PVOID>(nameLen),2,&dwLen,NULL);
  WriteFile(hFile,reinterpret_cast<PVOID>(szName),sizeof(WCHAR) * (lstrlenW(szName) + 1),&dwLen,NULL);
  WriteFile(hFile,reinterpret_cast<PVOID>(uriLen),2,&dwLen,NULL);
  WriteFile(hFile,reinterpret_cast<PVOID>(szURI),sizeof(WCHAR) * (lstrlenW(szURI) + 1),&dwLen,NULL);
  WriteFile(hFile,reinterpret_cast<PVOID>(part2),sizeof(part2),&dwLen,NULL);

  CloseHandle(hFile);
  
  //ファイルにRO属性を設定
  SetFileAttributes(szPath,FILE_ATTRIBUTE_READONLY);

  //ディレクトリにRO属性
  SetFileAttributes(szDir,FILE_ATTRIBUTE_READONLY);

cleanup:
  return hres;
}

一応、会社で使っているWindows XP SP3ではネットワークプレースをプログラムから作成できました。
が!、当然ながら、Windows7では、うまくいきません(笑)
Windows7/Vista から仕様が変わったようです。当たり前か(^^;;;

う~ん・・・なんかこう、すんなり旨い方法がないものだろうか・・・。

ネットワークドライブとWebDAV over SSL

Windows7だとWebDAVをネットワークドライブとして割り当てることができるので、ログオン時はいつも再接続させたい・・・のですが、クライアント認証を使ってアクセスする関係上、自動的に再接続できません。「ログオン時に再接続する」にチェックを入れているのですが、「再接続できませんでした」とかなんとかいうメッセージが出てエラーになってしまいます。

というわけで、今まではデスクトップに、以下のようなコマンドファイルを置いてその都度ダブルクリックすることで回避してました。

;---- allocate_network_drive.cmd ----
net use M: https://webdav_over_ssl/my_folder/
start M:

ただ、ダブルクリックすると、コマンドプロンプトのウィンドウが出たりして、なんかスマートじゃありません。。。

net use なんちゃら の部分を、自前でコーディングすれば事足ります。幸いにも、Windows API には WNetAddConnection2() という便利なAPIがあるので、このAPIをコールすれば一発でできそうです(^^)

早速、C++超手抜きネットワークドライブ・クラスを書いてみました。今年から積極的にC#を使おうとか、宣言しながら、早速C++使ってます。すみません(笑) 直接API叩くのがカンタンなので・・・。

#pragma comment(lib,"mpr.lib")
/**********************************************************
 簡易ネットワークドライブ・クラス 
 NetworkDrive.h
***********************************************************/
#include <windows.h>
#include <strsafe.h>
#include <tchar.h>

class CNetworkDrive
{
private:
  PTSTR m_strURI;
  PTSTR m_strDevice;

public:
  //コンストラクタ&デストラクタ
  CNetworkDrive(PCTSTR uri,PCTSTR device)
    {
      m_strURI    = CreateAndCopyString(uri);
      m_strDevice = CreateAndCopyString(device);
    }
  virtual ~CNetworkDrive()
    {
      DestroyString(m_strURI);
      DestroyString(m_strDevice);
    }

  DWORD Allocate(bool bRemember = false)
    {
      NETRESOURCE NetResource = {0};

      NetResource.dwType       = RESOURCETYPE_DISK;
      NetResource.lpLocalName  = m_strDevice;
      NetResource.lpRemoteName = m_strURI;
      
      return WNetAddConnection2(&NetResource,NULL,NULL,CONNECT_INTERACTIVE | (bRemember ? CONNECT_UPDATE_PROFILE : 0));
    }
  
  DWORD Cancel(bool bUpdate = false,bool bForce = true)
    {
      DWORD dwFlags = bUpdate ? CONNECT_UPDATE_PROFILE : 0;
      BOOL bwForce = bForce ? TRUE : FALSE;

      return WNetCancelConnection2(m_strDevice,dwFlags,bwForce);
    }

//雑関数
private:
  static PTSTR CreateAndCopyString(PCTSTR src)
    {
      DWORD dwNum = 0;
      PTSTR dest = NULL;
      
      if((dwNum = lstrlen(src)) > 0)
        {
          dest = new TCHAR[dwNum+1];
          StringCchCopy(dest,dwNum+1,src);
        }
      
      return dest;
    }

  static void DestroyString(PTSTR str)
    {
      if(str)
        delete [] str;
    }
  
};

これを、使って、以下のようなテストコードで試してみたところ、うまく行きました。

#pragma comment(lib,"user32.lib")

#include <windows.h>
#include <tchar.h>
#include "NetworkDrive.h"

#define URI TEXT("https://my_webdav_site/my_folder/")
#define DEVICE TEXT("Z:")

//スタートアップ
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
  return MessageBox(NULL,
                    (CNetworkDrive(URL,DRIVE).Allocate() == NO_ERROR) ? TEXT("接続しました。") : TEXT("失敗ました。"),
                    TEXT("メッセージ"),
                    MB_OK);
}

最初、エラーで接続できなくてググっても分からず、全く原因が分からくて途方に暮れてたのですが・・・結局ドキュメントを見落としてました。。。英語なんでサラッと流し読みしたのが悪かった(^_^;)

WNetAddConnection2 APIに渡す最後の引数のフラグに、CONNECT_INTERACTIVE を指定していなかったのが原因でした。これを指定することで、クライアント証明書選択ダイアログ(っていうのかな?)が出て正常に認証が済み、無事ネットワークドライブを割り当てることができました。

やっぱりドキュメントはちゃんと読まないといけませんねぇ・・・

CommandLineToArgvAってないの???

ちょっとした小さなツールをC++で組むとき、CRTは使わないときはできるだけCRTをリンクしないようにしたいわけです。

でも、エントリポイントに WinMainとかしてしまうと、コマンドライン引数をうまくハンドリングできなくて悩む。#define UNICODE とかして、UNICODEにすると、CommandLineToArgvWというAPIがあるので、簡単に、argc(int) と argv(char**)がとれてラクができますが・・・。

なぜか・・・ANSIバージョンの CommandLineToArgvA がないのはなんでなんでしょうかねぇ・・・。
というわけで、解決方法は3つ。

  1. そもそもANSI文字列を使わない。
  2. 自分でコマンドライン文字列(GetCommandLine API)パーサーを書く
  3. CommandLineToArgvWで得られた引数リストをANSI文字列に変換する

一番ラクそうなのは3かな・・・ということで、やっつけで書いてみましたが・・・これでいいのかな・・・(^^;;;

#define GLOBALALLOC(S) reinterpret_cast(GlobalAlloc(GPTR,(S)))
#define GLOBALFREE(X) {if(X){GlobalFree(reinterpret_cast(X));(X)=NULL;}}

#define HEAPALLOC(S) HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,(S))
#define HEAPFREE(X) {if(X){HeapFree(GetProcessHeap(),0,reinterpret_cast(X));(X)=NULL;}}

PSTR *CommandLineToArgvA(LPCSTR lpCmdLine,int *pNumArgs)
{
  PSTR *pArgvA = NULL;
  int Argc = 0;

  PWSTR *pArgvW = NULL;
  PWSTR pM2WBuffer = NULL;
  int *pcchLen = NULL;
  int cchBufferLength = 0;

  *pNumArgs = 0;

  if((cchBufferLength = MultiByteToWideChar(CP_ACP,MB_PRECOMPOSED,lpCmdLine,-1,NULL,0))     goto cleanup;

  if((pM2WBuffer = (PWSTR)HEAPALLOC((cchBufferLength+1)*sizeof(WCHAR))) == NULL)
    goto cleanup;

  MultiByteToWideChar(CP_ACP,MB_PRECOMPOSED,lpCmdLine,-1,pM2WBuffer,cchBufferLength+1);
  pArgvW = CommandLineToArgvW(pM2WBuffer,&Argc);

  //コマンドラインをマルチバイトに変換した後の
  //文字数(配列pcchLen)、文字数合計(cchBufferLength)を求める
  if((pcchLen = (int*)HEAPALLOC(sizeof(int)*Argc)) == NULL)
    goto cleanup;

  cchBufferLength = 0;
  for(int i=0;i    {
      pcchLen[i] = WideCharToMultiByte(CP_ACP,0,pArgvW[i],-1,NULL,0,NULL,NULL);
      cchBufferLength += ++pcchLen[i];
    }

  //コマンドライン文字列の配列を取得して格納する。
  if((pArgvA = (PSTR*)GLOBALALLOC(cchBufferLength + sizeof(PSTR)*Argc)) == NULL)
    goto cleanup;

  for(int i=0;i    {
      pArgvA[i] = (i < 1) ? (PSTR)pArgvA + sizeof(PSTR)*Argc : pArgvA[i-1] + pcchLen[i-1];
      WideCharToMultiByte(CP_ACP,0,pArgvW[i],-1,pArgvA[i],pcchLen[i],NULL,NULL);
    }

cleanup:
  HEAPFREE(pcchLen);
  HEAPFREE(pM2WBuffer);
  GLOBALFREE(pArgvW);  // 解放処理を忘れていたので追加しました。(2010/7/4)
  *pNumArgs = Argc;
  return pArgvA;
}

こんなんでエエのかな・・・。。。


CommandLineToArgvA その2