ひとり勉強会

ひとり楽しく勉強会

コルーチン

Luaには「coroutine(コルーチン)」という機能があって、Lua 5.0のVMのデザインもこれを楽に実装するために採用された面もあるらしいので、VMを学ぶにあたって重要なポイントです。こんな機能。

function countUp()
  local n = 0
  while true do
    coroutine.yield(n) -- 対応するcoroutine.resumeにnを返す
    n = n+1
  end
end

co = coroutine.create(countUp)
repeat
  f,a = coroutine.resume(co) -- countUp実行開始、あるいはyieldから実行再開
  print(a)
until a>=10

他の言語でもよくある「スレッド」に似ていますが(実際Luaではコルーチンオブジェクトの型は"thread"だったりします)、coroutine.resume と coroutine.yield を明示的に呼んだ時だけ実行コンテキストが切り替わるのがポイントです。あと、Luaのコルーチンの特徴として、resumeは移る先のコルーチンを指定できますが、yieldではresume元に「戻る」ことしかできないのもポイント。コルーチン達は対称な関係ではなくて、親コルーチンが子コルーチンを「呼び出し」て、子コルーチンは親に「returnする」という「非対称コルーチン」になってるそうです。

コルーチンの実装も、この関係を反映した感じになっています。luaV_execute でVM実行中にcoroutine.resumeを呼び出すと、別のluaV_execute関数を「呼び出し」て、coroutine.yieldで、luaV_executeからreturnして親に戻ります。Luaレベルだとコルーチンという特別な存在ですが、VMレベルだとほぼ普通の関数呼び出しになっちゃってる感じです。

lbaselib.c

第1回あたりで触れたように、コルーチン(thread)を表すデータ構造は、lua_State です。

// lstate.h
struct lua_State {
  ...
  StkId top;  /* first free slot in the stack */
  StkId base;  /* base of current function */
  ...
  CallInfo *ci;  /* call info for current function */
  const Instruction *savedpc;  /* `savedpc' of current function */
  ...
};

データスタックや、呼び出し情報スタック、PC(プログラムカウンタ)などなどが入ってます。

coroutine.* の関数の実装は、他の基本ライブラリと一緒にlbaselib.cに収まってます。

coroutine.create(fun)
// lbaselib.c
static int luaB_cocreate (lua_State *L) {
  lua_State *NL = lua_newthread(L);
  ...
  lua_pushvalue(L, 1);  /* move function to top */
  lua_xmove(L, NL, 1);  /* move function from L to NL */
  return 1;
}

// lapi.c
LUA_API lua_State *lua_newthread (lua_State *L) {
  ...
  L1 = luaE_newthread(L);
  setthvalue(L, L->top, L1);
  ...
}

lua_newthread関数で新しいlua_Stateオブジェクトを作って、呼び出し側スレッドのtopに置きます(coroutine.createの返り値ですね)。新しく作ったスレッドでは、coroutine.createに渡された引数(関数オブジェクトなはず)をスタックに1個だけ置いておきます。

coroutine.resume (前半)
// lbaselib.c
static int luaB_coresume (lua_State *L) {
  lua_State *co = lua_tothread(L, 1);
  ...
  r = auxresume(L, co, lua_gettop(L) - 1);
  ...

luaB_coresume処理の本体はauxresume関数みたいです。

// lbaselib.c
static int auxresume (lua_State *L, lua_State *co, int narg) {
  ...
  lua_xmove(L, co, narg);
  ...
  status = lua_resume(co, narg);
  ...

lua_xmoveで、coroutine.resumeに渡された引数を呼ばれるコルーチンの方にコピーしてます。続きはlua_resumeで。

// ldo.c
LUA_API int lua_resume (lua_State *L, int nargs) {
  ...
  status = luaD_rawrunprotected(L, resume, L->top - nargs);
  ...
}

そしてresume関数へ。

// ldo.c
static void resume (lua_State *L, void *ud) {
  StkId firstArg = cast(StkId, ud);
  CallInfo *ci = L->ci;
  if (L->status == 0) {  /* start coroutine? */
    lua_assert(ci == L->base_ci && firstArg > L->base);
    if (luaD_precall(L, firstArg - 1, LUA_MULTRET) != PCRLUA)
      return;
  }

coroutine.createで作ったばっかりのコルーチンのスタックには、関数オブジェクトが1個乗っているだけでした。そこにauxresumeで引数を積んだので、ここで、luaD_precall(OP_CALLやOP_TAILCALLの実装に使われてた補助関数)を呼ぶと、引数の個数や可変長引数などなどの準備が整って、関数の中身の実行開始準備完了状態になります。
コルーチン開始

  else {  /* resuming from previous yield */
    lua_assert(L->status == LUA_YIELD);
    L->status = 0;
    if (!f_isLua(ci)) {  /* `common' yield? */
      /* finish interrupted execution of `OP_CALL' */
      lua_assert(GET_OPCODE(*((ci-1)->savedpc - 1)) == OP_CALL ||
                 GET_OPCODE(*((ci-1)->savedpc - 1)) == OP_TAILCALL);
      if (luaD_poscall(L, firstArg))  /* complete it... */
        L->top = L->ci->top;  /* and correct top if not multiple results */
    }
    else  /* yielded inside a hook: just continue its execution */
      L->base = L->ci->base;
  }

coroutine.yieldから実行再開する場合は、coroutine.resumeに渡された引数は、逆に、coroutine.yield関数の「返値」になります。なのでここは、OP_RETURNの補助関数luaD_poscallで、多値の場合の処理などをうまいことやってやります。f_isLuaはC関数からyieldした場合かな?ここは省略。
で、最後に、予告通りluaV_executeでVMのメインループに突入します。

  luaV_execute(L, cast_int(L->ci - L->base_ci));
}

どん。

coroutine.yield

この luaV_execute の実行中に coroutine.yield が呼ばれた場合の処理は、親に値と制御を戻すこと。

// lbaselib.c
static int luaB_yield (lua_State *L) {
  return lua_yield(L, lua_gettop(L));
}

lua_yieldへ。

// ldo.c
LUA_API int lua_yield (lua_State *L, int nresults) {
  ...
  if (L->nCcalls > L->baseCcalls)
    luaG_runerror(L, "attempt to yield across metamethod/C-call boundary");
  L->base = L->top - nresults;  /* protect stack slots below */
  L->status = LUA_YIELD;
  ...
  return -1;
}

一番親のスレッドでcoroutine.yieldしようとするとエラーになるようになってます。statusをLUA_YIELDに変えて、return -1。この-1が、「yieldした」ことを意味する値です。実際、luaV_executeの値はこんな風になっていて

// lvm.c
void luaV_execute (lua_State *L, int nexeccalls) {
  ...
  for (;;) {
    ...
    switch (GET_OPCODE(i)) {
      ...
      case OP_CALL: {
        ...
        switch (luaD_precall(L, ra, nresults)) {
          case PCRLUA: { ... }
          case PCRC: { ... }
          default: {
            return;  /* yield */
          }

関数の呼び出し(luaD_precall)でPCRLUA/PCRC以外の値が返るとluaV_executeを抜けるようになっていて、

// ldo.c
int luaD_precall (lua_State *L, StkId func, int nresults) {
  ...
  else {  /* if is a C function, call it */
    ...
    n = (*curr_func(L)->c.f)(L);  /* do the actual call */
    if (n < 0)  /* yielding? */
      return PCRYIELD;
    ...
  }

luaD_precallの方は、呼んだ関数が負の値を返すとPCRYIELDを返す、と。
lua_State *L に現在のスタックの状態やPCは全て保存されているはずなので、次にluaV_executeしたときにはこの次から実行再開できます。

coroutine.resume (後半)

coroutine.yieldが呼ばれるとluaV_execute→resume→lua_resume→auxresume、とガンガンreturnしてきて、

// lbaselib.c :: auxresume(後半)
  if (status == 0 || status == LUA_YIELD) {
    int nres = lua_gettop(co);
    if (!lua_checkstack(L, nres))
      luaL_error(L, "too many results to resume");
    lua_xmove(co, L, nres);  /* move yielded values */
    return nres;
  }
  else {
    lua_xmove(co, L, 1);  /* move error message */
    return -1;  /* error flag */
  }

auxresumeで、yieldに渡された引数をauxresumeの返り値として、呼び出し元のスタックにコピーしてます。そしてさらにもう一段returnしたluaB_coresumeで、

// lbaselib.c :: luaB_coresume(後半)
  r = auxresume(L, co, lua_gettop(L) - 1);
  if (r < 0) {
    lua_pushboolean(L, 0);
    lua_insert(L, -2);
    return 2;  /* return false + error message */
  }
  else {
    lua_pushboolean(L, 1);
    lua_insert(L, -(r + 1));
    return r + 1;  /* return true + `resume' returns */
  }

正常にyieldされた場合はtrue、そうでない場合はfalseとエラーメッセージをスタックに置き直す、という処理を行っています。これであとは普通に呼び出し側コルーチンの実行ループに戻ります。

まとめ
  • luaV_execute = ひとつひとつのコルーチンの実行ループ
  • coroutine.resume = luaV_execute呼び出し
  • coroutine.yield = luaV_executeからreturn

シンプル!