lua metatable使用和源码分析(一)

lua metatable(以下简称元表)类似c++的operator overloads,可以对复合结构进行操作,在lua里最常见的就是对表的操作.举例来说,当两个表作加法操作的时候,Lua会检查表的元表中是否有”__add”事件是否对应一个函数(metamethod)。如果存在Lua会调用这个函数来执行一次表的加法操作.

__index和__newindex是表常常要添加的事件,用于处理键值在表无法被查找到之后的处理.

hoterran@~/Projects/lua$ cat meta_test.lua
t = {}
t.a = 1

print(t.a)
print(t.b)
setmetatable(t, {__index = function(x) return "test" end})
print(t.b)

hoterran@~/Projects/lua$ lua meta_test.lua
1
nil
test

可见当键值”b”在表内无法查找到之后会调用__index事件对应的匿名函数,后面我们从源码角度分析这个例子.

接下来源码角度分析各种类型的元表.
对于基本类型的元表,每类类型只有一个元表,某类类型共享同一个元表,基本类型的元表未设置之前都为空.

lstate.c
================
struct global_State{
  ....
  struct Table *mt[NUM_TAGS];  /* metatables for basic types */
  TString *tmname[TM_N];  /* array with tag-method names */
  ....
}

mt[NUM_TAGS]用于存储lua各种类型的元表,NUM_TAGS就lua类型个数加一.

mt[1] = {__len = func1, __index = func2 ….}   /*boolean*/
mt[2] = {__len = func1, __index = func2 ….}   /*lightuserdata*/
....
mt[NUM_TAGS]

tmname[TM_N]用于存储元表事件的字符串,位置与TMS一一对应,这张表在lua启动时会初始化.

ltm.c
==========
void luaT_init (lua_State *L) {
    static const char *const luaT_eventname[] = { /* ORDER TM */
    "__index", "__newindex",
    "__gc", "__mode", "__eq",
    "__add", "__sub", "__mul", "__div", "__mod",
    "__pow", "__unm", "__len", "__lt", "__le",
    "__concat", "__call"
};
int i;
for (i=0; i G(L)->tmname[i] = luaS_new(L, luaT_eventname[i]);
    luaS_fix(G(L)->tmname[i]); /* never collect these names */
}
}

查找某个对象类型的元表直接通过对象类型查找元表

(L)->mt[ttype(o)];

查找元表的事件函数,先从字节码的TMS查找tmname到事件字符串,然后到元表里以事件字符串作为键值查找函数.

luaH_getstr(mt, G(L)->tmname[event])

对于表和userdata每个对象都有自己独有的元表,是对象私有的元表而不像基本类型是类型共享元表

struct uv {
…
struct table metatable;
...
}
struct table {
…
struct table metatable;
...
}

lua程序里的setmetatable,getmetatable实际调用的是luaB_setmetatable/luaB_getmetatable两函数,以下是setmetatable的源码.可见在lua程序里只能为表设置元表

lbaselib.c
================
static int luaB_setmetatable (lua_State *L) {
  int t = lua_type(L, 2);
  luaL_checktype(L, 1, LUA_TTABLE);
  luaL_argcheck(L, t == LUA_TNIL || t == LUA_TTABLE, 2,
                    "nil or table expected");
  if (luaL_getmetafield(L, 1, "__metatable"))
    luaL_error(L, "cannot change a protected metatable");
  lua_settop(L, 2);
  lua_setmetatable(L, 1);
  return 1;
}

而在c程序里,我们就可以对userdata和一些基本类型进行设置元表.使用的函数是lua_setmetatable/lua_getmetatable.

lapi.c
============
LUA_API int lua_setmetatable (lua_State *L, int objindex) {
  TValue *obj;
  Table *mt;
  lua_lock(L);
  api_checknelems(L, 1);
  obj = index2adr(L, objindex);
  api_checkvalidindex(L, obj);
  if (ttisnil(L->top - 1))
    mt = NULL;
  else {
    api_check(L, ttistable(L->top - 1));
    mt = hvalue(L->top - 1);
  }
  switch (ttype(obj)) {
    case LUA_TTABLE: {
      hvalue(obj)->metatable = mt;
      if (mt)
        luaC_objbarriert(L, hvalue(obj), mt);
      break;
    }
    case LUA_TUSERDATA: {
      uvalue(obj)->metatable = mt;
      if (mt)
        luaC_objbarrier(L, rawuvalue(obj), mt);
      break;
    }
    default: {
      G(L)->mt[ttype(obj)] = mt;
      break;
    }
  }
  L->top--;
  lua_unlock(L);
  return 1;
}

根据类型到不同的位置来设置元表,查找元表简单就不例举了.

以上是元表的基础知识,接下来讲介绍元表的执行过程.

One Comment

  1. [...] 表是如何调度到元表的呢,上篇博文的例子我们看到当键值未能被查找到之后会调用__index事件对应的函数.现在举一个”__add”事件来遍历重要环节的源码.当然从luaV_execute开始. [...]

Leave a Reply