継続マラソン - PCL / Io のコルーチンの実装を見てみる

id:shinichiro_hさんからコメントを頂きました。ありがとうございます。

 『たぶん実装は似たものになると思うのですが、コルーチンですと Ruby や Io なんかから実装ひっこ抜いてきたらそれなりに動いた経験があります。
あと PCL も参考になるかなと思いますが。Scheme の継続をわかってないので外していたらすいません。』

確かに coroutine の実装は

  • context save
  • context restore

が共通しているのでとても参考になりそうです。(それを言ったら OS 側での context restore もほぼ同じだけど、ユーザー側でどう工夫しているかは知りたいところでもあります。)


共通しないところとしては、Scheme 継続の場合 longjmp とは逆方向への jmp が必要なので、スタックの一部を save/restore しなければいけない点でしょうか。
しかもスタックの一部に積まれているデータを書き換えてあげる必要がある予感。(スタックに確保されたバッファのアドレスを引数に渡した場合など)


早速実装を見て行こうと思います。

Ruby

Ruby でのコルーチン周りの実装は見つけられませんでした。残念。

PCL

Portable Coroutine Library


なるほど。
ポータブルらしいのでコードを見てみよう。

  • getcontext/swapcontext 、 setjmp/longjmp のどちらかを使うか選べる。
  • 各 coroutine の stack を heap 上に確保して setcontext する
static int co_set_context(co_ctx_t *ctx, void *func, char *stkbase, long stksiz) {
    char *stack;

    stack = stkbase + stksiz - sizeof(long);

    setjmp(ctx->cc);

#if defined(__GLIBC__) && defined(__GLIBC_MINOR__) \
    && __GLIBC__ >= 2 && __GLIBC_MINOR__ >= 0 && defined(JB_PC) && defined(JB_SP)
    ctx->cc[0].__jmpbuf[JB_PC] = (int) func;
    ctx->cc[0].__jmpbuf[JB_SP] = (int) stack;

ふむふむ。setjmp したあとにjmpbuf の eip と esp を上書きしている。
確かにそれで十分だ。
stack はヒープに確保したものでも良いっぽい。
ところで、Mona OS 以外の実装を知らないのだけど、heap 領域のメモリアドレスをスタックポインタに指定して何も問題ないのかな。(Mona OS では何もチェックしていないので問題ない)

Io

最初 Lo と読んでいてググっても出てこないなと思ったのは秘密。


libs/coroutine/source/Coro.c
Coro と書くとかわいく見えるから不思議。Coro かわいいよ Coro。
きれいにライブラリが分かれているのが好感。

self->env[0].__jmpbuf[6] = ((unsigned long)(Coro_stack(self));
self->env[0].__jmpbuf[7] = ((long)Coro_Start);

うむ。結局 setjmp/longjmp 。

/* since ucontext seems to be broken on amg64 */
setjmp(self->env);

というコメントがおもろい。

  • valgrind のヘッダを include する場合もあるようだ。

まとめ

コルーチンライブラリたちは、とてもコンパクト。
アーキテクチャに対応する場合に参考になりそう。

スタックのコピーまで引き受けるようなライブラリを作って公開したら需要はあるかな?。