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

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

lvm.c
=============
void luaV_execute (lua_State *L, int nexeccalls) {
    ....
    switch (GET_OPCODE(i)) {
        case OP_ADD: {
            arith_op(luai_numadd, TM_ADD);
            continue;
        }
....

arith_op是个宏,会针对操作数进行类型判断,如果是数字类型就会立即进行相加,这就是为什么基本类型即便设置了加减等事件函数后还是没有效果,例如你为数字的加法设置了__add事件后数字相加不会掉用这个元表方法,后面会举一个例子.
继续当发现操作数字为非数字类型后,就会调用Airth函数.

#define arith_op(op,tm) { \
        TValue *rb = RKB(i); \
        TValue *rc = RKC(i); \
        if (ttisnumber(rb) && ttisnumber(rc)) { \
          lua_Number nb = nvalue(rb), nc = nvalue(rc); \
          setnvalue(ra, op(nb, nc)); \
        } \
        else \
          Protect(Arith(L, ra, rb, rc, tm)); \
      }

Airth会对操作数再进行一次努力试图把其转化为数字,这样某些字符串也会被转换成数字相加.如果无法转化成数字则会尝试元表调用call_binTM函数.

static void Arith (lua_State *L, StkId ra, const TValue *rb,
                   const TValue *rc, TMS op) {
  TValue tempb, tempc;
  const TValue *b, *c;
  if ((b = luaV_tonumber(rb, &tempb)) != NULL &&
      (c = luaV_tonumber(rc, &tempc)) != NULL) {
    lua_Number nb = nvalue(b), nc = nvalue(c);
    switch (op) {
      case TM_ADD: setnvalue(ra, luai_numadd(nb, nc)); break;
      case TM_SUB: setnvalue(ra, luai_numsub(nb, nc)); break;
      case TM_MUL: setnvalue(ra, luai_nummul(nb, nc)); break;
      case TM_DIV: setnvalue(ra, luai_numdiv(nb, nc)); break;
      case TM_MOD: setnvalue(ra, luai_nummod(nb, nc)); break;
      case TM_POW: setnvalue(ra, luai_numpow(nb, nc)); break;
      case TM_UNM: setnvalue(ra, luai_numunm(nb)); break;
      default: lua_assert(0); break;
    }
  }
  else if (!call_binTM(L, rb, rc, ra, op))
    luaG_aritherror(L, rb, rc);
}

call_binTM函数会对操作数进行尝试获取获取元表再根据事件名字(这里是__add)获取事件函数,如果获取成功则把操作数字压栈并调用这个事件函数.

static int call_binTM (lua_State *L, const TValue *p1, const TValue *p2,
                       StkId res, TMS event) {
  const TValue *tm = luaT_gettmbyobj(L, p1, event);  /* try first operand */
  if (ttisnil(tm))
    tm = luaT_gettmbyobj(L, p2, event);  /* try second operand */
  if (ttisnil(tm)) return 0;
  callTMres(L, res, tm, p1, p2);
  return 1;
}

好了以上就是调度元表函数的过程.

我们再来看看最常见的__index,__new_index函数是怎么被调用的,还是luaV_execute.

lvm.c
=====================

      case OP_GETTABLE: {
        Protect(luaV_gettable(L, RB(i), RKC(i), ra));
        continue;
      }
....
      case OP_SETTABLE: {
        Protect(luaV_settable(L, ra, RKB(i), RKC(i)));
        continue;
      }

我们来看查找表,先进行常规的键值查找,如果找到则返回,如果键值未被发现则会查找TM_INDEX对应的元表事件函数.

void luaV_gettable (lua_State *L, const TValue *t, TValue *key, StkId val) {
  int loop;
  for (loop = 0; loop < MAXTAGLOOP; loop++) {     const TValue *tm;     if (ttistable(t)) {  /* `t' is a table? */       Table *h = hvalue(t);       const TValue *res = luaH_get(h, key); /* do a primitive get */       if (!ttisnil(res) ||  /* result is no nil? */           (tm = fasttm(L, h->metatable, TM_INDEX)) == NULL) { /* or no TM? */
        setobj2s(L, val, res);
        return;
      }
      /* else will try the tag method, cant find so try metamethod */
    }
    else if (ttisnil(tm = luaT_gettmbyobj(L, t, TM_INDEX)))
      luaG_typeerror(L, t, "index");
    if (ttisfunction(tm)) {
      callTMres(L, val, tm, t, key);
      return;
    }
    t = tm;  /* else repeat with `tm' */
  }
  luaG_runerror(L, "loop in gettable");
}

luaT_gettmbyobj先查找到元表再查找对应的事件函数

const TValue *luaT_gettmbyobj (lua_State *L, const TValue *o, TMS event) {
  Table *mt;
  switch (ttype(o)) {
    case LUA_TTABLE:
      mt = hvalue(o)->metatable;
      break;
    case LUA_TUSERDATA:
      mt = uvalue(o)->metatable;
      break;
    default:
      mt = G(L)->mt[ttype(o)];
  }
  return (mt ? luaH_getstr(mt, G(L)->tmname[event]) : luaO_nilobject);
}

回到luaV_gettable找到这个事件函数后就调用callTMres,参数压栈执行函数.

这就是元表事件函数的调用过程,接下来会讲解元表的其它用法,未完待续.

One Comment

  1. [...] 这个lua程序在普通的lua解释器下是回报错的,因为数字类型并未有计算长度的函数.当给数字类型设置元表后,__len成功的执行了,而__add并未调用元表事件函数,这符合我们在上一篇博文里的分析.同时可以发现数字类型已经设置了一个元表,而其它基本类型的元表都是为nil的. [...]

Leave a Reply