環境
ここまでグローバル変数の扱いすっごく適当に見てたんですけど、
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と同じように実装できそうです。