Luaソースコード勉強会 (4)
今回は、割と言語のコアな部分だけれど(VMじゃなくて)ライブラリ実装になってる部分を拾っておきます。
コルーチン
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
シンプル!
環境
ここまでグローバル変数の扱いすっごく適当に見てたんですけど、
Foo = {x = 100} (function () setfenv(1, Foo) y = 2 z = x + y end)() print(Foo.x) -- 100 print(Foo.y) -- 2 print(Foo.z) -- 102
setfenv/getfenvというので、グローバル変数の参照するテーブルを差し替えられるんですね。モジュールを実装するのによく使われるみたいです。
さりげなくスルーしてしまったGETGLOBAL, SETGLOBAL命令の実装を見直してみると、こんな感じです。cl->envというテーブルに、値を読み書きしています。
case OP_GETGLOBAL: { TValue g; TValue *rb = KBx(i); sethvalue(L, &g, cl->env); lua_assert(ttisstring(rb)); Protect(luaV_gettable(L, &g, rb, ra)); continue; } case OP_SETGLOBAL: { TValue g; sethvalue(L, &g, cl->env); lua_assert(ttisstring(KBx(i))); Protect(luaV_settable(L, &g, KBx(i), ra)); continue; }
というわけで、getfenv/setfenvはcl->envを操作しているはず。cl->は現在実行中の関数オブジェクトなので、つまり関数ごとに環境を設定できるわけですね。
// lbaselib.c static int luaB_getfenv (lua_State *L) { getfunc(L, 1); if (lua_iscfunction(L, -1)) /* is a C function? */ lua_pushvalue(L, LUA_GLOBALSINDEX); /* return the thread's global env. */ else lua_getfenv(L, -1); return 1; } // lbaselib.c static void getfunc (lua_State *L, int opt) { if (lua_isfunction(L, 1)) lua_pushvalue(L, 1); else { lua_Debug ar; int level = opt ? luaL_optint(L, 1, 1) : luaL_checkint(L, 1); luaL_argcheck(L, level >= 0, 1, "level must be non-negative"); if (lua_getstack(L, level, &ar) == 0) luaL_argerror(L, 1, "invalid level"); lua_getinfo(L, "f", &ar); if (lua_isnil(L, -1)) luaL_error(L, "no function environment for tail call at level %d", level); } }
getfenvでは、関数オブジェクトそのものを指定するか、コールスタックの深さを整数で指定します。関数オブジェクトが直接来たときはいいとして、整数だった場合は、lua_getstackでコールスタックをたどって関数を取得しています。
LUA_API void lua_getfenv (lua_State *L, int idx) { ... switch (ttype(o)) { case LUA_TFUNCTION: sethvalue(L, L->top, clvalue(o)->c.env); break; ... case LUA_TTHREAD: setobj2s(L, L->top, gt(thvalue(o))); break; default: setnilvalue(L->top); break; } ... }
取得した関数オブジェクトのenvを返す感じで。あ、コルーチンを指定してコルーチンのグローバル環境を変えることも出来るんですね。あれ、そうでもないかも。調べ中。
setfenvも基本的に同じで、getfuncで対象の関数オブジェクトをとってきて、lua_setfenv
static int luaB_setfenv (lua_State *L) { luaL_checktype(L, 2, LUA_TTABLE); getfunc(L, 0); lua_pushvalue(L, 2); if (lua_isnumber(L, 1) && lua_tonumber(L, 1) == 0) { /* change environment of current thread */ lua_pushthread(L); lua_insert(L, -2); lua_setfenv(L, -2); return 0; } ... }
lua_setfenvはこう、envにテーブルを設定。
LUA_API int lua_setfenv (lua_State *L, int idx) { ... case LUA_TFUNCTION: clvalue(o)->c.env = hvalue(L->top - 1); break;
というわけでした。基本ライブラリのmodule関数もVMの言語実装部分的な仕組みとしてはgetfenvとsetfenvと同じように実装できそうです。
おまけ:LuLu (4)
コルーチンと getfenv/setfenv 実装しました。あと、今回はインチキして loadfile 関数でホストのLuaにコンパイル処理をやってもらうことにしたので、luac の出力ではなく、直接 .lua のソースも実行できるようになっています。
lua lulu.lua /usr/local/lua/test/sieve.lua
みたいな感じで、Luaについてくるテストプログラム動かせます。かっこいい!
今残っている Lua 5.1 と比べて足りてない部分、変な部分は
- 未実装:メタテーブル
- 未実装:debugモジュール
- 未実装:module/require/package
- 手抜き実装:pcall/cpcall
- 手抜き実装:dofile/load/loadfile/loadstring/string.dump
- 手抜き実装:string.dump
- 手抜き実装:function と thread が table として使えちゃうバグ(内部実装が漏れ出している)。tostringしたときにもtable 扱いされる
となっています。他のライブラリ(io, string, math)は、ホストLuaのライブラリに直接投げるという手抜き処理ですが、それによって特に問題は起きないはず。pcallやdofileは、それだとあんまり上手くいかないのでどうにかしないといけません。
次回は、この辺りなんとかしたりYueliangとくっつけたりするかもしないかも。The LuaJIT Project 勉強会に移行するかもしないかも。ではまたー。