4.5 – Cでのyield処理

内部的に、LuaはコルーチンをyieldするためにCのlongjmp機能を使います。そのため、C関数fooがAPI関数を呼び出し、そのAPI関数がyieldすると(直接、またはyieldする別の関数を呼び出すことで間接的に)、longjmpがCスタックからそのフレームを除去するため、Luaはfooへ戻れなくなります。

この種の問題を避けるため、LuaはAPI呼び出しをまたいでyieldしようとするとエラーを発生させます。ただし、lua_yieldklua_callklua_pcallkの3関数は例外です。これらの関数はすべて、yield後に実行を継続する継続関数を(kという名前の引数として)受け取ります。

継続を説明するため、いくつかの用語を定めます。Luaから呼び出されるC関数があり、これを元の関数と呼びます。この元の関数は、次にC APIの上記3関数の1つを呼び出します。これを呼び出された関数と呼び、これは現在のスレッドをyieldします。呼び出された関数がlua_yieldkである場合、または呼び出された関数がlua_callklua_pcallkで、それらが呼び出した関数がyieldする場合に、この状況が起こります。

実行中のスレッドが、呼び出された関数の実行中にyieldしたとします。スレッドをresumeすると、最終的には呼び出された関数の実行を完了します。しかし、yieldによってCスタック内のフレームが破棄されたため、呼び出された関数は元の関数へ戻れません。代わりに、Luaは呼び出された関数への引数として指定された継続関数を呼び出します。その名前が示すとおり、継続関数は元の関数の処理を継続する必要があります。

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

     int original_function (lua_State *L) {
       ...     /* code 1 */
       status = lua_pcall(L, n, m, h);  /* calls Lua */
       ...     /* code 2 */
     }

ここで、lua_pcallが実行するLuaコードに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が実行するLuaコードが何らかの形(エラーまたは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が継続を呼び出すのは、エラーの場合、またはyield後にresumeする場合など、必要なときだけです。呼び出した関数が一度もyieldせず正常に返ると、lua_pcallk(およびlua_callk)も正常に返ります。(もちろん、この場合に継続を呼び出す代わりに、元の関数内で等価な処理を直接行っても構いません。)

Luaステートに加え、継続関数は2つの引数、すなわち呼び出しの最終ステータスと、元々lua_pcallkへ渡されたコンテキスト値(ctx)を持ちます。Luaはこのコンテキスト値を使用せず、元の関数から継続関数へ渡すだけです。lua_pcallkでは、ステータスはlua_pcallkが返すはずの値と同じです。ただし、yield後に実行された場合はLUA_OKではなくLUA_YIELDになります。lua_yieldklua_callkでは、Luaが継続を呼び出すときのステータスは常にLUA_YIELDです。(これら2つの関数はエラーを処理しないため、エラー時にLuaが継続を呼び出すことはありません。)同様に、lua_callkを使う場合は、ステータスをLUA_OKとして継続関数を呼び出すべきです。(lua_yieldkでは、lua_yieldkが通常返らないため、継続関数を直接呼び出すことに大きな意味はありません。)

Luaは継続関数を元の関数であるかのように扱います。継続関数は、呼び出された関数が返った場合と同じ状態にある、元の関数と同じLuaスタックを受け取ります。(たとえば、lua_callkの後では、関数とその引数がスタックから除去され、呼び出しの結果に置き換えられます。)また、同じ上位値を持ちます。継続関数が返すものは、元の関数からの戻り値であるかのようにLuaによって処理されます。