Boost.Coroutine を組み込み環境向けに移植してみた

ある程度動いてる感じがするので公開してみます。
BREW で人柱になってくれる方を募集中です。

ソースとか

いつもの場所に置いときました。

移植にあたって

Boost.Coroutine の実装は膨大すぎて途方にくれてしまったので、コンパクトで移植のやりやすそうな id:y-hamigaki さんの Hamigaki.Coroutine をごちゃごちゃ弄って移植させて頂きました。

本家 Boost との違い

  • coroutine の move semantics は実装していません。
    • どうしても所有権を移動したい場合は move_to と move_from を用意しておきましたので、こちらを使用してください。
    • 関数の戻り値にするのは諦めて下さい。もしくは shared_coroutine を使ってください。
  • coroutine と shared_coroutine を相互に変換することはできません。
  • abnormal_exit から type_info を取得することはできません。
  • future は実装していません。
  • tuple をちゃんと実装していないので、tuple を使う部分は少しだけ制限があります。例えば Boost では以下のように書けるコードを、
namespace coro = boost::coroutines;

int func(coro::coroutine<int (int, int)>::self& self, int n, int m)
{
    int s = 0;
    while (true) boost::tie(n, m) = self.yield(s++ * (n + m));
}

移植版では以下のように書く必要があります。

namespace coro = moost::coroutines;

int func(coro::coroutine<int (int, int)>::self& self, int n, int m)
{
    int s = 0;
    coro::coroutine<int (int, int)>::arg_type arg = coro::make_tuple(n, m);
    while (true) arg = self.yield(s++ * (arg.get<0>() + arg.get<1>()));
}
  • 多分他にもいっぱいあると思うのですが、基本的な使い方は出来てるっぽいので大丈夫でしょう。

使い方

Boost.Coroutine のドキュメントHamigaki.Coroutine のドキュメントに十分書かれてるので特に書くことは無かったり・・・。

ARM のコンテキスト切り替え処理

独自に追加した部分なので、ここだけ解説しておこうと思います。

template<typename T>
inline void trampoline(void* pv)
{
    T* fun = static_cast<T*>(pv);
    (*fun)();
}

typedef void* command;

__asm void asm_swap_context(command** from, const command* to)
{
    PUSH            {r0-r12,lr}

    STR             sp,[r0]
    MOV             sp,r1

    POP             {r0-r12,lr}

    MOV             pc,lr
}

...

class arm_context_impl : public arm_context_impl_base, private noncopyable {
public:
    ...

    template<class Functor>
    arm_context_impl(Functor& cb, std::ptrdiff_t stack_size = -1)
        : stack_(new command[get_command_num(stack_size)])
    {
        sp_ = stack_ + get_command_num(stack_size);
        *--sp_ = (void*)&trampoline<Functor>;   // lr
        *--sp_ = 0;                     // r12
        *--sp_ = 0;                     // r11
        *--sp_ = 0;                     // r10
        *--sp_ = 0;                     // r9
        *--sp_ = 0;                     // r8
        *--sp_ = 0;                     // r7
        *--sp_ = 0;                     // r6
        *--sp_ = 0;                     // r5
        *--sp_ = 0;                     // r4
        *--sp_ = 0;                     // r3
        *--sp_ = 0;                     // r2
        *--sp_ = 0;                     // r1
        *--sp_ = &cb;                   // r0
    }

    ...
};

asm_swap_context で実際にコンテキスト(スタック)の切り替え(わずか5命令!)を行っています。
PUSH でレジスタをスタックに退避して、STR で切り替え前のスタックポインタを *from に書き込んで、MOV でスタックポインタを to に書き換えて、POP で新しいスタックからレジスタを復帰させて、MOV でプログラムカウンタを lr の位置に変更して戻すだけ。
初回呼び出しは切り替え先のスタックに退避させるレジスタなんてのは存在していないのですが、スタックポインタの初期化時に設定してやっています。lr に &trampoline が、r0 に &cb が渡るように設定すると、初回呼び出し時には最後の MOV 命令で &trampoline にジャンプします。trampoline の第一引数にはコルーチン側のスタートアップ関数(&cb)が渡ってきているので、それを呼び出すようになっています。


要するに Case study 2: Linux-x86-GCC の実装を ARM で書いただけです。

今後欲しい機能

今はスタック領域の確保に動的なアロケートを行っているのですが、これをユーザが指定できるようにできたらいいなーとか考えています。
ただ、Windows の場合は Fiber を使っているためそういったことは不可能なので、ここをうまく分ける方法を検討する必要がありそうです。