4.5 C からの yield

Lua は内部で longjmp を使ってコルーチンの yield を処理します。そのため、もし Lua が呼び出した C 関数 foo が別の API 関数を呼び、その API 関数 (もしくはその関数が呼ぶ他の関数) が yield した場合、Lua は foo に戻る方法を失います。yield に伴う longjmpfoo のフレームを C スタックから消去するためです。

この種の問題に対処するために、API 呼び出しを跨いで yield すると Lua はエラーを送出するようになっています。ただし三つの API 関数 lua_yieldk, lua_callk, lua_pcallk は例外です。これらの関数は yield の後にも実行を続けるために継続関数 (continuation function) をパラメータ k に受け取ります。

継続を説明するには用語の定義が必要です。Lua が呼び出す C 関数を元の関数と呼ぶことにします。この元の関数が前段落で示した三つの関数のどれかを C API で呼び出したとして、呼び出された関数を呼び出し先の関数とします。呼び出し先の関数が現在のスレッドを yield した状況を考えます。この状況が起こるのは呼び出し先の関数が lua_yieldk のとき、あるいは呼び出し先の関数が lua_callk または lua_pcallk でそれが呼び出す関数が yield したときです。

実行中のスレッドが呼び出し先の関数の実行中に yield したとします。その後スレッドが resume するといずれ呼び出し先の関数が終了しますが、このとき呼び出し先の関数は元の関数に戻ることができません。というのも yield が C スタックを破壊するからです。そこで Lua は引数として渡される継続関数を呼び出します。名前が示すように、継続関数は元の関数の処理を再開します。

例として次の関数を考えます。

int original_function (lua_State *L) {
  ...     /* code 1 */
  status = lua_pcall(L, n, m, h);  /* Lua 関数を呼ぶ */
  ...     /* code 2 */
}

ここで lua_pcall が呼び出す関数からの yield を許したいとします。このときにはまず関数を次のように書き換えます:

int k (lua_State *L, int status, lua_KContext ctx) {
  ...  /* code 2 */
}

int original_function (lua_State *L) {
  ...     /* code 1 */
  return k(L, lua_pcall(L, n, m, h), ctx);
}

上のコードにおける k継続関数であり、その型は lua_KFunction です。この関数は lua_pcall の後の処理を全て行います。続いて lua_pcall が実行するコードが (エラーや yield で) 中断されたときには k を実行しなければならないことを Lua に伝えます。lua_pcalllua_pcallk に書き換えるとこれを行えます:

int original_function (lua_State *L) {
  ...     /* code 1 */
  return k(L, lua_pcallk(L, n, m, h, ctx2, k), ctx1);
}

継続関数を lua_pcallk の外側でも明示的に呼び出していることに注意してください。Lua は yield 後の resume やエラーで継続が必要になった限って継続関数を呼び出すので、関数が yield することなく通常終了した場合には lua_pcallk (および lua_callk) は通常通り値を返します。もちろん、継続関数を呼び出す代わりにその処理を元の関数で直接行うこともできます。

継続関数は Lua ステート以外に二つの引数を受け取ります: 状態コードと、lua_pcallk に渡されたコンテキスト (ctx) です。Lua はコンテキストを利用せず、ただ継続関数に渡すだけです。lua_pcallk における状態コードは lua_pcallk が返すはずの値ですが、yield した場合には LUA_OK ではなく LUA_YIELD となります。つまり lua_yieldklua_callk で継続が呼ばれた場合には必ず状態コードが LUA_YIELD となります (この二つの関数はエラーを処理しないので、Lua はエラー時に継続を呼びません)。これと合わせて、lua_callk を使うときは LUA_OK を状態コードとして継続関数を呼び出すべきです。また lua_yieldk は通常返らないので、この関数を使うときに継続関数を直接呼び出す意味はありません。

Lua は継続関数を元の関数であるかのように扱います。継続関数が受け取る Lua スタックは元の関数で呼び出し先関数が返ったときに得られるであろうスタックです (例えば lua_callk の後には関数と引数がスタックから取り除かれ、返り値で置き換わります)。またアップバリューも同様です。Lua は継続関数の返り値を元の関数の返り値であるかのように扱います。