プログラミングについてのWiki。

http://atnd.org/events/19963

勉強会で配布する資料の叩き台です。

※ 完成版はこちらhttp://d.hatena.ne.jp/n7shi/20111130



thunkとは

  • 英単語としては特に意味がない。
  • Algol 60でアドレスを遅延評価するために導入。
  • PEでもDLLからインポートした関数のアドレスがサンクに入る。
  • 16ビットコードと32ビットコードの変換もサンクと呼ばれる。
  • 今回は実行時にアドレスを変換する機構をまとめてサンクと呼ぶ。
  • サンクの生成はOSやCPUに依存する。
    • 今回はi386(32bit x86)に焦点を絞る。
  • 古いAPIへのコールバックや独自言語の実装には有用。
    • 純粋なC++の枠内ではあまり使う余地はない。

アセンブラとスタック


必要となる範囲を簡単に説明。

プロローグとエピローグ

関数の最初と最後にある、
スタックフレームの確保など決まった処理。

int test(int a, int b) {
    return a + b;
}

Releaseでアセンブリ出力

_test PROC
    ; プロローグ
    push ebp
    mov  ebp, esp
    ; 本体
    mov  eax, DWORD PTR 12[ebp]
    add  eax, DWORD PTR 8[ebp]
    ; エピローグ
    pop  ebp
    ret  0
_test ENDP

ebpは関数のスタックフレームを明示するレジスタ。
デバッグの時にローカル変数などが把握しやすくなる。

「フレームポインターなし」を有効にする。
(gccでは -fomit-frame-pointer)

_test PROC
    mov eax, DWORD PTR 12[esp-4]
    add eax, DWORD PTR 8[esp-4]
    ret  0
_test ENDP

ebpを経由せずにespを直接使うようになる。
ディスプレースメントがebpを意識したものになっている。
  • 12[esp-4] → 8[esp]
  • 8[esp-4] → 4[esp]

スタック

スタックは一時的な情報を格納するのに使用するメモリ領域
具体的にはローカル変数や呼び出し履歴などが入っている

FIFOバッファ(最後に入れたデータが最初に取り出される)
push/pop命令で操作

push dword 1
push dword 2
push dword 3
pop eax
pop ebx
pop ecx

eax: 3
ebx: 2
ecx: 1

スタックはespが指している
入れる前に減らして、出した後に増やす

sub esp, 4
mov dword [esp], 1
sub esp, 4
mov dword [esp], 2
sub esp, 4
mov dword [esp], 3
mov eax, [esp]
add esp, 4
mov ebx, [esp]
add esp, 4
mov ecx, [esp]
add esp, 4

練習: esp=0x10として動作をトレース

関数の簡単な呼び方

; test(2, 3);
push 3
push 2
call test
add esp, 8
  • 引数は後ろからpushする
  • eaxに結果が入る
  • pushした分のスタックは add esp, 8 で戻している
  • popを使わないのは値を捨てるから
  • このようにespがちょこちょこ動くとき、ebpでの固定に意味が出る

あらかじめ引数に必要な分スタックを確保する手法もある

sub esp, 8
mov [esp+4], 3
mov [esp], 2
call test
add esp, 8

gccでは関数全体で引数に使うスタックを確定させた上で、
プロローグであらかじめスタックを確保するコードを吐く。
そのためアセンブリを出力させてもpushが出てこない。

引数

関数を呼び出すcall命令は戻り先をスタックに積む
  • call test == push eip; jmp test(擬似コード)
  • ret == pop edx; jmp edx
  • CPUによっては擬似コード相当の命令を明示的に書くものがある
  • 詳細は坂井さんが本を執筆中
http://kozos.jp/books/asm/

引数を積んで呼び出した場合のスタック構造

push 3
push 2
call test

esp+0: 戻り先
esp+4: 2
esp+8: 3

そのため呼び出し先で引数を [esp+4], [esp+8] として取得できる。
引数を足して返す関数は以下の通り。

test:
    mov eax, [esp+4]
    add eax, [esp+8]
    ret

インラインアセンブラ

_declspec(naked): プロローグとエピローグの省略(VC++専用)

#include <stdio.h>

int test1(int a, int b) {
    return a + b;
}

_declspec(naked) int test2(int a, int b) {
    _asm {
        mov eax, 4[esp]
        add eax, 8[esp]
        ret
    }
}

int main() {
    printf("test1(2, 3): %d\n", test1(2, 3));
    printf("test2(2, 3): %d\n", test2(2, 3));
}

gccにはnakedというattributeがあるが、x86には未対応
関数丸ごとインラインアセンブラで書く(VC++ではできない)

#include <stdio.h>

int test1(int a, int b) {
    return a + b;
}

extern "C" int test2(int, int);
asm(
"_test2:\n"
"    mov eax, dword ptr [esp+4]\n"
"    add eax, dword ptr [esp+8]\n"
"    ret\n");

int main() {
    printf("test1(2, 3): %d\n", test1(2, 3));
    printf("test2(2, 3): %d\n", test2(2, 3));
}

アセンブラをIntel形式で書いているためオプションが必要
g++ -masm=intel 03.cpp

test2にextern "C"を付けているのはマングリング対策
付けない場合はこんな感じ

int test2(int, int);
asm(
"__Z5test2ii:\n"
"    mov eax, dword ptr [esp+4]\n"
"    add eax, dword ptr [esp+8]\n"
"    ret\n");

マングリング規則の変化に対応できないので避けた。
*** おまけ

デマングルの例(先頭の_を落としてから渡す)


JIT


実行時にマシンコードを生成して呼び出すのがJIT

NX

昔は単にメモリにコードを書き込むだけで出来ていた

#include <stdio.h>

unsigned char testf[] = {
    0x8b, 0x44, 0x24, 0x04, // mov eax, [esp+4]
    0x03, 0x44, 0x24, 0x08, // add eax, [esp+8]
    0xc3 // ret
};

int main() {
    auto test = reinterpret_cast<int(*)(int, int)>(&testf);
    printf("test(2, 3): %d\n", test(2, 3));
}

今はメモリ保護(NXビット)が働くためこのコードは落ちる。
ヒープやスタックでのコード実行も同様。
※想定外の箇所でコードを動かすのはマルウェアの常套手段。
*** NX対策

明示的に実行可能属性を付与したページを確保してコードをコピーする。

https://gist.github.com/1225880 (raw.cpp)

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

unsigned char testf[] = {
    0x8b, 0x44, 0x24, 0x04, // mov eax, [esp+4]
    0x03, 0x44, 0x24, 0x08, // add eax, [esp+8]
    0xc3 // ret
};

int main() {
    auto page = VirtualAlloc(nullptr, sizeof(testf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(page, testf, sizeof(testf));
    auto test = reinterpret_cast<int(*)(int, int)>(page);
    printf("test(2, 3): %d\n", test(2, 3));
    VirtualFree(page, 0, MEM_RELEASE);
}
*** 関数ポインタ型のコツみたいなもの。

int test(int, int);

この関数のポインタ型は以下の通り。

int (*)(int, int)

名前の部分が (*) になっている。
上の例ではこの型がreinterpret_castで使われている。

Xbyak

マシンコードはどうやって生成するのか?
ライブラリを使うと手軽。
  • Xbyak(カイビャック)

herumiさんによるインラインアセンブラ感覚でJITできるライブラリ

http://homepage1.nifty.com/herumi/soft/xbyak.html

ヘッダをincludeするだけで使用可能

https://gist.github.com/1225880 (xbyak.cpp)

#include <stdio.h>
#include "xbyak/xbyak.h"

struct TestFunc : public Xbyak::CodeGenerator {
    TestFunc() {
        mov(eax, ptr[esp+4]);
        add(eax, ptr[esp+8]);
        ret();
    }
};

int main() {
    TestFunc testf;
    auto test = reinterpret_cast<int(*)(int, int)>(
        const_cast<Xbyak::uint8 *>(testf.getCode()));
    printf("test(2, 3): %d\n", test(2, 3));
}

バイナリを確認するためにダンプしてみる。


この手のライブラリは GNU lightning など色々ある。
以下の記事にまとめられている。

Brainf*ckで学ぶスクリプト言語処理系高速化。
http://d.hatena.ne.jp/hogelog/20100914/p1

呼び出し規約


関数で引数や戻り値をどのように渡すかという規則

cdecl
  • C declare
  • 普通に定義した関数はすべてこれ: int test(int, int);
  • VC++ではオプションでデフォルトを変更できる: C/C++ → 詳細設定
  • 引数は後ろからスタックに積み、戻り値はeaxで返す
  • スタックは呼び出し側で戻す
  • 今まで引用した例もcdecl

; test(2, 3);
push 3
push 2
call _test
add esp, 8

_test:
mov eax, [esp+4] add eax, [esp+8] ret

stdcall
  • standard call
  • Win32APIの標準呼び出し規約。
  • 引数は後ろからスタックに積み、戻り値はeaxで返す
  • スタックは呼び出された側で戻す。
    • x86ではretに引数を渡してスタックを戻すことができる。

; test(2, 3);
push 3
push 2
call _test

_test:
mov eax, [esp+4] add eax, [esp+8] ret 8
  • 引数がゼロのときはcdeclと同じ
  • 可変長引数が使えない
    • Win32APIのwsprintf()はcdecl
    • wはWindowsでワイドではない。printfのワイド版はswprintf()で別物。
*** 関数ポインタ

ポインタのアスターの前に呼び出し規約を書く。

int(_stdcall *p)(int, int) = test;

簡単な確認方法として、型推論させてツールチップに出す方法がある。

auto p = test;

上例のpにマウスポインタを当ててツールチップを確認する。

fastcall
  • 処理系によって規約が異なる。VC++とgccは同じ。
  • Borland系ではよく使われていた。
  • VC++では最初の2つの引数をECX, EDX、残りをスタックで渡す。
  • スタックは呼び出された側で戻す。

; test(2, 3);
mov edx, 3
mov ecx, 2
call _test

_test:
mov eax, ecx add eax, edx ret
  • 引数がゼロのときはcdeclと同じ
  • 可変長引数が使えない
  • 引数をレジスタ渡しするのはRISCやx64に似ている

thiscall
  • インスタンスメンバ関数の呼び出し規約
  • 処理系によって規約が異なる
  • VC++ではthisポインタをECX、引数をスタックで渡す。
  • スタックは呼び出された側で戻す。可変長引数は呼び出した側で戻す。

; reinterpret_cast<Test *>(2)->test(3);
mov ecx, 2
push 3
call ?test@Test@@QAEHH@Z

; struct Test { int test(int a) { return a; } };
?test@Test@@QAEHH@Z:
mov eax, [esp+4] ret 4
  • g++は最初の引数としてthisを渡すcdecl
  • int Test::foo(int a) int foo(Test *this, int a)
  • 後述のthiscallに対するサンクは別に実装する必要がある

; reinterpret_cast<Test *>(2)->test(3);
push 3
push 2
call __ZN4Test4testEi
add esp, 8

; struct Test { int test(int a) { return a; } };
__ZN4Test4testEi:
mov eax, [esp+8] ret

問題

[問] stdcallの関数をサンクでcdeclに変換してください。
具体的には、以下のConvをXbyakで実装してください。

#include <stdio.h>
#include "xbyak/xbyak.h"

void call(int(*p)(int, int), int a, int b) {
    printf("%d\n", p(a, b));
}

int _stdcall test(int a, int b) { return a + b; }

int main() {
    Conv c(test);
    auto f = reinterpret_cast<int(*)(int, int)>(
        const_cast<Xbyak::uint8 *>(c.getCode()));
    call(f, 2, 3);
}

回答例

struct Conv : public Xbyak::CodeGenerator {
    Conv(int(_stdcall *p)(int, int)) {
        push(ptr[esp+8]);
        push(ptr[esp+8]);
        call(p);
        ret();
    }
};

関数型


C++で関数を型として扱う方法など。

関数ポインタ

関数を渡して呼ぶ。

#include <stdio.h>

void test(int a) {
    printf("%d\n", a);
}

int main() {
    auto f = test;
    f(1);
}

staticメンバも同じように扱える。

#include <stdio.h>

struct Test {
    static void show(int n) {
        printf("%d\n", n);
    }
};

int main() {
    auto f = Test::show;
    f(1);
}

型が同じなので、同じ関数に渡して呼び出せる。

#include <stdio.h>

void test(int a) {
    printf("%d\n", a);
}

struct Test {
    static void show(int n) {
        printf("%d\n", n);
    }
};

void call(void (*f)(int), int a) {
    f(a);
}

int main() {
    call(test, 1);
    call(Test::show, 1);
}

メンバ関数ポインタ

構文が厄介。

#include <stdio.h>

struct Test {
    int n;
    Test(int n) : n(n) {}
    void show() {
        printf("%d\n", n);
    }
};

int main() {
    Test t(1);
    auto f = &Test::show;
    (t.*f)();
}

autoがないと非常に面倒。

void (Test::*f)() = &Test::show;

非メンバ関数のポインタを取るときには & が省略できるが、
メンバ関数では省略できない。
下の例ではf1とf2は同じ。

void test();
struct Test { void test(); } t;

○ auto f1 = test;
○ auto f2 = &test;
× auto f3 = Test::test;
○ auto f4 = &Test::test;

呼び出し時の参照剥がしについても同様。
というよりC++では .* で1つの演算子になっているし、
そうでないとただのメンバアクセスと区別が付かない。

○ f2();
○ (*f2)();
× (t.f4)();
○ (t.*f4)();

staticの有無で互換性がなくなる。型もクラスに依存する。

https://gist.github.com/1273084

#include <stdio.h>

struct Test1 {
    int n;
    Test1(int n) : n(n) {}
    void show() {
        printf("%d\n", n);
    }
};

struct Test2 {
    int n;
    Test2(int n) : n(n) {}
    void show() {
        printf("%d\n", n);
    }
};

struct Test3 {
    static void show() {
        printf("?\n");
    }
};

void show() {
    printf("!\n");
}

void call(void (*f)()) { f(); }

int main() {
    Test1 t1(1);
    Test2 t2(2);
    auto f1 = &Test1::show;
    auto f2 = &Test2::show;
    auto f3 = &Test3::show;
    auto f4 = &show;
    (t1.*f1)();
    (t2.*f2)();
    (*f3)();
    (*f4)();
    call(f3);
    call(f4);
}

f1 != f2 != (f3 == f4)
  • インスタンスメンバとクラスメンバは扱いが異なる。
  • クラスメンバ(static)と非メンバは同じ扱い。同じようにcall()に渡せる。
  • インスタンスメンバは型にクラス名が入るため、f1とf2は型が異なる。

とりあえず同じ型として扱うにはどうするか。

関数オブジェクト

関数のように振舞うクラス。

#include <stdio.h>

struct Test {
    int n;
    Test(int n) : n(n) {}
    void operator()() {
        printf("%d\n", n);
    }
};

int main() {
    Test t(1);
    t();
}

C++11ではラムダ式により簡単に関数オブジェクトが作れる。
以下のtはラムダ式によって作られた関数オブジェクト。

#include <stdio.h>

int main() {
    int n = 1;
    auto t = [n] { printf("%d\n", n); };
    t();
}

標準ライブラリには std::function という
関数ポインタのように使う関数オブジェクトがある。
std::bind()やラムダ式などでメンバ関数を渡せる。

これを使うとメンバ・非メンバを統合して扱えるようになる。

https://gist.github.com/1273115

#include <functional>
#include <stdio.h>

struct Test1 {
    int n;
    Test1(int n) : n(n) {}
    void show() {
        printf("%d\n", n);
    }
};

struct Test2 {
    int n;
    Test2(int n) : n(n) {}
    void show() {
        printf("%d\n", n);
    }
};

struct Test3 {
    static void show() {
        printf("?\n");
    }
};

void show() {
    printf("!\n");
}

void call(const std::function<void()> &f) {
    f();
}

int main() {
    Test1 t1(1);
    Test2 t2(2);
    auto f1 = std::bind(&Test1::show, &t1);
    auto f2 = [&] { t2.show(); };
    auto f3 = Test3::show;
    auto f4 = show;
    call(f1);
    call(f2);
    call(f3);
    call(f4);
}
*** おまけ

比較のためF#に移植。

type Test1(n) =
    member x.show() = printfn "%d" n

type Test2(n) =
    member x.show() = printfn "%d" n

type Test3 =
    static member show() = printfn "?"

let show() = printfn "!"

let call f = f()

let t1 = new Test1(1)
let t2 = new Test2(2)
let d1 = t1.show
let d2 = fun() -> t2.show()
let d3 = Test3.show
let d4 = show
call d1
call d2
call d3
call d4

d1, d2, d3, d4はすべて同じ型: unit->unit

サンク


C++と古いAPIの橋渡しとして実行時コード生成を利用する。

JITによるラッパー

コードがC++で閉じているなら関数オブジェクトで充分。
しかしOSのAPIで関数ポインタ型が指定されている場合など、
関数オブジェクトをそのまま使えないケースがある。

JITでラッパーを作ってしまえば解決!

https://gist.github.com/1225997

#include <stdio.h>
#include "xbyak/xbyak.h"

template <class T>
struct Delegate : public Xbyak::CodeGenerator {
    Delegate(T *t, void (T::*p)()) {
        mov(ecx, reinterpret_cast<intptr_t>(t));
        jmp(*reinterpret_cast<void **>(&p));
    }

    void (*getPtr())() {
        return reinterpret_cast<void (*)()>(
            const_cast<Xbyak::uint8 *>(getCode()));
    }
};

struct Test1 {
    int n;
    Test1(int n) : n(n) {}
    void show() {
        printf("%d\n", n);
    }
};

struct Test2 {
    int n;
    Test2(int n) : n(n) {}
    void show() {
        printf("%d\n", n);
    }
};

struct Test3 {
    static void show() {
        printf("?\n");
    }
};

void show() {
    printf("!\n");
}

void call(void (*f)()) { f(); }

int main() {
    Test1 t1(1);
    Test2 t2(2);
    Delegate<Test1> d1(&t1, &Test1::show);
    Delegate<Test2> d2(&t2, &Test2::show);
    call(d1.getPtr());
    call(d2.getPtr());
    call(Test3::show);
    call(show);
}
  • メンバ関数ポインタにインスタンスを埋め込んで関数ポインタに変換。
  • このJITラッパーが今回のメインテーマであるサンク。
  • gccではthiscallが異なるためそのままでは動かない。
*** 関数ポインタを返す関数

void (*getPtr())();
  • getPtr() が関数名と引数で、それ以外の void (*)() が戻り値の型。
  • void (*f)() は変数で、fの部分に引数を付けたら関数。

引数がある場合

VC++のthiscallでは呼び出し先が引数のスタックを片付けるため、
サンクではcdeclとの変換が必要となる。

https://gist.github.com/1273062

#include <stdio.h>
#include "xbyak/xbyak.h"

template <class T, class TA1>
struct Thunk : public Xbyak::CodeGenerator {
    Thunk(T *t, void (T::*p)(TA1)) {
        mov(ecx, reinterpret_cast<intptr_t>(t));
        push(ptr[esp+4]);
        call(*reinterpret_cast<void **>(&p));
        ret();
    }

    void (*getPtr())(TA1) {
        return reinterpret_cast<void (*)(TA1)>(
            const_cast<Xbyak::uint8 *>(getCode()));
    }
};

struct Test {
    int n;
    Test(int n) : n(n) {}
    void show(int n2) {
        printf("%d, %d\n", n, n2);
    }
};

void show(int n) {
    printf("%d\n", n);
}

void call(void (*f)(int), int n) { f(n); }

int main() {
    Test t(1);
    Thunk<Test, int> thunk(&t, &Test::show);
    call(thunk.getPtr(), 2);
    call(show, 3);
}

void (*getPtr())(TA1)
  • getPtr()が関数名と引数
  • それ以外の void (*)(TA1) が戻り値の型

部分適用

サンクに引数を埋め込めば部分適用が可能。

#include <stdio.h>
#include "xbyak/xbyak.h"

struct Bind : public Xbyak::CodeGenerator {
    Bind(int(*f)(int, int), int a) {
        push(a);
        push(ptr[esp+8]);
        call(reinterpret_cast<void *>(f));
        add(esp, 8);
        ret();
    }

    int(*getPtr())(int) {
        return reinterpret_cast<int(*)(int)>(
            const_cast<Xbyak::uint8 *>(getCode()));
    }
};

int add(int a, int b) { return a + b; }

void call(int(*p)(int), int a) {
    printf("%d\n", p(a));
}

int main() {
    Bind inc(add, 1);
    call(inc.getPtr(), 2);
}

しかしこれはやろうと思えば出来る、程度のネタ。
std::bindで部分適用してからサンクに入れた方がスマート。

#include <functional>
#include <stdio.h>
#include "xbyak/xbyak.h"

template <class T, class TA1, class TR>
struct Thunk : public Xbyak::CodeGenerator {
    Thunk(T *f) {
        mov(ecx, reinterpret_cast<intptr_t>(f));
        push(ptr[esp+4]);
        auto p = &T::operator();
        call(*reinterpret_cast<void **>(&p));
        ret();
    }

    TR(*getPtr())(TA1) {
        return reinterpret_cast<TR(*)(TA1)>(
            const_cast<Xbyak::uint8 *>(getCode()));
    }
};

int add(int a, int b) { return a + b; }

void call(int(*p)(int), int a) {
    printf("%d\n", p(a));
}

int main() {
    std::function<int(int)> inc =
        std::bind(add, std::placeholders::_1, 1);
    Thunk<decltype(inc), int, int> thunk(&inc);
    call(thunk.getPtr(), 2);
}

このように可能な限りC++の範囲内で扱って、サンクは最終手段。

クロージャ

キャプチャを部分適用で表現すれば、サンクでクロージャが作れる。

https://gist.github.com/1226147

#include <functional>
#include <stdio.h>
#include "xbyak/xbyak.h"

void call(void (*f)()) { f(); }

int main() {
    int a = 1;
    struct _ : public Xbyak::CodeGenerator {
        _(int *a) {
            push(reinterpret_cast<intptr_t>(a));
            call(reinterpret_cast<void *>(run));
            add(esp, 4);
            ret();
        }

        static void run(int &a) {
            printf("%d\n", a++);
        }
    } closure(&a);
    auto f = reinterpret_cast<void (*)()>(
        const_cast<Xbyak::uint8 *>(closure.getCode()));
    f();
    f();
    call(f);
}

これは独自に言語を作る場合は使える手法だと思うが、
言語仕様でクロージャを持つC++11で敢えてやる意味は薄い。

普通にC++で作ったクロージャをサンクで包んだ方がシンプル。

https://gist.github.com/1273073

#include <functional>
#include <stdio.h>
#include "xbyak/xbyak.h"

template <class T>
struct Thunk : public Xbyak::CodeGenerator {
    Thunk(T *f) {
        mov(ecx, reinterpret_cast<intptr_t>(f));
        auto p = &T::operator();
        jmp(*reinterpret_cast<void **>(&p));
    }

    void (*getPtr())() {
        return reinterpret_cast<void (*)()>(
            const_cast<Xbyak::uint8 *>(getCode()));
    }
};

void call(void (*f)()) { f(); }

int main() {
    int a = 1;
    auto f = [&] { printf("%d\n", a++); };
    f();
    f();
    Thunk<decltype(f)> thunk(&f);
    auto f2 = thunk.getPtr();
    f2();
    call(f2);
}

Win32API


[問] Windowsアプリケーションの雛形を改造して、以下をメンバ関数にしてください。

ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);



class Window {
public:
ATOM MyRegisterClass(HINSTANCE hInstance); BOOL InitInstance(HINSTANCE hInstance, int nCmdShow); LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
};
  • メンバは適宜追加してください。
  • CALLBACKはstdcallを意味します。

回答例

https://gist.github.com/1226294

--- win32thunk.cpp.orig
+++ win32thunk.cpp
@@ -3,6 +3,7 @@
 
 #include "stdafx.h"
 #include "win32thunk.h"
+#include "xbyak/xbyak.h"
 
 #define MAX_LOADSTRING 100
 
@@ -12,11 +13,25 @@
 TCHAR szWindowClass[MAX_LOADSTRING];			// メイン ウィンドウ クラス名
 
 // このコード モジュールに含まれる関数の宣言を転送します:
-ATOM				MyRegisterClass(HINSTANCE hInstance);
-BOOL				InitInstance(HINSTANCE, int);
-LRESULT CALLBACK	WndProc(HWND, UINT, WPARAM, LPARAM);
 INT_PTR CALLBACK	About(HWND, UINT, WPARAM, LPARAM);
 
+template <class T, class TA1, class TA2, class TA3, class TA4, class TR>
+struct StdCallThunk : public Xbyak::CodeGenerator {
+    void init(T *t, TR(T::*p)(TA1, TA2, TA3, TA4)) {
+        mov(ecx, reinterpret_cast<intptr_t>(t));
+        jmp(*reinterpret_cast<void **>(&p));
+    }
+};
+
+class Window {
+protected:
+    StdCallThunk<Window, HWND, UINT, WPARAM, LPARAM, LRESULT> wndProc;
+public:
+    ATOM MyRegisterClass(HINSTANCE hInstance);
+    BOOL InitInstance(HINSTANCE hInstance, int nCmdShow);
+    LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
+};
+
 int APIENTRY _tWinMain(HINSTANCE hInstance,
                      HINSTANCE hPrevInstance,
                      LPTSTR    lpCmdLine,
@@ -32,10 +47,11 @@
 	// グローバル文字列を初期化しています。
 	LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
 	LoadString(hInstance, IDC_WIN32THUNK, szWindowClass, MAX_LOADSTRING);
-	MyRegisterClass(hInstance);
+	Window win;
+	win.MyRegisterClass(hInstance);
 
 	// アプリケーションの初期化を実行します:
-	if (!InitInstance (hInstance, nCmdShow))
+	if (!win.InitInstance (hInstance, nCmdShow))
 	{
 		return FALSE;
 	}
@@ -70,14 +86,16 @@
 //    正しい形式の小さいアイコンを取得できるようにするには、
 //    この関数を呼び出してください。
 //
-ATOM MyRegisterClass(HINSTANCE hInstance)
+ATOM Window::MyRegisterClass(HINSTANCE hInstance)
 {
+	wndProc.init(this, &Window::WndProc);
+
 	WNDCLASSEX wcex;
 
 	wcex.cbSize = sizeof(WNDCLASSEX);
 
 	wcex.style			= CS_HREDRAW | CS_VREDRAW;
-	wcex.lpfnWndProc	= WndProc;
+	wcex.lpfnWndProc	= (WNDPROC)wndProc.getCode();
 	wcex.cbClsExtra		= 0;
 	wcex.cbWndExtra		= 0;
 	wcex.hInstance		= hInstance;
@@ -101,7 +119,7 @@
 //        この関数で、グローバル変数でインスタンス ハンドルを保存し、
 //        メイン プログラム ウィンドウを作成および表示します。
 //
-BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
+BOOL Window::InitInstance(HINSTANCE hInstance, int nCmdShow)
 {
    HWND hWnd;
 
@@ -131,7 +149,7 @@
 //  WM_DESTROY	- 中止メッセージを表示して戻る
 //
 //
-LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
+LRESULT Window::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
 {
 	int wmId, wmEvent;
 	PAINTSTRUCT ps;

委譲


イベントハンドラを関数オブジェクト化して委譲モデル化。
  • ハンドルはあちこちで使うので、Windowクラスのpublicメンバに追加。
  • Window::InitInstance()でローカル変数の宣言を削除。

これで自動的にメンバの方に代入される。

--- win32thunk.cpp.orig
+++ win32thunk.cpp
@@ -27,6 +27,7 @@
 protected:
     StdCallThunk<Window, HWND, UINT, WPARAM, LPARAM, LRESULT> wndProc;
 public:
+    HWND hWnd;
     ATOM MyRegisterClass(HINSTANCE hInstance);
     BOOL InitInstance(HINSTANCE hInstance, int nCmdShow);
     LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
@@ -121,8 +122,6 @@
 //
 BOOL Window::InitInstance(HINSTANCE hInstance, int nCmdShow)
 {
-   HWND hWnd;
-
    hInst = hInstance; // グローバル変数にインスタンス処理を格納します。
 
    hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,

Command


WM_COMMANDを委譲化

あまり修正しないヘッダはstdafx.hに押し込むとビルドが速くなる。

 // TODO: プログラムに必要な追加ヘッダーをここで参照してください。
+#include <list>
+#include <functional>
+#include "xbyak/xbyak.h"

Windowクラスのpublicメンバに追加

※listに入れることでマルチキャストを可能にしている。

    std::list<std::function<bool(int, int)>> Command;

Window::WndProcで対応

※for eachはVC++の独自拡張なので、適宜修正してください。

    case WM_COMMAND:
        for each (auto f in Command)
            if (f(LOWORD(wParam), HIWORD(wParam)))
                return 0;
        return DefWindowProc(hWnd, message, wParam, lParam);

イベントを設定

    win.Command.push_back([&](int id, int e)->bool {
        // 選択されたメニューの解析:
        switch (id)
        {
        case IDM_ABOUT:
            DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), win.hWnd, About);
            return true;
        case IDM_EXIT:
            DestroyWindow(win.hWnd);
            return true;
        }
        return false;
    });

Paint


WM_PAINTを委譲化

Windowクラスのpublicメンバに追加

    std::list<std::function<void(HDC)>> Paint;

Window::WndProcで対応

    case WM_PAINT:
        hdc = BeginPaint(hWnd, &ps);
        // TODO: 描画コードをここに追加してください...
        for each (auto f in Paint) f(hdc);
        EndPaint(hWnd, &ps);
        break;

イベントを設定

    win.Paint.push_back([&](HDC hdc) {
        auto oldPen = (HPEN)SelectObject(hdc, GetStockObject(BLACK_PEN));
        auto oldBrush = (HBRUSH)SelectObject(hdc, GetStockObject(GRAY_BRUSH));
        Rectangle(hdc, 10, 10, 40, 40);
        SelectObject(hdc, oldPen);
        SelectObject(hdc, oldBrush);
    });

四角が表示される。

マウスイベント


マウスボタン関係のメッセージを委譲化。

stdafx.hに追加

#include <windowsx.h>

Windowクラスのpublicメンバに追加

    std::list<std::function<void(int, int, int, WPARAM)>> MouseDown, MouseUp;
    std::list<std::function<void(int, int, WPARAM)>> MouseMove;

Windowクラスのprotectedメンバに追加

    void OnMouseDown(int button, WPARAM wParam, LPARAM lParam) {
        int x = GET_X_LPARAM(lParam), y = GET_Y_LPARAM(lParam);
        for each (auto f in MouseDown) f(button, x, y, wParam);
    }
    void OnMouseUp(int button, WPARAM wParam, LPARAM lParam) {
        int x = GET_X_LPARAM(lParam), y = GET_Y_LPARAM(lParam);
        for each (auto f in MouseUp) f(button, x, y, wParam);
    }

Window::WndProcで対応

    case WM_LBUTTONDOWN: OnMouseDown(1, wParam, lParam); break;
    case WM_RBUTTONDOWN: OnMouseDown(2, wParam, lParam); break;
    case WM_MBUTTONDOWN: OnMouseDown(3, wParam, lParam); break;
    case WM_XBUTTONDOWN: OnMouseDown(3 + HIWORD(wParam), LOWORD(wParam), lParam); break;
    case WM_LBUTTONUP: OnMouseUp(1, wParam, lParam); break;
    case WM_RBUTTONUP: OnMouseUp(2, wParam, lParam); break;
    case WM_MBUTTONUP: OnMouseUp(3, wParam, lParam); break;
    case WM_XBUTTONUP: OnMouseUp(3 + HIWORD(wParam), LOWORD(wParam), lParam); break;
    case WM_MOUSEMOVE:
        for each (auto f in MouseMove)
            f(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), wParam);
        break;

コードの整理


ここまでのソース。

https://gist.github.com/1273003

ちょっと長いので、ソースを分割して整理。

https://gist.github.com/1274679

Window.h

#pragma once

#include <list>
#include <functional>
#include <string>
#include <windows.h>
#include "xbyak/xbyak.h"

template <class T, class TA1, class TA2, class TA3, class TA4, class TR>
struct StdCallThunk : public Xbyak::CodeGenerator {
    void init(T *t, TR(T::*p)(TA1, TA2, TA3, TA4)) {
        mov(ecx, reinterpret_cast<intptr_t>(t));
        jmp(*reinterpret_cast<void **>(&p));
    }
};

typedef std::basic_string<TCHAR> tstring;

class Window {
protected:
    HINSTANCE hInst;
    tstring windowClass;
    StdCallThunk<Window, HWND, UINT, WPARAM, LPARAM, LRESULT> wndProc;
    void OnMouseDown(int button, WPARAM wParam, LPARAM lParam);
    void OnMouseUp(int button, WPARAM wParam, LPARAM lParam);

public:
    HWND hWnd;
    ATOM MyRegisterClass(HINSTANCE hInstance, const tstring &windowClass);
    BOOL InitInstance(const tstring &title, int nCmdShow);
    LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);

    std::list<std::function<bool(int, int)>> Command;
    std::list<std::function<void(HDC)>> Paint;
    std::list<std::function<void(int, int, int, WPARAM)>> MouseDown, MouseUp;
    std::list<std::function<void(int, int, WPARAM)>> MouseMove;
};

Window.cpp

#include "stdafx.h"
#include "resource.h"

void Window::OnMouseDown(int button, WPARAM wParam, LPARAM lParam) {
    int x = GET_X_LPARAM(lParam), y = GET_Y_LPARAM(lParam);
    for each (auto f in MouseDown) f(button, x, y, wParam);
}

void Window::OnMouseUp(int button, WPARAM wParam, LPARAM lParam) {
    int x = GET_X_LPARAM(lParam), y = GET_Y_LPARAM(lParam);
    for each (auto f in MouseUp) f(button, x, y, wParam);
}

//
//  関数: Window::MyRegisterClass()
//
//  目的: ウィンドウ クラスを登録します。
//
//  コメント:
//
//    この関数および使い方は、'RegisterClassEx' 関数が追加された
//    Windows 95 より前の Win32 システムと互換させる場合にのみ必要です。
//    アプリケーションが、関連付けられた
//    正しい形式の小さいアイコンを取得できるようにするには、
//    この関数を呼び出してください。
//
ATOM Window::MyRegisterClass(HINSTANCE hInstance, const tstring &windowClass)
{
    hInst = hInstance;
    this->windowClass = windowClass;

    wndProc.init(this, &Window::WndProc);

    WNDCLASSEX wcex;

    wcex.cbSize = sizeof(WNDCLASSEX);

    wcex.style          = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc    = (WNDPROC)wndProc.getCode();
    wcex.cbClsExtra     = 0;
    wcex.cbWndExtra     = 0;
    wcex.hInstance      = hInstance;
    wcex.hIcon          = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WIN32THUNK));
    wcex.hCursor        = LoadCursor(NULL, IDC_ARROW);
    wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1);
    wcex.lpszMenuName   = MAKEINTRESOURCE(IDC_WIN32THUNK);
    wcex.lpszClassName  = windowClass.c_str();
    wcex.hIconSm        = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

    return RegisterClassEx(&wcex);
}

//
//   関数: Window::InitInstance(HINSTANCE, int)
//
//   目的: インスタンス ハンドルを保存して、メイン ウィンドウを作成します。
//
//   コメント:
//
//        この関数で、グローバル変数でインスタンス ハンドルを保存し、
//        メイン プログラム ウィンドウを作成および表示します。
//
BOOL Window::InitInstance(const tstring &title, int nCmdShow)
{
    hWnd = CreateWindow(windowClass.c_str(), title.c_str(), WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInst, NULL);

    if (!hWnd)
    {
        return FALSE;
    }

    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    return TRUE;
}

//
//  関数: Window::WndProc(HWND, UINT, WPARAM, LPARAM)
//
//  目的:  メイン ウィンドウのメッセージを処理します。
//
//  WM_COMMAND  - アプリケーション メニューの処理
//  WM_PAINT    - メイン ウィンドウの描画
//  WM_DESTROY  - 中止メッセージを表示して戻る
//
//
LRESULT Window::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    PAINTSTRUCT ps;
    HDC hdc;

    switch (message)
    {
    case WM_COMMAND:
        for each (auto f in Command)
            if (f(LOWORD(wParam), HIWORD(wParam)))
                return 0;
        return DefWindowProc(hWnd, message, wParam, lParam);
    case WM_PAINT:
        hdc = BeginPaint(hWnd, &ps);
        // TODO: 描画コードをここに追加してください...
        for each (auto f in Paint) f(hdc);
        EndPaint(hWnd, &ps);
        break;
    case WM_LBUTTONDOWN: OnMouseDown(1, wParam, lParam); break;
    case WM_RBUTTONDOWN: OnMouseDown(2, wParam, lParam); break;
    case WM_MBUTTONDOWN: OnMouseDown(3, wParam, lParam); break;
    case WM_XBUTTONDOWN: OnMouseDown(3 + HIWORD(wParam), LOWORD(wParam), lParam); break;
    case WM_LBUTTONUP: OnMouseUp(1, wParam, lParam); break;
    case WM_RBUTTONUP: OnMouseUp(2, wParam, lParam); break;
    case WM_MBUTTONUP: OnMouseUp(3, wParam, lParam); break;
    case WM_XBUTTONUP: OnMouseUp(3 + HIWORD(wParam), LOWORD(wParam), lParam); break;
    case WM_MOUSEMOVE:
        for each (auto f in MouseMove)
            f(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), wParam);
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

win32thunk.cpp (プロジェクト名により異なる)

// win32thunk.cpp : アプリケーションのエントリ ポイントを定義します。
//

#include "stdafx.h"
#include "win32thunk.h"

static tstring LoadTString(HINSTANCE hInstance, UINT uID) {
    const int buflen = 256;
    TCHAR buf[buflen];
    LoadString(hInstance, uID, buf, buflen);
    return buf;
}

// このコード モジュールに含まれる関数の宣言を転送します:
INT_PTR CALLBACK        About(HWND, UINT, WPARAM, LPARAM);

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

    // TODO: ここにコードを挿入してください。
    MSG msg;
    HACCEL hAccelTable;

    Window win;
    win.MyRegisterClass(hInstance, LoadTString(hInstance, IDC_WIN32THUNK));
    win.Command.push_back([&](int id, int e)->bool {
        // 選択されたメニューの解析:
        switch (id)
        {
        case IDM_ABOUT:
            DialogBox(hInstance, MAKEINTRESOURCE(IDD_ABOUTBOX), win.hWnd, About);
            return true;
        case IDM_EXIT:
            DestroyWindow(win.hWnd);
            return true;
        }
        return false;
    });
    win.Paint.push_back([&](HDC hdc) {
        auto oldPen = (HPEN)SelectObject(hdc, GetStockObject(BLACK_PEN));
        auto oldBrush = (HBRUSH)SelectObject(hdc, GetStockObject(GRAY_BRUSH));
        Rectangle(hdc, 10, 10, 40, 40);
        SelectObject(hdc, oldPen);
        SelectObject(hdc, oldBrush);
    });

    // アプリケーションの初期化を実行します:
    if (!win.InitInstance(LoadTString(hInstance, IDS_APP_TITLE), nCmdShow))
    {
        return FALSE;
    }

    hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_WIN32THUNK));

    // メイン メッセージ ループ:
    while (GetMessage(&msg, NULL, 0, 0))
    {
        if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

    return (int) msg.wParam;
}

// バージョン情報ボックスのメッセージ ハンドラーです。
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    UNREFERENCED_PARAMETER(lParam);
    switch (message)
    {
    case WM_INITDIALOG:
        return (INT_PTR)TRUE;

    case WM_COMMAND:
        if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
        {
            EndDialog(hDlg, LOWORD(wParam));
            return (INT_PTR)TRUE;
        }
        break;
    }
    return (INT_PTR)FALSE;
}

色々と突っ込みどころはあると思いますが、
今回はクラスライブラリを作るのが目的ではないので、
適当に見通しを良くする程度に留めています。

問題


[問] 描画されている四角をドラッグで動かせるようにしてください。

ヒント
  • 再描画要求: InvalidateRect(hWnd, nullptr, true);
回答例

https://gist.github.com/1226666

    POINT pb = { 10, 10 };
    win.Paint.push_back([&](HDC hdc) {
        auto oldPen = (HPEN)SelectObject(hdc, GetStockObject(BLACK_PEN));
        auto oldBrush = (HBRUSH)SelectObject(hdc, GetStockObject(GRAY_BRUSH));
        Rectangle(hdc, pb.x, pb.y, pb.x + 40, pb.y + 40);
        SelectObject(hdc, oldPen);
        SelectObject(hdc, oldBrush);
    });

    bool sel = false;
    POINT pd, pb2;
    win.MouseDown.push_back([&](int btn, int x, int y, WPARAM) {
        if (pb.x <= x && x <= pb.x + 40 && pb.y <= y && y <= pb.y + 40) {
            pd.x = x;
            pd.y = y;
            pb2 = pb;
            sel = true;
        }
    });
    win.MouseMove.push_back([&](int x, int y, WPARAM) {
        if (sel) {
            pb.x = pb2.x + (x - pd.x);
            pb.y = pb2.y + (y - pd.y);
            InvalidateRect(win.hWnd, nullptr, true);
        }
    });
    win.MouseUp.push_back([&](int, int, int, WPARAM) { sel = false; });

継続


次のような関数があったとする。

int test() {
    return 1;
    return 2;
    return 3;
}

通常、最初のreturn以降は到達できないコードとして扱われる。
これがもし、呼ばれる度に次に進むとしたらどうだろうか。

printf("%d\n", test()); // 1
printf("%d\n", test()); // 2
printf("%d\n", test()); // 3

このようなモデルを「継続」と呼び、
継続を含む関数をコルーチンと呼ぶ。

単純に実装しただけだと、一度しか呼べなくなってしまう。
※上の例では二度と1が返せなくなる。

そのため関数に対するインスタンスという考え方を導入する。
以下は擬似コード。

auto t1 = new test;
printf("%d\n", t1()); // 1
printf("%d\n", t1()); // 2
auto t2 = new test;
printf("%d\n", t2()); // 1

C#ではyieldとして継続がサポートされている。
yieldを含むメソッドはインスタンスを返す。

using System;
using System.Collections.Generic;

class Test
{
    static IEnumerable<int> test()
    {
        yield return 1;
        yield return 2;
        yield return 3;
    }

    static void Main()
    {
        var t1 = test().GetEnumerator();
        if (t1.MoveNext()) Console.WriteLine(t1.Current);
        if (t1.MoveNext()) Console.WriteLine(t1.Current);
        var t2 = test().GetEnumerator();
        if (t2.MoveNext()) Console.WriteLine(t2.Current);
    }
}

Pythonではジェネレータと呼ばれている。

def test():
    yield 1
    yield 2
    yield 3

t1 = test()
print t1.next()
print t1.next()
t2 = test()
print t2.next()

C#/Pythonともに実行結果は以下の通り。


継続をどう実装するかを見ていく。

setjmp/longjmp


setjmp/longjmpでスタックを巻き戻す例。

#include <stdio.h>
#include <setjmp.h>

jmp_buf jb;

int main() {
    if (setjmp(jb) == 0) {
        printf("setjmp\n");
        printf("test\n");
        longjmp(jb, 1);
        printf("never reach here\n");
    }
    printf("longjmp done.\n");
}
  • 一見過去に戻ったように見える。
  • レジスタを戻して続行するだけで、状態が保存されているわけではない。

次の例ではたまたま引数のアドレスが一致したためすり替わる。

#include <stdio.h>
#include <setjmp.h>

jmp_buf jb;

void test1(const char *s) {
    setjmp(jb);
    printf("&s: %p, s: %p; %s\n", &s, s, s);
}

void test2(int a) {
    printf("&a: %p, a: %d\n", &a, a);
    longjmp(jb, 0);
}

int main() {
    test1("test");
    test2(0);
    printf("end\n");
}

&s: 0013FE98, s: 00415758; test
&a: 0013FE98, a: 0
&s: 0013FE98, s: 00000000; (null)
end
  • aの値がそのままs扱いされて続行。
  • main()の中の流れには影響しない。
    • 過去に戻ったわけではないのがわかると思う。
  • 予期しない動作を引き起こす可能性があるため危険。
    • 下位構造から上位に戻るような使い方が推奨される。

RAII

RAIIは処理系依存

#include <stdio.h>
#include <setjmp.h>

jmp_buf jb;

class Test {
    const char *s;
public:
    Test(const char *s) : s(s) {}
    ~Test() { printf("%s\n", s); }
};

void test() {
    Test t1("t1");
    longjmp(jb, 1);
}

int main() {
    Test t2("t2");
    if (setjmp(jb) == 0) {
        Test t3("t3");
        test();
    }
}

[VC++]
t1
t3
t2

[g++]
t2

この違いは継続を扱う上で厄介。

コルーチン
  • 処理を途中で抜けて任意に再開できる構造をコルーチンと呼ぶ。
  • setjmp/longjmpを交互に呼べばコルーチンが実装できる。

#include <stdio.h>
#include <setjmp.h>

jmp_buf jb1, jb2;
int i;

void test() {
    for (i = 1; i <= 10; i++)
        if (setjmp(jb2) == 0)
            longjmp(jb1, i);
    longjmp(jb1, -1);
}

int main() {
    int value = setjmp(jb1);
    if (value == 0)
        test();
    else if (value > 0) {
        printf("%d\n", value);
        longjmp(jb2, 1);
    }
}
  • WindowsではファイバーとしてOSでサポートされている。
  • スタックが別に確保されるためローカル変数やRAIIの問題もない。
  • OSの機能を使うと原理の説明にならないので今回は使用しない。
  • 一旦抜けた処理に戻ることを「継続」と呼ぶ。
    • test()内でsetjmp(jb2)として保存したコンテキストに戻っている。
*** ローカル変数
  • 継続した際にローカル変数は復帰しない。
  • iをローカル変数にすると誤動作する。

 #include <setjmp.h>

 jmp_buf jb1, jb2;
-int i;

 void test() {
+    int i;
     for (i = 1; i <= 10; i++)
         if (setjmp(jb2) == 0)
             longjmp(jb1, i);
  • ローカル変数ですら失われるため、RAIIは論外。
  • 関数からの戻り先も保存されないため、test()の最後はlongjmp()で抜ける。
*** C#

C#で同じものを書いて比較。

class Program
{
    static IEnumerable<int> test()
    {
        for (int i = 1; i <= 10; i++)
            yield return i;
    }

    static void Main(string[] args)
    {
        foreach (var i in test())
            Console.WriteLine(i);
    }
}

自前実装

少しずつ拡張していくために、まずは自前実装してみる。

https://gist.github.com/1257249

#include <stdio.h>

typedef struct {
    unsigned long eip, esp, ebp, ebx, edi, esi;
} myjmp_buf;

_declspec(naked) int mysetjmp(myjmp_buf *jbuf) {
    _asm {
        mov edx, [esp+4]
        mov eax, [esp]
        mov [edx   ], eax
        mov [edx+ 4], esp
        mov [edx+ 8], ebp
        mov [edx+12], ebx
        mov [edx+16], edi
        mov [edx+20], esi
        xor eax, eax
        ret
    }
}

_declspec(naked) void mylongjmp(myjmp_buf *jbuf, int value) {
    _asm {
        mov eax, [esp+ 8]
        mov edx, [esp+ 4]
        mov ecx, [edx   ]
        mov esp, [edx+ 4]
        mov ebp, [edx+ 8]
        mov ebx, [edx+12]
        mov edi, [edx+16]
        mov esi, [edx+20]
        mov [esp], ecx
        ret
    }
}

myjmp_buf jb;

int main() {
    if (mysetjmp(&jb) == 0) {
        printf("setjmp\n");
        printf("test\n");
        mylongjmp(&jb, 1);
        printf("never reach here\n");
    }
    printf("longjmp done.\n");
}

レジスタを構造体に保存しているだけ。

RAIIは無視しているのでg++と同じ結果となる。

チューニング
  • setjmp/longjmpでのスタックの消費を減らすためfastcall化。
  • メモリアクセスを減らすためretをjmpに書き換え。
  • いきなりpopで戻り先を取得してからespを保存しておく。

typedef struct {
    unsigned long eip, esp, ebp, ebx, edi, esi;
} myjmp_buf;

_declspec(naked) int _fastcall mysetjmp(myjmp_buf *jbuf) {
    _asm {
        pop edx
        mov [ecx   ], edx
        mov [ecx+ 4], esp
        mov [ecx+ 8], ebp
        mov [ecx+12], ebx
        mov [ecx+16], edi
        mov [ecx+20], esi
        xor eax, eax
        jmp edx
    }
}

_declspec(naked) void _fastcall mylongjmp(myjmp_buf *jbuf, int value) {
    _asm {
        mov eax, edx
        mov esp, [ecx+ 4]
        mov ebp, [ecx+ 8]
        mov ebx, [ecx+12]
        mov edi, [ecx+16]
        mov esi, [ecx+20]
        jmp dword ptr [ecx]
    }
}

スタック破壊

単純にsetjmp/longjmpで中断した処理を再開すると
スタックが破壊されるためローカル変数や戻り先が不定。

#include <stdio.h>

myjmp_buf jb1, jb2;
int i, *p;

void test() {
    for (i = 1; i <= 10; i++)
        if (mysetjmp(&jb2) == 0) {
            for (p = (int *)jb2.esp; p < (int *)jb1.esp; p++)
                printf("%p: %p\n", p, *p);
            mylongjmp(&jb1, i);
        }
    mylongjmp(&jb1, -1);
}

int main() {
    int value = mysetjmp(&jb1);
    if (value == 0)
        test();
    else if (value > 0) {
        printf("%d\n", value);
        mylongjmp(&jb2, 1);
    }
}

スタックの消費を抑えるためReleaseビルドで確認。


スタックが破壊されている。

スタック退避・復元

復元用バッファを追加。

typedef struct {
    unsigned long eip, esp, ebp, ebx, edi, esi;
    void *stack;
    int length;
} myjmp_buf;

_declspec(naked) int _fastcall mysetjmp(myjmp_buf *jbuf) {
    _asm {
        pop edx
        mov [ecx   ], edx
        mov [ecx+ 4], esp
        mov [ecx+ 8], ebp
        mov [ecx+12], ebx
        mov [ecx+16], edi
        mov [ecx+20], esi
        xor eax, eax
        mov [ecx+24], eax
        mov [ecx+28], eax
        jmp edx
    }
}

_declspec(naked) void _fastcall mylongjmp(myjmp_buf *jbuf, int value) {
    _asm {
        mov eax, edx
        mov edx, ecx
        mov esp, [edx+ 4]
        mov edi, esp
        mov esi, [edx+24]
        mov ecx, [edx+28]
        cld
        rep movsb
        mov ebp, [edx+ 8]
        mov ebx, [edx+12]
        mov edi, [edx+16]
        mov esi, [edx+20]
        jmp dword ptr [edx]
    }
}

スタック退避用関数。

#include <string.h>
#include <vector>

void save_stack(std::vector<char> *dest, unsigned long last, myjmp_buf *callee) {
    callee->length = last - callee->esp;
    dest->resize(callee->length);
    callee->stack = &(*dest)[0];
    memcpy(callee->stack, (void *)callee->esp, callee->length);
}

サンプルを改造。

#include <stdio.h>

myjmp_buf jb1, jb2;
std::vector<char> stack;
int i, *p;

void test() {
    for (i = 1; i <= 10; i++)
        if (mysetjmp(&jb2) == 0) {
            save_stack(&stack, jb1.esp, &jb2);
            for (p = (int *)jb2.esp; p < (int *)jb1.esp; p++)
                printf("%p: %p\n", p, *p);
            mylongjmp(&jb1, i);
        }
    mylongjmp(&jb1, -1);
}

int main() {
    int value = mysetjmp(&jb1);
    if (value == 0)
        test();
    else if (value > 0) {
        printf("%d\n", value);
        mylongjmp(&jb2, 1);
    }
}

スタックが正常に保存されていることを確認。


ローカル変数を使っても大丈夫。

 myjmp_buf jb1, jb2;
 std::vector<char> stack;
-int i, *p;
+int *p;
 
 void test() {
-    for (i = 1; i <= 10; i++)
+    for (int i = 1; i <= 10; i++)
         if (mysetjmp(&jb2) == 0) {
             save_stack(&stack, jb1.esp, &jb2);
             for (p = (int *)jb2.esp; p < (int *)jb1.esp; p++)

実行結果


戻り先が保持されるようになったため、終了通知は不要。
呼び出し元できちんと継続される。

https://gist.github.com/1259601

#include <stdio.h>

myjmp_buf jb1, jb2;
std::vector<char> stack;

void test() {
    for (int i = 1; i <= 10; i++)
        if (mysetjmp(&jb2) == 0) {
            save_stack(&stack, jb1.esp, &jb2);
            mylongjmp(&jb1, i);
        }
}

int main() {
    int value = mysetjmp(&jb1);
    if (value == 0) {
        test();
        printf("end\n");
    } else if (value > 0) {
        printf("%d\n", value);
        mylongjmp(&jb2, 1);
    }
}

実行結果


サポートクラス


バッファを相互に呼び合うのは分かりにくいので簡略化を試みる。

https://gist.github.com/1271852

#include <functional>
#include <vector>

template <class T> class Coroutine {
    myjmp_buf caller, callee;
    std::vector<char> stack;
    std::function<void()> f;
    int status;

public:
    T value;
    Coroutine() : status(0) {}
    Coroutine(const decltype(f) &f) : f(f), status(0) {}
    void operator=(const decltype(f) &f) { this->f = f; }

    bool operator()() {
        if (mysetjmp(&caller)) return true;
        switch (status) {
        case 0:
            status = 1;
            f();
            status = 3;
            break;
        case 2:
            mylongjmp(&callee, 1);
        }
        return false;
    }

    T yield(T value) {
        this->value = value;
        if (mysetjmp(&callee) == 0) {
            status = 2;
            save_stack(&stack, caller.esp, &callee);
            mylongjmp(&caller, 1);
        }
        return this->value;
    }
};

これを使って例を書き直すと以下の通り。

#include <stdio.h>

Coroutine<int> cr;

void test() {
    for (int i = 0; i <= 5; i++)
        cr.yield(i);
}

int main() {
    cr = test;
    while (cr())
        printf("%d\n", cr.value);
    printf("end\n");
}

以前はlongjmpで値を渡していたため0が特別扱いされていたが、
値と状態を分離したたため0も問題なく渡せるようになった。

yield

先ほどの実装は事前にCoroutineのインスタンスを定義して、
yieldがそのインスタンスに縛られている。
これでは格好悪いのでyield()を文脈依存で独立させてみる。

https://gist.github.com/1271857

#include <functional>
#include <vector>
#include <stack>

class CoroutineBase {
public:
    virtual ~CoroutineBase() {}
};

static std::stack<CoroutineBase *> coroutines;

template <class T> class Coroutine : public CoroutineBase {
    myjmp_buf caller, callee;
    std::vector<char> stack;
    std::function<void()> f;
    int status;

public:
    T value;
    Coroutine() : status(0) {}
    Coroutine(const decltype(f) &f) : f(f), status(0) {}
    void operator=(const decltype(f) &f) { this->f = f; }

    bool operator()() {
        if (mysetjmp(&caller)) return true;
        switch (status) {
        case 0:
            status = 1;
            coroutines.push(this);
            f();
            coroutines.pop();
            status = 3;
            break;
        case 2:
            coroutines.push(this);
            mylongjmp(&callee, 1);
        }
        return false;
    }

    T yield(T value) {
        if (coroutines.top() == this) {
            coroutines.pop();
            status = 2;
            this->value = value;
            if (mysetjmp(&callee) == 0) {
                save_stack(&stack, caller.esp, &callee);
                mylongjmp(&caller, 1);
            }
        }
        return this->value;
    }
};

template <class T> T yield(T value) {
    auto cr = dynamic_cast<Coroutine<T> *>(coroutines.top());
    return cr ? cr->yield(value) : T();
}

これを使えば非常にすっきり書ける。

#include <stdio.h>

void test() {
    for (int i = 0; i <= 5; i++)
        yield(i);
}

int main() {
    Coroutine<int> cr = test;
    while (cr())
        printf("%d\n", cr.value);
    printf("end\n");
}

RAII

RAIIも使える。

https://gist.github.com/1258016

#include <stdio.h>

class Test {
    const char *s;
public:
    Test(const char *s) : s(s) {}
    ~Test() { printf("%s\n", s); }
};

void test() {
    Test t("test()");
    for (int i = 0; i <= 5; i++)
        yield(i);
}

int main() {
    Test t("main()");
    Coroutine<int> cr = test;
    while (cr())
        printf("%d\n", cr.value);
    printf("end\n");
}

実行結果


問題点

違う階層から呼ぶと予期しない結果となる。
Debugでは表面化しないのでReleaseでビルド。

https://gist.github.com/1273598

#include <stdio.h>

void test() {
    for (int i = 0; i <= 5; i++)
        yield(i);
    printf("test: done\n");
}

Coroutine<int> cr = test;

void test2() {
    if (cr()) printf("test2: %d\n", cr.value);
}

int main() {
    if (cr()) printf("main: %d\n", cr.value);
    test2();
    if (cr()) printf("main: %d\n", cr.value);
}

呼び出し元のスタックに依存するため、
test2()から呼ばれたコルーチンは正常に動作しない。
  • 図示

ちなみにfを関数ポインタに変更するとDebugビルドでも落ちる。
ただし落ちるタイミングは多少異なる。

 template <class T> class Coroutine : public CoroutineBase {
     myjmp_buf caller, callee;
     std::vector<char> stack;
-    std::function<void()> f;
+    void (*f)();
     int status;
 
 public:

スタックの管理


コルーチンが使用するスタックをそのまま積むのではなく、
特定の領域に取って管理することで問題を解決する。
  • 通常、スタックは1本
  • ファイバーやスレッドでスタックを新設できる
    • 自前実装で原理を探るのが目的のため今回は使用しない

スタックの上限

無限再帰でスタックの限界を探る。

#include <stdio.h>

void test() {
    int a;
    printf("\r&a: %p ", &a);
    fflush(stdout);
    test();
}

int main() {
    int a;
    printf("&a: %p\n", &a);
    test();
    return 0;
}

実行結果は以下の通り。
※環境によって値は異なると思われる。

  • 0x13ff78 - 0x44d4c = 0xfb22c 1004.5KB

スタックの限界が約1MBだということが分かる。
この値はPEヘッダに書いてある。
  • IMAGE_OPTIONAL_HEADER32::SizeOfStackReserve

VC++2010のデフォルト値は0x100000(1MB)。

不完全な修正

スタックをある程度空けてコルーチンを実行する。
空けたスタックの先頭をメンバに残しておく。

     std::vector<char> stack;
     std::function<void()> f;
     int status;
+    unsigned long last;
 
 public:
     T value;
     Coroutine() : status(0) {}
     Coroutine(const decltype(f) &f) : f(f), status(0) {}
     void operator=(const decltype(f) &f) { this->f = f; }
 
     bool operator()() {
+        if (status == 0) _alloca(32 * 1024);
         if (mysetjmp(&caller)) return true;
         switch (status) {
         case 0:
+            last = caller.esp;
             status = 1;
             coroutines.push(this);
             f();

ローカルに巨大な配列を確保しない限り、
32KBもスタックを消費することは少ないと思う。

念のため継続時にスタックを突き破っていないかチェックする。

             status = 3;
             break;
         case 2:
+            if (caller.esp < callee.esp)
+                return false;
             coroutines.push(this);
             mylongjmp(&callee, 1);
         }

空けたスタックを保存すると無駄なので、
メンバに残したスタックのアドレスを使う。

             status = 2;
             this->value = value;
             if (mysetjmp(&callee) == 0) {
-                save_stack(&stack, caller.esp, &callee);
+                save_stack(&stack, last, &callee);
                 mylongjmp(&caller, 1);
             }
         }

問題点として前掲したコードが一見正常に動く。


コルーチン内のループの上限を5から0にすると異常動作する。

https://gist.github.com/1273606

 #include <stdio.h>
 
 void test() {
-    for (int i = 0; i <= 5; i++)
+    for (int i = 0; i <= 0; i++)
         yield(i);
     printf("test: done\n");
 }

コルーチンを抜けるときにスタックが壊れる。


修正

コルーチンを呼んでいる部分を見ると、
終了時はそのまま流れ落ちることを期待している。

case 0:
    status = 1;
    coroutines.push(this);
    f();
    coroutines.pop();
    status = 3;
    break;

別の階層から呼ぶと復帰時のebpが食い違うため、
ローカル変数が正常に参照できずにエラーとなる。
  • 図示

コルーチンをラッパー経由で呼んで終了通知すれば解決。
  • 図示

https://gist.github.com/1271829

     int status;
     unsigned long last;
 
+    void exec() {
+        f();
+        mylongjmp(&caller, 2);
+    }
+
 public:
     T value;
     Coroutine() : status(0) {}
     Coroutine(const decltype(f) &f) : f(f), status(0) {}
     void operator=(const decltype(f) &f) { this->f = f; }
 
     bool operator()() {
         if (status == 0) _alloca(32 * 1024);
-        if (mysetjmp(&caller)) return true;
+        switch (mysetjmp(&caller)) {
+        case 1:
+            return true;
+        case 2:
+            coroutines.pop();
+            status = 3;
+            return false;
+        }
         switch (status) {
         case 0:
             last = caller.esp;
             status = 1;
             coroutines.push(this);
-            f();
-            coroutines.pop();
-            status = 3;
-            break;
+            exec();
         case 2:
             if (caller.esp < callee.esp)
                 return false;

コードの整理


今まで修正も同時に行うため1つのソースに全部書いていた。
修正が落ち着いたのでソースを分割して整理。

https://gist.github.com/1274612

Coroutine.h

#pragma once

#include <vector>
#include <functional>
#include <stack>

typedef struct {
    unsigned long eip, esp, ebp, ebx, edi, esi;
    void *stack;
    int length;
} myjmp_buf;

extern int _fastcall mysetjmp(myjmp_buf *jbuf);
extern void _fastcall mylongjmp(myjmp_buf *jbuf, int value);
extern void save_stack(std::vector<char> *dest, unsigned long last, myjmp_buf *callee);

class CoroutineBase {
public:
    virtual ~CoroutineBase();
};

extern std::stack<CoroutineBase *> coroutines;

template <class T> class Coroutine : public CoroutineBase {
    myjmp_buf caller, callee;
    std::vector<char> stack;
    std::function<void()> f;
    int status;
    unsigned long last;

    void exec() {
        f();
        mylongjmp(&caller, 2);
    }

public:
    T value;
    Coroutine() : status(0) {}
    Coroutine(const decltype(f) &f) : f(f), status(0) {}
    void operator=(const decltype(f) &f) { this->f = f; }

    bool operator()() {
        if (status == 0) _alloca(32 * 1024);
        switch (mysetjmp(&caller)) {
        case 1:
            return true;
        case 2:
            coroutines.pop();
            status = 3;
            return false;
        }
        switch (status) {
        case 0:
            last = caller.esp;
            status = 1;
            coroutines.push(this);
            exec();
        case 2:
            if (caller.esp < callee.esp)
                return false;
            coroutines.push(this);
            mylongjmp(&callee, 1);
        }
        return false;
    }

    T yield(T value) {
        if (coroutines.top() == this) {
            coroutines.pop();
            status = 2;
            this->value = value;
            if (mysetjmp(&callee) == 0) {
                save_stack(&stack, last, &callee);
                mylongjmp(&caller, 1);
            }
        }
        return this->value;
    }
};

template <class T> T yield(T value) {
    auto cr = dynamic_cast<Coroutine<T> *>(coroutines.top());
    return cr ? cr->yield(value) : T();
}

Coroutine.cpp

#include "Coroutine.h"

_declspec(naked) int _fastcall mysetjmp(myjmp_buf *jbuf) {
    _asm {
        pop edx
        mov [ecx   ], edx
        mov [ecx+ 4], esp
        mov [ecx+ 8], ebp
        mov [ecx+12], ebx
        mov [ecx+16], edi
        mov [ecx+20], esi
        xor eax, eax
        mov [ecx+24], eax
        mov [ecx+28], eax
        jmp edx
    }
}

_declspec(naked) void _fastcall mylongjmp(myjmp_buf *jbuf, int value) {
    _asm {
        mov eax, edx
        mov edx, ecx
        mov esp, [edx+ 4]
        mov edi, esp
        mov esi, [edx+24]
        mov ecx, [edx+28]
        cld
        rep movsb
        mov ebp, [edx+ 8]
        mov ebx, [edx+12]
        mov edi, [edx+16]
        mov esi, [edx+20]
        jmp dword ptr [edx]
    }
}

void save_stack(std::vector<char> *dest, unsigned long last, myjmp_buf *callee) {
    callee->length = last - callee->esp;
    dest->resize(callee->length);
    callee->stack = &(*dest)[0];
    memcpy(callee->stack, (void *)callee->esp, callee->length);
}

std::stack<CoroutineBase *> coroutines;

CoroutineBase::~CoroutineBase() {}

サンプル


少し凝った使い方を紹介。

多重コルーチン

型が実行時判定のため、型指定を間違えると動かないので注意。

#include <iostream>
#include <string>
#include "Coroutine.h"

using namespace std;

void test1() {
    yield<string>("abc");
    yield<string>("1234");
    yield<string>("finish!");
}

void test2() {
    Coroutine<string> cr = test1;
    while (cr()) {
        auto s = cr.value;
        cout << s << ": ";
        yield<int>(s.size());
    }
}

int main() {
    Coroutine<int> cr = test2;
    while (cr())
        cout << cr.value << endl;
}


引数

コルーチンの継続時に引数(相当)を渡す方法。

valueを書き換えるとyieldでその値が戻される。
※あらかじめこれができるように考慮した仕様。

#include <iostream>
#include "Coroutine.h"

using namespace std;

void test() {
    int n = 0;
    for (;;) n = yield(++n);
}

int main() {
    Coroutine<int> cr = test;
    if (cr()) cout << cr.value << endl;
    if (cr()) cout << cr.value << endl;
    cr.value = 5;
    if (cr()) cout << cr.value << endl;
}


これ以外の方法をいくつか紹介する。

クロージャ

クロージャでローカル変数をキャプチャ。

#include <iostream>
#include "Coroutine.h"

using namespace std;

int main() {
    int n = 0;
    Coroutine<int> cr = [&] {
        for (;;) yield(++n);
    };
    if (cr()) cout << cr.value << endl;
    if (cr()) cout << cr.value << endl;
    n = 5;
    if (cr()) cout << cr.value << endl;
}


メンバ

メンバ変数を書き換える方法。



関数オブジェクト

関数オブジェクトを渡すとインスタンスが作られて、
引数としてメンバをいじる目的は達成できない。
あまり使い道はないが、ハマる可能性があるため紹介。

ある意味ジェネレータっぽい。
しかし外部から値を操作できないなら、
単純にローカル変数でカウントした方が良い。

#include <iostream>
#include "Coroutine.h"

using namespace std;

int main() {
    struct Test {
        int n;
        Test() : n(0) {}
        void operator()() {
            for (;;) yield(++n);
        }
    } test;
    Coroutine<int> cr1 = test;
    if (cr1()) cout << "cr1: " << cr1.value << endl;
    if (cr1()) cout << "cr1: " << cr1.value << endl;
    Coroutine<int> cr2 = test;
    if (cr2()) cout << "cr2: " << cr2.value << endl;
    if (cr2()) cout << "cr2: " << cr2.value << endl;
    cout << "test.n: " << test.n << endl;
}


まとめ

  • あらかじめローカル用にスタックを取っておいても、
コルーチンからの関数呼び出しでスタックが破壊される恐れがある。
  • そもそもスタックを巻き戻した時点で値の保存を期待してはいけない。
  • ヒープなどにローカルスタックを取っても同様の問題
  • OS機能としてのファイバーを使えば別にスタックが確保できて自動伸張される
    • 下回りがWindowsに依存するため今回はOSファイバーを使わない
  • スレッドでも別の所にスタックが確保されて自動伸張される。
    • スレッドを同期的に呼び出せばファイバーとほぼ等価
    • GUIなどスレッド依存の処理でスレッド間呼び出しが必要になり面倒
  • 巻き戻しを使わないコルーチンが理想的
  • 言語機能として実装できるなら、ローカル変数はメモリ上の任意に取って、
それより下への呼び出しは現行のスタックを消費させるのがスマート。

イベントの直列化


サンクと委譲を導入したWin32に継続を組み合わせる。

イベントドリブンで状態を保持している部分の図示。
一連の流れが分断されている。

☆→{}
☆→{}
☆→{}

イベント間で状態を共有するためフラグの類が必要となる。
パッシブ(プッシュ型)なコード。

これをアクティブ(プル型)な形に変換すると、
状態は分岐やローカル変数に押し込むことができる。

継続はなるべくライブラリ側に隠すと使いやすくなる。

プロジェクトに追加


前回の問題で使用したWin32プログラムを拡張する。

Coroutine.hとCoroutine.cppをプロジェクトに追加。

stdafx.hに追加。

 // TODO: プログラムに必要な追加ヘッダーをここで参照してください。
 #include <list>
 #include <functional>
 #include <string>
 #include "xbyak/xbyak.h"
 #include "Window.h"
+#include "Coroutine.h"

Coroutine.cppではstdafx.hをincludeする。

- #include "Coroutine.h"
+ #include "stdafx.h"

これでCoroutineクラスの追加は完了。

継続を組み込む


ドラッグをコルーチンで監視。

https://gist.github.com/1275118

    int mx, my;
    Coroutine<bool> cr = [&] {
        for (;;) {
            yield(false);
            int x = mx, y = my, px = pb.x, py = pb.y;
            while (yield(true)) {
                pb.x = px + (mx - x);
                pb.y = py + (my - y);
                InvalidateRect(win.hWnd, nullptr, true);
            }
        }
    };
    cr();
    win.MouseDown.push_back([&](int btn, int x, int y, WPARAM) {
        if (pb.x <= x && x <= pb.x + 40 && pb.y <= y && y <= pb.y + 40) {
            mx = x;
            my = y;
            if (!cr.value) cr();
        }
    });
    win.MouseMove.push_back([&](int x, int y, WPARAM) {
        if (cr.value) {
            mx = x;
            my = y;
            cr();
        }
    });
    win.MouseUp.push_back([&](int btn, int x, int y, WPARAM) {
        if (cr.value) {
            cr.value = false;
            cr();
        }
    });

ドラッグの一般化


もう少しコードを整理する。

https://gist.github.com/1275258

Coroutineクラスを拡張して巻き戻せるようにする。

     Coroutine() : status(0) {}
     Coroutine(const decltype(f) &f) : f(f), status(0) {}
     void operator=(const decltype(f) &f) { this->f = f; }
+    void reset() { if (status == 3) status = 0; }
 
     bool operator()() {
         if (status == 0) _alloca(32 * 1024);

ハンドラとの協調動作を抽出してクラス化する。
巻き戻しを利用して無限ループを排除している。

class DragHandler {
    Coroutine<bool> cr;
    std::function<void()> handler;

public:
    int x, y;

    DragHandler(Window *win) {
        win->MouseMove.push_back([&](int x, int y, WPARAM) {
            if (cr.value) { this->x = x; this->y = y; cr(); }
        });
        win->MouseUp.push_back([&](int button, int x, int y, WPARAM) {
            if (cr.value) { cr.value = false; cr(); }
        });
    }

    void operator=(const decltype(handler) &h) {
        cr = handler = h;
        cr.value = false;
    }

    void start(int x, int y) {
        this->x = x;
        this->y = y;
        if (!cr.value) { cr.reset(); cr(); }
    }
};

これを使ってハンドラを書き直す。

    DragHandler dh(&win);
    win.MouseDown.push_back([&](int btn, int x, int y, WPARAM) {
        if (pb.x <= x && x <= pb.x + 40 && pb.y <= y && y <= pb.y + 40)
            dh.start(x, y);
    });
    dh = [&] {
        int x = dh.x, y = dh.y, px = pb.x, py = pb.y;
        while (yield(true)) {
            pb.x = px + (dh.x - x);
            pb.y = py + (dh.y - y);
            InvalidateRect(win.hWnd, nullptr, true);
        }
    };

サンク・委譲・継続を使ったイベントハンドリングが完成。
これが今回提示したかったモデル。

足回りから作っていたので結構長い道のりだったけど、
できたものを利用するだけならシンプルだと思う。

複数項目


複数の項目を扱えるようにしてみる。

https://gist.github.com/1275467

    struct Rect {
        int x, y, w, h;
        inline int r() { return x + w; }
        inline int b() { return y + h; }
        Rect(int x, int y, int w, int h): x(x), y(y), w(w), h(h) {}
        bool contains(int px, int py) {
            return x <= px && px < r() && y <= py && py < b();
        }
    };
    std::vector<Rect> rects;
    rects.push_back(Rect(10, 10, 40, 40));
    rects.push_back(Rect(60, 60, 40, 40));
    win.Paint.push_back([&](HDC hdc) {
        auto oldPen = (HPEN)SelectObject(hdc, GetStockObject(BLACK_PEN));
        auto oldBrush = (HBRUSH)SelectObject(hdc, GetStockObject(GRAY_BRUSH));
        for (auto it = rects.begin(); it != rects.end(); it++)
            Rectangle(hdc, it->x, it->y, it->r(), it->b());
        SelectObject(hdc, oldPen);
        SelectObject(hdc, oldBrush);
    });

    DragHandler dh(&win);
    decltype(rects.rbegin()) sel;
    win.MouseDown.push_back([&](int btn, int x, int y, WPARAM) {
        for (auto it = rects.rbegin(); it != rects.rend(); it++) {
            if (it->contains(x, y)) {
                sel = it;
                dh.start(x, y);
                break;
            }
        }
    });
    dh = [&] {
        int x = dh.x, y = dh.y, rx = sel->x, ry = sel->y;
        while (yield(true)) {
            sel->x = rx + (dh.x - x);
            sel->y = ry + (dh.y - y);
            InvalidateRect(win.hWnd, nullptr, true);
        }
    };

ちらつきが激しいが、対策にはダブルバッファが必要。
今回はサンクと継続がメインなので説明は見送る。

問題


[問] 図形の右下をつまむとリサイズできるようにしてみてください。
回答例

    DragHandler dh(&win), dh2(&win);
    decltype(rects.rbegin()) sel;
    win.MouseDown.push_back([&](int btn, int x, int y, WPARAM) {
        for (auto it = rects.rbegin(); it != rects.rend(); it++) {
            if (it->contains(x, y)) {
                sel = it;
                if (it->r() - x <= 5 && it->b() - y <= 5)
                    dh2.start(x, y);
                else
                    dh.start(x, y);
                break;
            }
        }
    });
    dh = [&] {
        int x = dh.x, y = dh.y, rx = sel->x, ry = sel->y;
        while (yield(true)) {
            sel->x = rx + (dh.x - x);
            sel->y = ry + (dh.y - y);
            InvalidateRect(win.hWnd, nullptr, true);
        }
    };
    dh2 = [&] {
        int x = dh2.x, y = dh2.y, rw = sel->w, rh = sel->h;
        while (yield(true)) {
            sel->w = rw + (dh2.x - x);
            sel->h = rh + (dh2.y - y);
            InvalidateRect(win.hWnd, nullptr, true);
        }
    };

終わりに


今回掲載したサンプルコードはすべてCC0で提供します。

今回やったような内容を勉強するには、
既存のコンパイラの出力を盗み見しながら、
自分でコンパイラを作ってみるのが良いと思います。

参考資料


サンク

  • WTL/ATLのメッセージマップ実現のしくみ
http://hp.vector.co.jp/authors/VA022575/c/msgmap.h...

コルーチン
  • libconcurrent
http://code.google.com/p/libconcurrent/wiki/Projec...
    • @sharowさんによるコルーチンライブラリ。各種実装のリンク集がある。
  • [C#][programming]C# の yield return の使い道
http://d.hatena.ne.jp/u_1roh/20080302/1204471238
    • イベントハンドラとコルーチンに関する議論

Wiki内検索

Menu

ここは自由に編集できるエリアです。

管理人/副管理人のみ編集できます