yohhoyの日記(別館)

もうちょい長めの技術的メモをしていきたい日記

Boost.勉強会 #10 に参加しました

2012/7/28に開催されたイベント Boost.勉強会 #10 東京 にて、“C++ Transactional Memory言語拡張の紹介”というタイトルで20分ほど話す機会をいただきました。

質疑応答

下記は発表後に行われた質問と回答を並べたものです。改めて書き起こすと結構分量がありますね。

  • Q. コミットとキャンセルしか提供されず機能がミニマムセット過ぎる?(Haskeller曰く)リトライが無いと使い物にならないらしい。補足として、TBoost.STMにはリトライ機能が存在する。
  • A.ドラフト仕様Ver.1.1段階では指摘どおり。ドラフト仕様のAppendixに拡張機能の候補として上げられており、今後の改訂で追加される可能性はあるかも。
  • Q. gccのTM拡張はゼロオーバーヘッドなのか?TM拡張を有効にした場合、atomicトランザクションを利用しなくてもオーバーヘッドは無いのか?
  • A. 性能については分からない。現時点ではSTM実装となっており何らかのオーバーヘッドがあるかも。
  • Q. atomicトランザクションから操作する変数であるか否かを指定する構文が存在しないとすると、全ての変数がトランザクション操作されうると見なすのか?
  • A. atomicトランザクションをSTMでどう実装するのか想像つかなかった。HTMなら専用命令で変数アクセスを検知できるような気がする。
  • Q. HTMでも対象となるアドレス数(変数の個数)には制限があるのでは?
  • A. そう思う。
  • Q. トランザクション内ではint等のプリミティブしか扱えない?クラスは?
  • A. 使える。クラス操作はプリミティブ操作の集合へと分解されるため。
  • Q. トランザクション内でnew演算子も使える?
  • A. 仕様上は組み込みの演算子に限りnew/deleteを使えるとある。ただし処理系によっては使用不可と制限がかかるかもしれない。
  • Q. ロックとTMのパフォーマンス比較って何か公開されている?
  • A. 自分で計ったデータは無い。GCC公式ドキュメントにも実行速度は今後にご期待くださいとあるのみ。
  • Q. トランザクション内から整数しか例外送出できないのは超悲しい。ポインタ値を送出した場合にIsolationが壊れるのを防ぐためだろう。何とかかならないのか?
  • A. 何とかなる仕様を提案してね。
  • Q. atomicトランザクション内でファイルI/Oのような非可逆操作を行おうとすると、ビルドエラーとなるのか?
  • A. コンパイル時エラーとなる。transaction_safe/unsafe属性によりコンパイル時に判断可能な仕様設計となっている。
  • Q. 例えばC言語ライブラリを呼ぶなど判断できない場合はどうする?
  • A. 標準ライブラリが提供する安全でない関数には、処理系の責任でtransaction_unsafe属性を付けているはず。

感想とか

参加された方々の前提知識レベルをどこに置けばよいか分からず、発表時間の半分をTransactional Memoryそのものの説明に当てました。一方で共有メモリアーキテクチャを前提とすることや、マルチスレッドプログラミングの知識があることは無条件に仮定しています。もっとC++TM言語拡張仕様について説明するパターンでも良かったのかなと感じました。

発表内容の意図としては「C++言語仕様に対するTransactional Memory拡張“言語仕様”の紹介」でしたが、当然ながらSTM/HTM等でどのように実装・実現されるのかという質問も出ました。実性能が気になるというのは十分予測されたのですが、現時点では性能面における期待はできないと考え、発表内容は純粋に言語仕様のみを対象としました。

今回紹介したC++TM言語拡張が本当に次期C++へ導入されるか否かは分かりませんが、Transactional Memoryなどの並列・並行処理における抽象化機構に足を踏み入れるきっかけになれば幸いです。

UnVisualBasic 6.0

この記事は2001年頃に書いた文章をそのまま転記し、はてなブログ用に体裁を整えたものです。Visual Basic 6.0を前提としていますが、当時からネタ記事なので悪しからず。

プロジェクト一式はこちら: https://gist.github.com/yohhoy/3054269


まえがき

往々にしてGUI(Graphical User Interface)プログラミングではコード量が多くなり面倒なものですが、Microsoft社のVisualBasicを用いると僅かなコーディングで簡単・迅速にGUI構築することが可能になります。
しかし、VBの守備範囲を越えたことを実装する際には、どうしてもWin32API等に頼らざるを得ません。これを見越して(?)かVBには外部DLL関数を呼ぶ仕掛けが用意されています。この機能を使えばWindows上で可能なことは大抵実装できます。さらにVB5.0から導入された AddressOf演算子 を用いるとVB関数のアドレスが取得できます。つまりVBでコールバック関数が使用可能になったのです。この AddressOf演算子 の登場で、(実用上ほぼ)全てのAPI関数が利用できるようになったのです。

目的

AddressOf演算子 の登場でVBでもウインドウのサブクラス化を実現できるようになり、今までOCXやDLLに頼っていた機能を自前で実装できるようになりました。
ここまでVBで実現できるのなら、いっそVBの機能を使わず(基本制御構造は除く)にWin32APIのみでソフトを開発出来ないかと考えてみました。要するに、本来ならメニューから

[プロジェクト]→[フォームモジュールの追加]

とするだけで終わるところを、RegisterClass関数やCreateWindow関数等を用いて実装しようというのが目的です。

実装例1

とりあえず上記の目標に従って実装してみました。ただしAPI関数のDeclare宣言や型の定義、定数の定義は省いています。

Private m_hInstance As Long

Private Const WNDCLSNAME As String = "TrickyVbWndCls"
Private Const IDM_EXIT = 1

'
' Entry Point
'
Public Sub Main()
  Dim wc As WNDCLASS
  Dim hMenu As Long, hSubMenu As Long
  Dim hwnd As Long
  Dim message As MSG

  '初期化
  m_hInstance = GetModuleHandle(0)

  'ウインドウクラスを登録
  With wc
    .style = CS_VREDRAW Or CS_HREDRAW
    .lpfnWndProc = funcaddr(AddressOf WindowProc)
    .cbClsExtra = 0
    .cbWndExtra = 0
    .hinstance = m_hInstance
    .hIcon = LoadIcon(0, IDI_APPLICATION)
    .hCursor = LoadCursor(0, IDC_ARROW)
    .hbrBackground = COLOR_BTNFACE + 1
    .lpszMenuName = vbNullString
    .lpszClassName = WNDCLSNAME
  End With
  Call RegisterClass(wc)

  'メニュー作成
  hMenu = CreateMenu()
  hSubMenu = CreatePopupMenu()
  Call AppendMenu(hMenu, MF_STRING Or MF_POPUP, hSubMenu, "&File")
  Call AppendMenu(hSubMenu, MF_STRING, IDM_EXIT, "&Exit")

  'ウインドウ作成
  hwnd = CreateWindowEx(0, WNDCLSNAME, "tricky", _
                  WS_OVERLAPPEDWINDOW, _
                  CW_USEDEFAULT, CW_USEDEFAULT, _
                  CW_USEDEFAULT, CW_USEDEFAULT, _
                  0, hMenu, m_hInstance, 0)
  Call ShowWindow(hwnd, SW_SHOW)

  'メッセージループ
  Do While (GetMessage(message, 0, 0, 0))
    Call TranslateMessage(message)
    Call DispatchMessage(message)
  Loop

  'メニュー破棄
  Call DestroyMenu(hSubMenu)
  Call DestroyMenu(hMenu)

End Sub

'
' ウインドウプロシージャ
'
Private Function WindowProc(ByVal hwnd As Long, ByVal uMsg As Long, _
  ByVal wParam As Long, ByVal lParam As Long) As Long

  WindowProc = 0

  Select Case uMsg
  Case WM_CREATE
    '

  Case WM_COMMAND
    Select Case (wParam And &HFFFF)
    Case IDM_EXIT '[Exit]
      'ウインドウを閉じる
      Call SendMessage(hwnd, WM_CLOSE, 0, 0)
    End Select

  Case WM_DESTROY
    'アプリケーション終了
    Call PostQuitMessage(0)

  Case Else
    WindowProc = DefWindowProc(hwnd, uMsg, wParam, lParam)

  End Select

End Function

Private Function funcaddr(ByVal addr As Long) As Long
  funcaddr = addr
End Function

当然のことですが、基本制御構造の If や Do While や Select Case、Call ステートメント を使用しないというのは不可能ですね。
あと、メニューはリソースにしようと思ったのですがVBのリソースエディタでは無理でした。VC++でリソース(.res)だけ作成すればどうにかなったかも…
わざわざ funcaddr()関数を作っているのは、AddressOf演算子が返す値(Long型)を変数に直接代入できなかった為です。

実装例2

ちなみに実装例1と同等のプログラムを、VB本来の手法で作ってみます。

  • メニューから[プロジェクト]→[フォームモジュールの追加]
  • 新しく作ったフォームを右クリックして[メニューエディタ]
  • キャプション:&File 名前:mnuFile [次へ]をクリック
  • キャプション:&Exit 名前:mnuFileExit [⇒]をクリック
  • [OK]で閉じる
  • フォーム上のメニューから[File]→[Exit]

開いたコードエディタ上で

Private Sub mnuFileExit_Click()
    Unload Me
End Sub

以上。
コーディング量は "Unload Me" の9文字だけ!この際 "Unload" だけでも可!

結論

餅は餅屋

付録

普通にC言語とWin32SDKで作るとこんな感じになります。

#define STRICT
#include <windows.h>

/* マクロ定義 */
#define WNDCLSNAME "TrickyVbWndCls"
#define IDM_EXIT   1

/* 関数プロトタイプ宣言 */
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg,
  WPARAM wParam, LPARAM lParam);

/* Entry Point */
int WINAPI WinMain(
  HINSTANCE hInstance,
  HINSTANCE hPrevInstance,
  LPSTR lpCmdLine,
  int nCmdShow)
{
  WNDCLASS wc;
  HMENU hmenu, hsubmenu;
  HWND hwnd;
  MSG msg;

  /* ウインドウクラスを登録 */
  wc.style = CS_VREDRAW | CS_HREDRAW;
  wc.lpfnWndProc = WindowProc;
  wc.cbClsExtra = 0;
  wc.cbWndExtra = 0;
  wc.hInstance = hInstance;
  wc.hIcon = LoadIcon(0, IDI_APPLICATION);
  wc.hCursor = LoadCursor(0, IDC_ARROW);
  wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
  wc.lpszMenuName = NULL;
  wc.lpszClassName = WNDCLSNAME;
  RegisterClass(&wc);

  /* メニュー作成 */
  hmenu = CreateMenu();
  hsubmenu = CreatePopupMenu();
  AppendMenu(hmenu, MF_STRING|MF_POPUP, (UINT)hsubmenu, "&File");
  AppendMenu(hsubmenu, MF_STRING, IDM_EXIT, "&Exit");

  /* ウインドウ作成 */
  hwnd = CreateWindowEx(0, WNDCLSNAME, "tricky",
                        WS_OVERLAPPEDWINDOW,
                        CW_USEDEFAULT, CW_USEDEFAULT,
                        CW_USEDEFAULT, CW_USEDEFAULT,
                        0, hmenu, hInstance, 0);
  ShowWindow(hwnd, nCmdShow);

  /* メッセージループ */
  while (GetMessage(&msg, NULL, 0, 0)) {
     TranslateMessage(&msg);
     DispatchMessage(&msg);
  }

  /* メニュー破棄 */
  DestroyMenu(hsubmenu);
  DestroyMenu(hmenu);

  return 0;
}

/* ウインドウプロシージャ */
LRESULT CALLBACK WindowProc(
  HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch (uMsg) {
  case WM_CREATE:
    /* ... */
    break;

  case WM_COMMAND:
    switch (LOWORD(wParam)) {
    case IDM_EXIT: /* [Exit] */
       /* ウインドウを閉じる*/
       SendMessage(hwnd, WM_CLOSE, 0, 0);
       break;
    }
    break;

  case WM_DESTROY:
    /* アプリケーション終了 */
    PostQuitMessage(0);
    break;

  default:
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
  }
  return 0;
}

OLE Drag & Dropを実装する

この記事は2001年頃に書いた文章をそのまま転記し、はてなブログ用に体裁を整えたものです。Windows2000+Win32 SDKC言語を前提としており、2012年現在では内容が陳腐化している箇所もあります。

(当時の)ソースコードはこちら: https://gist.github.com/3053385


目的

Windowsの便利な機能に、ドラッグ・アンド・ドロップ(以下D&D)によるファイルのコピー・移動・ショートカット作成があります。これに相当する機能を実装してみようというのが今回の目的です。
ちなみに開発環境は、Win32 SDKベースのC言語を想定しています。

非OLE Drag&Drop

Explorer等からのファイルD&Dを実装する際に、最も簡単なのはDragAcceptFiles()関数を用いる方法です。
ファイルのドロップを受け付けたいウインドウのウインドウハンドル(hwnd)とすると、

HWND hwnd = /* 対象ウインドウのHWND */;

DragAcceptFiles(hwnd, TRUE);

とすることで、ファイルのドロップを受け付けるようになります。
対象ウインドウにExplorerからファイルがD&Dされると、ウインドウプロシージャーにWM_DROPFILESが送られるので、このメッセージをフックしてドロップされたファイルを処理してやります。

LRESULT CALLBACK WindowProc(
  HWND hwnd,
  UINT uMsg,
  WPARAM wParam,
  LPARAM lParam
  )
{
  HDROP hdrop;
  char filename[MAX_PATH];
  int num, i;

  switch (uMsg) {
  case WM_DROPFILES:
    hdrop = (HDROP)wParam;
    /* ドロップされたファイルの個数を得る */
    num = DragQueryFile(hdrop, -1, NULL, 0);
    /* ファイルを処理 */
    for (i = 0; i < num; i++) {
      DragQueryFile(hdrop, i, filename, &filename);
      /*
       * filename にファイル名が入っているので、ここで処理を行う。
       */
    }
    DragFinish(hdrop);
    break;

  /* other messages... */
  }
  return 0;
}

これでExplorerからのファイルドロップを実装できました。しかし、まだファイルD&Dのうち後者しか実現していません。
次はExplorerへのファイルドラッグの実装に挑戦してみます。

OLE

“ドラッグ”は“ドロップ”と対になっている動作ですが、Explorerへのファイル“ドラッグ”に関しては残念ながら上記のように単純には実装できません。これを行うにはOLE(Object Linking and Embedding)を利用する必要があります。OLEはCOM(Component Object Model)技術に基づいているため、実際はCOMサーバーを実装することが必要になってきます。
ここで問題になるのは、COMがオブジェクト指向であるためC言語で実装するのが面倒だということです。C++ならばclassを利用して比較的楽に実装できるのですが、Cにはそのような言語的サポートはありません。よって少々煩雑なコーディングが必要になってきます。
以下の解説ではCOMに関する説明は省略します。COMについて上手に説明しているサイトは沢山ありますのから、そちらを参考にして下さい。

OLE Drag&Drop

さて、OLE D&Dを実装するにあたって次の3つのCOMインターフェイスを使う必要があります。

  • IDataObject
  • IDropSource
  • IDropTarget

IDataObjectは受け渡しするデータ(ここではファイル)であり、IDropSourceはOLE D&Dのドラッグ開始元に、またIDropTargetはOLE D&Dのドロップ先に対応しています。そして、OLE D&Dのドラッグ開始するにはDoDragDrop()関数を用います。
OLE D&Dにおける処理の流れは次のようになっています。


  1. DoDragDrop()関数を呼び出す。
  2. ドラッグ中のオブジェクトがウインドウ領域の上に入ると、IDropTarget::DragEnter()が呼び出される。
  3. IDropSource::GiveFeedback()が呼び出される。
  4. マウスがウインドウ領域の上にいる間は次の関数が呼び出された後、IDropSource::GiveFeedback()が呼び出される。

    • マウスカーソルが領域内で動くと、IDropTarget::DragOver()が呼び出される。
    • マウスカーソルがウインドウ領域外に移動すると、IDropTarget::DragLeave()が呼び出される。

  5. ドラッグ中にキー入力やマウスクリックが発生するとIDropSource::QueryContinueDrag()が呼び出される。ここで、D&Dを完了させる(ドロップ)かキャンセルするかを決定する。
  6. オブジェクトがドロップされると、IDropTarget::Drop()が呼び出される。
  7. DoDragDrop()関数から戻る。このときIDropTarget側で設定されたDROPEFFECTが戻り値として得られる。

ここで、IDropSourceとIDropTargetは別々のアプリケーションに属している(ことが多い)に注意してください。
例えばExplorerからのファイルD&Dの場合、IDropSourceはExplorer側で提供され、IDropTargetは自作アプリケーション側で提供したものが使われます。

IDropSource

まず、ファイルの“ドラッグ”を実現するためのIDropSourceインターフェイスを実装します。IDropSourceには次の2つのメソッドがあります。

  • IDropSource::GiveFeedback()
  • IDropSource::QueryContinueDrag()

GiveFeedback()メソッドではドラッグ先(IDropTarget)で指定されたD&Dの状態(DROPEFFECT)によって、SetCursor()関数等でマウスカーソルにフィードバックを与えます。このときDRAGDROP_S_USEDEFAULTCURSORSを返すことで、OLEがデフォルトで提供するカーソル形状を利用できます。
QueryContinueDrag()メソッドではキー入力やマウス入力によって、D&Dを完了するかキャンセルするかを返します。Windowsの一般的なGUIとしては、ESCが押されたらキャンセルしマウスボタンが離されたらD&D完了(オブジェクトのドロップ)とします。

typedef struct tagOLEDropSource {
  IDropSource super;
  ULONG       m_cRefCnt;
} OLEDropSource;

/* IUnknown methods */
STDMETHODIMP OLEDropSource_QueryInterface(/*略*/);
STDMETHODIMP_(ULONG) OLEDropSource_AddRef(/*略*/);
STDMETHODIMP_(ULONG) OLEDropSource_Release(/*略*/);

/* IDropSource methods */
STDMETHODIMP OLEDropSource_QueryContinueDrag(
  OLEDropSource *this,
  BOOL fEscapePressed,
  DWORD grfKeyState
  )
{
  /* ESCキーが押されたらD&Dキャンセル */
  if (fEscapePressed)
    return DRAGDROP_S_CANCEL;
  /* マウスボタンが離されたらD&D完了 */
  if (!(grfKeyState & (MK_LBUTTON|MK_RBUTTON)))
    return DRAGDROP_S_DROP;
  return S_OK;
}

STDMETHODIMP OLEDropSource_QueryContinueDrag(
  OLEDropSource *this,
  DWORD dwEffect
  )
{
  /* OLEデフォルトのカーソルを使用する */
  return DRAGDROP_S_USEDEFAULTCURSORS;
}
IDataObject

次にOLE D&Dで受け渡されるデータを表現するためのIDataObjectインターフェイスを実装します。IDataObjectには次の9つのメソッドがあります。

  • IDataObject::GetData()
  • IDataObject::GetDataHere()
  • IDataObject::QueryGetData()
  • IDataObject::GetCanonicalFormatEtc()
  • IDataObject::SetData()
  • IDataObject::EnumFormatEtc()
  • IDataObject::DAdvise()
  • IDataObject::DUnadvise()
  • IDataObject::EnumDAdvise()

これら全てのメソッドを実装するのは大変な作業なのため、ここではファイルを表すHDROPを格納するための最低限の実装だけ行うことにします。
実装するのは次の4つのメソッドと、EnumFormatEtc()メソッドで必要になるIEnumFORMATETCインターフェイスです。

  • IDataObject::GetData()
  • IDataObject::QueryGetData()
  • IDataObject::SetData()
  • IDataObject::EnumFormatEtc()

ここではIDataObject/IEnumFORMATETCの実装の表記は省略します。

OLE D&D: Drag編

ではExplorerへのファイルドラッグのための前準備からD&D開始、後処理までの流れをコーディングします。

/*
 * ファイル名を'\0'で区切った文字列。末尾は2個の'\0'で終わる。
 */
char *files = "c:\file1.txt\0c:file2.txt\0\0";

IDataObject *lpDataObject = /* IDataObjectのインスタンス */;
IDropSource *lpDropSource = /* IDropSourceのインスタンス */;
FORMATETC fmtetc;
STGMEDIUM medium;
char *p;
DWORD effect;
HRESULT hr;


/* FORMATETC構造体をセット */
fmtetc.cfFormat = CF_HDROP;
fmtetc.ptd = NULL;
fmtetc.dwAspect = DVASPECT_CONTENT;
fmtetc.lindex = -1;
fmtetc.tymed = TYMED_HGLOBAL;

/* STGMEDIUM構造体をセット */
medium.tymed = TYMED_HGLOBAL;
medium.hGlobal = GlobalAlloc(
                   GMEM_MOVEABLE, 
                   sizeof(DROPFILES) + sizeof(files)
                 );
medium.pUnkForRelease = NULL;
p = GlobalLock(medium.hGlobal);
((DROPFILES *)p)->pFiles = sizeof(DROPFILES);
((DROPFILES *)p)->fWide = FALSE;
CopyMemory(p + sizeof(DROPFILES), files, sizeof(files));
GlobalUnlock(medium.hGlobal);

/* IDataObjectをセット */
IDataObject_SetData(lpDataObejct, &fmtetc, &medium, FALSE);

/* OLE Drag & Drop開始 */
hr = DoDragDrop(
       lpDataObejct,
       lpDropSource,
       DROPEFFECT_MOVE|DROPEFFECT_COPY|DROPEFFECT_LINK,
       &effect
     );

/* リソースを開放 */
IDataObject_Release(lpDataObejct);
IDropSource_Release(lpDropSource);

/*
 * hr, effectの値によってデータを処理...
 */

実際のコードはマウスのドラッグ開始メッセージなどによって実行されるようにします。これはWM_LBUTTONDOWN, WM_RBUTTONDOWN, LVN_BEGINDRAG(リストビュー), TVN_BEGINDRAG(ツリービュー)等が相当します。
これでExplorerへのファイルD&Dが実装できました。最初の非OLE D&DのExplorerからのファイルドロップ受付と、この実装を組み合わせればExplorerとの完全なD&Dが実現できます。
しかし、せっかくOLE D&Dの“ドラッグ”機能を実装したので、“ドロップ”機能もOLEを利用して実装することにします。

IDropTarget

では、ファイルの“ドロップ”を実現するためのIDropTargetインターフェイスを実装します。IDropTargetには次の4つのメソッドがあります。

  • IDropTarget::DragEnter()
  • IDropTarget::DragOver()
  • IDropTarget::DragLeave()
  • IDropTarget::Drop()

DragEnter()/DragOver()/DragLeave()メソッドは対象ウインドウ領域内にマウスカーソルが"入った"/"移動した"/"出た"という動きに対応して呼び出されます。これらのメソッド内でキー入力に応じてDROPEFFECTを指定することにより、IDropSourceが視覚的フィードバックを行なうめの情報を渡します。
Drop()メソッドは実際にオブジェクトが対象ウインドウにドロップされると呼び出されます。ここでIDataObjectを介して実際にデータのやり取りを行ないます。
今回はOLE D&Dのターゲットウインドウ側の(視覚的な)フィードバックは考慮していませんが、何らかのフィードバックを行いたい場合は各メソッドでリソースの初期化/破棄を行なう必要があります。

typedef struct tagOLEDropTarget {
  IDropTarget super;
  ULONG       m_cRefCnt;
} OLEDropTarget;


/* IUnknown methods */
STDMETHODIMP OLEDropTarget_QueryInterface(/*略*/);
STDMETHODIMP_(ULONG) OLEDropTarget_AddRef(/*略*/);
STDMETHODIMP_(ULONG) OLEDropTarget_Release(/*略*/);

/* IDropTarget methods */
STDMETHODIMP OLEDropTarget_DragEnter(
  OLEDropTarget *this,
  IDataObject *pDataObject,
  DWORD grfKeyState,
  POINTL pt,
  DWORD *pdwEffect
  )
{
  /*
   * pDataObject に応じてドロップを受け付けるかを判断
   * ドロップを拒否する際は
   *   pdwEffect = DROPEFFECT_NONE;
   * とする。
   */
  
  /*
   * キー押下の状態に応じて pdwEffect をセット
   */
  return S_OK;
}

STDMETHODIMP OLEDropTarget_DragOver(
  OLEDropTarget *this,
  DWORD grfKeyState,
  POINTL pt,
  DWORD *pdwEffect
  )
{
  /*
   * キー押下の状態に応じて pdwEffect をセット
   */
  return S_OK;
}

STDMETHODIMP OLEDropTarget_DragLeave(
  OLEDropTarget *this
  )
{
  return S_OK;
}

STDMETHODIMP OLEDropTarget_Drop(
  OLEDropTarget *this,
  IDataObject *pDataObject,
  DWORD grfKeyState,
  POINTL pt,D
  WORD *pdwEffect
  )
{
  /*
   * D&D完了時の処理...
   */
  return S_OK;
}

OLE D&D: Drop編

IDropTargetの準備ができたら、Explorerからのファイルドロップを受け付けるようコーディングしてみます。

RegisterDragDrop()関数を用いて、IDropTargetインターフェイスとファイルD&Dを受け付けるウインドウを関連付けます。

HWND hwnd = /* 対象ウインドウのハンドル */;
IDropTarget *lpDropTarget = /* IDropTargetのインスタンス */;

RegisterDragDrop(hwnd, lpDropTarget);

これでOLE D&Dの受け入れが登録され、ファイルがドロップされると関連付けたIDropTargetのDrop()メソッドが呼び出されるようになりました。
逆に登録を解除するにはRevokeDragDrop()関数を用いて次のようにします。

HWND hwnd = /* 対象ウインドウのハンドル */;

RevokeDragDrop(hwnd);

なおIDropTargetの参照は登録時にAddRef()/解除時にRelease()されるので明示的にRelease()する必要はありません。

OLE D&D: まとめ

これらのOLE D&Dを利用するためには、OLEライブラリの初期化を先立って行なわなければいけません。これはOleInitialize()関数で行ないます。また、利用しなくなったらOleUninitialize()関数で解放する必要があります。
これらはWinMain()で他の初期化/解放処理と一緒に行なったほうが良いでしょう。

int WINAPI WinMain(
  HINSTANCE hInstance,
  HINSTANCE hPrevInstance,
  LPSTR lpCmdLine,
  int nCmdShow
  )
{
  /* OLEライブラリを初期化 */
  OleInitialize(NULL);
  
  /*
   * メイン処理...
   */
  
  /* ライブラリを解放 */
  OleUninitialize();
  return 0;
}

あとがき

これで当初の目的であるExplorerとのファイルD&Dは実装できました。ただしIDataObjectの実装は手抜きですからIAdviseSinkを利用できません。これはソース側にオブジェクト(IDataObject)の変更を通知するためのインターフェイスです。また、Windows2000からはIDropSourceHelper/IDropTargetHelperというインターフェイスが追加されて、視覚的フィードバックの実装の手助けをしてくれるようです。
今回利用したOLE D&DではファイルのD&D以外にも様々なデータ(オブジェクト)のD&Dに対応しています。例えばMicrosoft Word等では選択したテキストをD&Dによってコピー&ペーストすることができますが、このようなテキストもOLE D&Dで受け取ることができます。
これらについての情報は、IDataObjectやFORMATETCといったキーワードから検索できると思います。

C++スレッド遅延開始の実装5パターン

局所的に「スレッド開始を遅延させる」ネタが盛り上がっていたので、C++とBoostライブラリを用いた色々な実装方法をまとめてみました。

この記事で対象とするのは、下記コードにある2つの要件を満たす実装方法です。

  • (1) スレッドを管理するオブジェクトXのコンストラクト時ではなく、その後の任意タイミングで新スレッド処理を開始する。
  • (2) オブジェクトXのデストラクト時に、上記(1)の別スレッドがまだ実行中ならそのスレッド処理完了を待機する。
class X {
  // threadオブジェクトを保持するメンバ変数

  void do_() { /* 別スレッド処理 */ }

public:
  ~X()
  {
    // (2) 別スレッドがまだ実行中なら完了を待機する
  }

  void start()
  {
    // (1) 新しいスレッドを開始して関数do_を実行する
  }
};

int main()
{
  X x;
  x.start();
}

実装方法の比較

#1,2,3は@cpp_akiraさんの std::threadをあとから開始。それとムーブ対応したコンテナについて - Faith and Brave - C++で遊ぼう、#4は@egtraさんのイグトランスの頭の中(のかけら) » Boost.ThreadとBoost.Optionalからのパク(ry引用です。#5が新たに追加した実装方法です。

実装方法 C++03/Boost C++11/Boost C++11/std
#1 SmartPtr+thread
#2 thread.swap
#3 threadムーブ
#4 Boost.Optimal+thread ×
#5 future+async() × ×

表中の"C++03/Boost"はC++03準拠コンパイラ+Boostライブラリ、"C++11/Boost"はC++11準拠コンパイラ+Boostライブラリ、"C++11/std"はC++11準拠コンパイラ+標準ライブラリのみの範囲において、各実装方法が実現可能/不可を示します。(なお、Boost 1.49.0を前提としています。)

注意:後述コードサンプルではC++11ラムダ式を使用しており、C++11非準拠コンパイラではコンパイルできません。C++03の範囲内では下記の書き換えが必要です。

boost::thread([this] { do_(); })
// ↓
boost::thread(&X::do_, this)

#1: SmartPtr+thread

#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>

class X {
  boost::shared_ptr<boost::thread> thread_;

  void do_() { /*...*/ }

public:
  ~X()
  {
    if (thread_)
      thread_->join();
  }

  void start()
  {
    thread_.reset(new boost::thread([this] { do_(); }));
  }
};

(個人的には、コピー可能なスマートポインタshared_ptrより、ムーブのみを許容するunique_ptrの方が好ましい気がします。)

#2: thread.swap

#include <boost/thread.hpp>

class X {
  boost::thread thread_;

  void do_() { /*...*/ }

public:
  ~X()
  {
    if (thread_.joinable())
      thread_.join();
  }

  void start()
  {
    boost::thread thd([this] { do_(); });
    thread_.swap(thd);
  }
};

#3: threadムーブ

#include <thread>

class X {
  std::thread thread_;

  void do_() { /*...*/ }

public:
  ~X()
  {
    if (thread_.joinable())
      thread_.join();
  }

  void start()
  {
    thread_ = std::thread([this] { do_(); });
  }
};

#4: Boost.Optimal+thread

#include <boost/optional.hpp>
#include <boost/utility/in_place_factory.hpp>
#include <boost/thread.hpp>

class X {
  boost::optional<boost::thread> thread_;

  void do_() { /*...*/ }

public:
  ~X()
  {
    if (thread_)
      thread_->join();
  }

  void start()
  {
    thread_ = boost::in_place([this] { do_(); });
  }
};

#5: future+async()

この実装方法は前述のものとは異なり、threadオブジェクトの明示的な生成/保持を行いません。代わりに、launch::asyncポリシーasync関数によるスレッド開始と、futureオブジェクトのデストラクタによる(暗黙的)スレッド完了待機を行います。暗黙的なスレッドjoinについてはasync関数launch::asyncポリシーとfutureのちょっと特殊な動作 - yohhoyの日記を参照下さい。

#include <future>

class X {
  std::future<void> task_;

  void do_() { /*...*/ }

public:
  // task_がlaunch::async起動されたfutureオブジェクトの場合に限り、
  // future<void>::~future()にてスレッドjoinが暗黙的に行われる。

  void start()
  {
    task_ = std::async(std::launch::async, [this] { do_(); });
  }
};

実装#1~#4では関数do_()から例外送出されるとプログラム終了(std::terminate)しますが、実装#5は関数do_()で送出した例外を無かったこととして処理続行します。正確に表現すると、例外はちゃんとfutureオブジェクト中に格納されています。上記コードではfuture::get()による処理結果の取り出しを行っておらず、結果として関数do_()が送出した例外が失われています。

各実装はgcc 4.6.3@Ubuntu 11.10にて動作確認済みです。(なお#5はgcc 4.7.0@MacOS X 10.6だとdo_からの例外送出時のみSegmentation Faultが発生。)

Boost.Contextでファイバーライブラリを実装してみた

Boost 1.50.x候補のBoost.Contextライブラリを利用して、一風変わったファイバーライブラリを作ってみたというお話です。(注意:Boost.ContextはBoost 1.49.0の正式リリースには含まれないため、svnレポジトリのtrunkからチェックアウトする必要アリ)

注意:2012年5月現在、Boost.Contextの破壊的API変更により実装コードはコンパイルできなくなっています。id:FlastさんのBoost.Contextの怒涛の変更も参照ください。

何これ?何ができるの?

一言で表現すると「単一スレッドで動作するスレッドライブラリ」です。

これだけだと白い目で見られそうなので…もうちょい説明的な表現では「C++11標準ライブラリ相当の同期プリミティブ群を提供する、ノンプリエンティブなスレッドライブラリ」となるでしょうか。さらに協調的同期プリミティブとしてのミューテックス(mutex)、条件変数(condition variable)も併せて提供します。

例えば下記コードのように、マルチスレッドプログラムと同じ構造で記述された生産者-消費者パターンが、ファイバーライブラリ上ではシングルスレッドアプリケーションとして動作します。ちなみに、このコードでfiber名前空間→std名前空間, fiberクラス→threadクラスへと単純置換すると、そのままマルチスレッドアプリケーションになります。(サンプルコード全体はhttps://gist.github.com/2342248

// 上限付きFIFOキュー
template <class T, std::size_t N>
class bound_queue {
public:
  typedef T value_type;
  bound_queue() : q_(N) {}
  void push(value_type data)
  {
    fiber::unique_lock<fiber::mutex> lk(mtx_);
    cv_pop_.wait(lk, [=]{ return !q_.full(); });
    q_.push_back(data);
    cv_push_.notify_all();
  }
  value_type pop()
  {
    fiber::unique_lock<fiber::mutex> lk(mtx_);
    cv_push_.wait(lk, [=]{ return !q_.empty(); });
    value_type result = q_.front();
    q_.pop_front();
    cv_pop_.notify_all();
    return result;
  }
private:
  boost::circular_buffer<value_type> q_;
  fiber::mutex mtx_;
  fiber::condition_variable cv_push_;
  fiber::condition_variable cv_pop_;
};
typedef bound_queue<int, 5> QueueType;

// 生産者ファイバー
void producer(QueueType& queue)
{
  for (;;) {
    int data = /*...*/;
    queue.push(data);
  }
}

// 消費者ファイバー
void consumer(QueueType& queue)
{
  for (;;) {
    int data = queue.pop();
    //...
  }
}

前提知識

Boost.Contextはコンテキスト切り替え機能を提供するライブラリです。面倒なので(またの名を説明力不足のため)ここでは“コンテキスト切り替え”の説明は行いません。次の記事/資料などを参照してください。

本ライブラリで提供する“ファイバー(Fiber)”は、コルーチン(Coroutine)/マイクロスレッド(Micro-thread)/軽量スレッド(Lightweight thread)とも呼ばれる協調的マルチタスク機構のための部品です。通常の“スレッド(Thread)”と“ファイバー”との違いは、OSが自動的にスレッドを切り替えて並行処理を実現する(プリエンティブ・マルチタスク)か、プログラマが明示的にファイバーを切り替えて協調的に並行処理を実現する(ノンプリエンプティブ・マルチタスク)と捉えると分かり易いかと。

また、C++11標準ライブラリが提供するスレッド(std::thread)や基本的な同期プリミティブ(std::mutex, std::unique_lock, std::condition_variableなど)の使い方を理解していることを前提としています。

Pros vs. Cons

このライブラリの長所は「C++11標準スレッドライブラリと同じインターフェイスを持つので、マルチスレッド動作へシームレスに移行可能」「プログラムが常に決定的な動作となるので、本物のマルチスレッドアプリに比べてデバッグが容易」あたりでしょうか?なお、“最初からマルチスレッドで良いんじゃ?”というもっともな質問への回答は持ち合わせていません。

一方で、短所は「必ずシングルスレッドで動作するため、マルチコアといったハードウェア並行性を活用できない」「Boostライブラリのsvn-trunkに依存するのでunstable」などです。

ライブラリ設計方針

ライブラリインターフェイスは、C++11標準スレッド関連<thread>, <mutex>, <condition_variable>ヘッダ提供のAPIに可能な限り似せています。ただし、C++11標準スレッドで提供されるタイムアウト系処理(std::this_thread::sleep_for関数、std::timed_mutexクラスなど)は削除しています。これは実行ファイバーの切り替えタイミングはユーザの責任であり、タイムアウト値の指定を行ったとしてもそれを守る仕組みが存在しないためです。

ファイバー間のスケジューリング方式は、単純ラウンドロビンによる「公平(fair)スケジューリング」を採ります。つまりファイバーがブロック中か否かに関わらず、スケジューラは全ファイバーに対し平等に実行機会を与えます。例えば、ロック獲得待ちでブロック中のファイバーであっても順番がくれば起動され、ロック獲得に失敗した場合は即座に次ファイバーへと実行が移ります。

ファイバーライブラリ独自機能として、ファイバーのスケジューリングポリシ変更関数を提供します。2つのポリシ:lazyモードとeagarモードが存在し、ライブラリ規定値はlazyモードとなっています。

  • lazyモード:別ファイバーへのコンテキスト切り替えを可能な限り後回しにする。
  • eagerモード:別ファイバーへのコンテキスト切り替えを可能な限り早期に行う。

スケジューリングポリシ挙動は、新しいファイバー作成時の切り替えが分かり易いかと思います。lazyモードでは新ファイバーへのコンテキスト切り替えが“同ファイバーとのjoin時”まで遅延されますが、eagerモードでは“新ファイバー作成直後”に新ファイバーへとコンテキスト切り替えます。

fiber::fiber fb(&proc);  // eagerモード:fbに切り替え
proc();
fb.join();  // lazyモード:fbに切り替え

提供機能の一覧

C++11標準スレッドライブラリとファイバーライブラリがそれぞれ提供する機能の対応関係は下記の通りです。

C++標準スレッド ファイバーライブラリ
スレッド/ファイバー std::threadクラス
std::this_thread名前空間
fiber::fiberクラス
fiber::this_fiber名前空間
ミューテックス std::mutexクラス
std::recursive_mutexクラス
fiber::mutexクラス
fiber::recursive_mutexクラス
ロック std::lock_guardクラステンプレート
std::unique_lockクラステンプレート
fiber::lock_guardクラステンプレート
fiber::unique_lockクラステンプレート
条件変数 std::condition_variableクラス fiber::condition_variableクラス
(追加機能) fiber::get_scheduling_policy関数
fiber::set_scheduling_policy関数

ポイント解説

実行ファイバーの切り替えは、スケジューリングポリシ設定に応じて下表のコンテキスト切り替えポイントで行われます。

lazyモード eagarモード
fiberコンストラクタ
fiber::join()
mutex::lock()
recursive_mutex::lock()
mutex::unlock()
recursive_mutex::unlock()
condition_variable::wait()
condition_variable::notify_one()
condition_variable::notify_all()
this_fiber::yield()

Boost.Contextライブラリによるファイバー間コンテキスト切り替え実装は、fiber::detail::scheduler::yieldメンバ関数がコアとなります…というか本質的にこの関数が全てです。実装概略は下記の通りです。

  • fiberオブジェクトとコンテキストboost::contexts::contextとを1:1対応させます。なお、プログラムに最初から1つあるファイバーは、便宜上“メインファイバー”と呼びます。
  • ファイバーは 実行状態/コンテキスト未開始状態/yieldメンバ関数内で休止状態 の何れかの状態を取ります。なお、スケジューラから見た未開始状態と休止状態の違いは、コンテキストに対して次に呼ぶべきメソッドがstartかresumeかの差異だけです。また常にただ1個のファイバーが実行状態にあり、プログラム開始直後はメインファイバーがそれに該当します。
  • コンテキスト切り替えでは必ずメインファイバーのコンテキストを経由します。すなわち、yieldメンバ関数がメインファイバー以外のコンテキストで呼び出された場合は、suspendで一旦メインファイバーへコンテキストを戻します。続いてスケジューラが決定した次ファイバーのコンテキストをstart/resumeし、コンテキスト切り替えの後にyieldメンバ関数を抜けます。ちなみに次ファイバーがメインファイバーの場合は、コンテキストのresumeを行わずにyieldメンバ関数を抜けるだけです。

ライブラリコード

ライブラリコードはBoost Software License 1.0で公開しています。動作確認はGCC 4.6.3+Boost svn-trunk rr77773@Ubuntu 11.10で行いました。[https://gist.github.com/2318086

既知の問題とか

このライブラリはコンセプト実証レベルで実装しているため、下記箇所で手抜きをしています。

  • fiberコンストラクタ引数でコピー不可かつムーブ可能な型を扱えない(std::threadはできる)
  • fiber::id型が単にuintptr_tのtypedef(std::thread::idは独立したclass)
  • C++11標準ライブラリ<future>の提供APIに未対応(std::future, std::asyncなど)