ひとり勉強会

ひとり楽しく勉強会

環境

ここまでグローバル変数の扱いすっごく適当に見てたんですけど、

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と同じように実装できそうです。