Lua源码阅读:表操作的指令

8.5k 词

表操作的相关指令。

创建表:

创建一个空表,代码:

1
local p = {}

对应的OPCODE为OP_NEWTABLE,用于创建一个表,将结果存入寄存器。

1
OP_NEWTABLE,

simpleexp最终调用Constructor 函数,这个函数专门负责构造表。

解析表的信息会存放在 ConsControl 结构体中:

1
2
3
4
5
6
7
struct  {
expdesc v; /* last list item read 存储表构造过程中最后一个表达式的信息 */
expdesc *t; /* table descriptor 构造表相关的表达式信息,因为是外部传入的字段所以使用指针 */
int nh; /* total number of 'record' elements 初始化表时,散列部分数据的数量 */
int na; /* total number of array elements 初始化表时,数组部分数据的数量 */
int tostore; /* number of array elements pending to be stored 当前构造表时内部的数组部分的数据如果超过这个值,就首先调用一次 OP_SETLIST 函数写入寄存器中 */
};

接下来进入 Constructor 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void constructor (LexState *ls, expdesc *t) {
/* constructor -> '{' [ field { sep field } [sep] ] '}'
sep -> ',' | ';' */
FuncState *fs = ls->fs;
int line = ls->linenumber;
int pc = luaK_codeABC(fs, OP_NEWTABLE, 0, 0, 0);
struct cc;
cc.na = cc.nh = cc.tostore = 0;
cc.t = t;
init_exp(t, VRELOCABLE, pc);
init_exp(&cc.v, VVOID, 0); /* no value (yet) */
luaK_exp2nextreg(ls->fs, t); /* fix it at stack top */
checknext(ls, '{');
do {
lua_assert(cc.v.k == VVOID || cc.tostore > 0);
if (ls->t.token == '}') break;
closelistfield(fs, &cc);
field(ls, &cc);
} while (testnext(ls, ',') || testnext(ls, ';'));
check_match(ls, '}', '{', line);
lastlistfield(fs, &cc);
SETARG_B(fs->f->code[pc], luaO_int2fb(cc.na)); /* set initial array size */
SETARG_C(fs->f->code[pc], luaO_int2fb(cc.nh)); /* set initial table size */
}
  • 第六行:生成一条 OP_NEWTABLE 指令,这条指令创建的表最终会根据指令中的参数 A 存储的寄存器地址,赋值给本函数栈内的寄存器,所以这条指令是需要重定向的。
  • 第七行到第十一行:初始化 ConsControl 结构体。需要说明的是第 11 行,此时将ConsControl 结构体中的对象 v 初始化为 VVOID。前面说过这个数据存储的是表构造过程中最后一个表达式的信息,因为这里还没有解析到表构造中的信息,所以这个表达式的类型为 VVOID。
  • 第十二行:调用前面提到的解析表达式到寄存器的函数 luaK_exp2nextreg,将寄存器地址修正为前面创建的 OP_NEWTABLE的指令 A。
  • 第 22 行~23 行:将 ConsControl 结构体中存放的散列和数组部分的大小,写入前面生成的 OP_NEWTABLE 指令的 B 和 C 部分。

上面创建了一个简单的空表,如果添加上数组部分:

1
local p = {1,2}

与前面相比,多了两条 loadk 指令以及一条setlist 指令。loadk 指令用于把表构造表达式中的常量 1 和 2 加载到函数栈中,而紧跟着的 setlist 指令则使用这两个常量初始化表的数组部分。

setlist 指令的格式如下,对应 OP_SETLIST 指令,用于以一个基地址和数量来将数据写入表的数组部分:

1
OP_SETLIST,/*	A B C	R(A)[(C-1)*FPF+i] := R(A+i), 1 <= i <= B	*/
  • A:OP_NEWTABLE 指令中创建好的表所在的寄存器,它后面紧跟着待写入的数据
  • B:待写入数据的数量
  • C:FPF 索引,即每次写入最多的是 LFIELDS_PER_FLUSH

在 Lua5.3中,使用field(ls, &cc);函数对散列、数组部分解析构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void field (LexState *ls, struct ConsControl *cc) {
/* field -> listfield | recfield */
switch(ls->t.token) {
case TK_NAME: { /* may be 'listfield' or 'recfield' */
if (luaX_lookahead(ls) != '=') /* expression? */
listfield(ls, cc);
else
recfield(ls, cc);
break;
}
case '[': {
recfield(ls, cc);
break;
}
default: {
listfield(ls, cc);
break;
}
}
}
  1. 当没有解析到符号}时,有一个解析表达式的循环会一直执行(上层函数constructor执行)。
  2. 调用closelistfield 函数生成上一个表达式的相关指令。这里调用了luaK_exp2nextreg(fs, &cc->v);,注意上面提到过,最开始初始化 ConsControl 表达式时,其成员变量 v 的表达式类型是 VVOID,因此这种情况下进入这个函数并不会有什么效果,这就把循环和前面的初始化语句衔接在了一起。
  3. 针对具体的类型来做解析:
    1. 如果解析到一个变量,那么看紧跟着这个符号的是不是=,如果不是,就是一个数组方式的赋值,否则就是散列方式的赋值。
    2. 如果看到的是[符号,就认为这是一个散列部分的构造。
    3. 否则就是数组部分的构造了。如果是数组部分的构造,那么进入的是listfiled函数,否则就是 recfield 函数了。

关于 listfiled 函数:

1
2
3
4
5
6
7
static void listfield (LexState *ls, struct ConsControl *cc) {
/* listfield -> exp */
expr(ls, &cc->v);
checklimit(ls->fs, cc->na, MAX_INT, "items in a constructor");
cc->na++;
cc->tostore++;
}
  • 调用 expr 函数解析这个表达式,得到对应的 ConsControl 结构体中成员 v 的数据。前面提过,这个对象会暂存表构造过程中当前表达式的结果。
  • 检查当前表中数组部分的数据梳理是否超过限制了。
  • 依次将 ConsControl 结构体中的成员 na 和 toshore 加 1。

每解析完一个表达式,会调用 closelistfield,它的工作是针对数组部分的。

1
2
3
4
5
6
7
8
9
static void closelistfield (FuncState *fs, struct ConsControl *cc) {
if (cc->v.k == VVOID) return; /* there is no list item */
luaK_exp2nextreg(fs, &cc->v);
cc->v.k = VVOID;
if (cc->tostore == LFIELDS_PER_FLUSH) {
luaK_setlist(fs, cc->t->u.info, cc->na, cc->tostore); /* flush */
cc->tostore = 0; /* no more items pending */
}
}
  • 调用 luaK_exp2nextreg 函数将前面得到的 ConsControl 结构体中成员 v 的信息存入寄存器中。
  • 如果此时 tostroe 成员的值等于 LFIELDS_PER_FLUSH,那么生成一个 OP_SETLIST 指令,用于将当前寄存器上的数据写入表的数组部分。需要注意的是,这个地方存取的数据在栈上的位置是紧跟着 OP_NEWTABLE 指令中的参数 A 在栈上的位置,这样的话使用一个参数既可以得到表的地址,又可以知道待存入的数据是哪些。之所以需要限制每次调用 OP_SETLIST 指令中的数据量不超过 LFIELDS_PER_FLUSH,是因为如果不做这个限制,会导致数组部分数据过多时,占用过多的寄存器,而 Lua 栈对寄存器数量是有限制的。

如果是散列表部分:

1
local p = {["a"] = 1}

紧跟着 newtable 的是 settable,这个指令用来完成散列部分的初始化,格式如下:

1
OP_SETTABLE,/*	A B C	R(A)[RK(B)] := RK(C)				*/

作用是向一个表的散列部分赋值,其中各个参数:

  • A:表所在的寄存器。
  • B:key 存放的位置,注意其格式是 RK,也就是说这个值可能来自寄存器,也可能来自常量数组。
  • C:value 存放的位置,注意其格式是 RK,也就是说这个值可能来自寄存器,也可能来自常量数组。

初始化散列部分的代码会走入 recfield 中,需要注意:

  1. key 是一个常量。
  2. key 是一个变量,需要首先去获取这个变量的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void recfield (LexState *ls, struct ConsControl *cc) {
/* recfield -> (NAME | '['exp1']') = exp1 */
FuncState *fs = ls->fs;
int reg = ls->fs->freereg;
expdesc key, val;
int rkkey;
if (ls->t.token == TK_NAME) {
checklimit(fs, cc->nh, MAX_INT, "items in a constructor");
checkname(ls, &key);
}
else /* ls->t.token == '[' */
yindex(ls, &key);
cc->nh++;
checknext(ls, '=');
rkkey = luaK_exp2RK(fs, &key);
expr(ls, &val);
luaK_codeABC(fs, OP_SETTABLE, cc->t->u.info, rkkey, luaK_exp2RK(fs, &val));
fs->freereg = reg; /* free registers */
}

上面的 lua 代码是第一种情况,具体如下:

  1. 得到 key 常量在常量数组中的索引,根据这个值调用 luaK_exp2RK 函数生成 RK 值。
  2. 得到 value 表达式的索引,根据这个值调用 luaK_exp2RK 函数生成 RK 值。
  3. 将前两步的值以及表在寄存器中的索引,写入 OP_SETTABLE 的参数中。

主要的步骤就是查找表达式,然后转换为对应的RK值写入OPCODE中。

当键是变量时,情况如下:

1
2
local a = "a"
local p = {[a] = 1}

首先需要一条语句将常量“a”加载到局部变量a中,这里需要一条loadk指令。此外,这里的键来自局部变量,那么对应的RK格式也会有差异,因为此时不是从常量数组中获取key的数据,而是从寄存器中:

  • 解析变量形成表达式相关的expdesc结构体;
  • 根据不同的表达式类型将表达式大的值存入寄存器。

查询表:

最后一个表相关的指令:OP_GETTABLE,其格式如下:

1
OP_GETTABLE,/*	A B C	R(A) := R(B)[RK(C)]				*/

其作用是根据key从表中获取数据存入寄存器中,各参数如下:

  • A:存放结果的寄存器
  • B:表所在的寄存器
  • C:key存放的位置,其格式为RK,意味着这个值可能来自寄存器,也可能来自常量数组

查询表中的数据分为以下两步:

  1. 将待查询的字符串变量赋值到栈上的一个位置中。
  2. 以第一步中已经存储了该变量字符串值的数据作为键,在表中进行查询。

元表的实现原理:

在lua 初始化时,首先会调用 luaT_init 函数初始化其中定义的几种元方法对应的字符串。这些都是全局共用的,在初始化完毕之后只可读不可写,也不能回收:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void luaT_init (lua_State *L) {
static const char *const luaT_eventname[] = { /* ORDER TM */
"__index", "__newindex",
"__gc", "__mode", "__len", "__eq",
"__add", "__sub", "__mul", "__mod", "__pow",
"__div", "__idiv",
"__band", "__bor", "__bxor", "__shl", "__shr",
"__unm", "__bnot", "__lt", "__le",
"__concat", "__call"
};
int i;
for (i=0; i<TM_N; i++) {
G(L)->tmname[i] = luaS_new(L, luaT_eventname[i]);
luaC_fix(L, obj2gco(G(L)->tmname[i])); /* never collect these names */
}
}

这里将遍历前面定义的枚举类型 TMS,将每一个类型对应的字符串赋值给 global_State 结构体中的 tmname,同时调用函数 luaC_fix 将这些字符串设置为不可回收的。因为在这个系统运行的过程中,这些字符串会一直用到,至于如何让它们变成不可回收的,后面 GC 的部分会做分析。

Lua 虚拟机从一个表中查询数据的过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
** Main function for table access (invoking metamethods if needed).
** Compute 'val = t[key]'
*/
void luaV_gettable (lua_State *L, const TValue *t, TValue *key, StkId val) {
int loop; /* counter to avoid infinite loops */
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 not nil? */
(tm = fasttm(L, h->metatable, TM_INDEX)) == NULL) { /* or no TM? */
setobj2s(L, val, res); /* result is the raw get */
return;
}
/* else will try metamethod */
}
else if (ttisnil(tm = luaT_gettmbyobj(L, t, TM_INDEX)))
luaG_typeerror(L, t, "index"); /* no metamethod */
if (ttisfunction(tm)) { /* metamethod is a function */
luaT_callTM(L, tm, t, key, val, 1);
return;
}
t = tm; /* else repeat access over 'tm' */
}
luaG_runerror(L, "gettable chain too long; possible loop");
}

这个函数根据该对象的元方法表中的_index 表逐层向上查找:

  • 第 9 行~第 16 行:如果 t 是表,则尝试根据 key 在该表中查找数据,如果找到了非空数据,或者找到该表的元方法表中_index 为空,都返回查找结果。反之,如果不返回查找结果,只可能是上面两个条件的反面情况,即在原表中查找的数据为空,同时原表的元方法表存在 _index成员,而且此时该成员已经赋值给了 tm。
  • 第 19 行~第 20 行:这说明前面判断 t 不是一个表,于是调用 luaT_gettmbyobj 函数,尝试拿到这个数据的 metatable[“__index”],如果返回空,那么报错并返回。
  • 第 21 行~第 24 行:此时 tm 不是一个空值,于是判断它是不是函数,如果是,就通过 luaT_callTM函数来调用它,然后返回。
  • 第 25 行:来到这里,说明前面得到的 tm,既不是空值,也不是函数,而是 t->metatable[“__index”],此时将这个值赋值为下一个循环中处理的 t,继续前面的操作。
  • 第 27 行:如果这个逐层查找过程的层次过多,超过了 MAXTAGLOOP,就终止循环,报错并返回。

关于 luaT_gettmbyobj 函数,它的作用是根据一个数据的类型返回它的元表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const TValue *luaT_gettmbyobj (lua_State *L, const TValue *o, TMS event) {
Table *mt;
switch (ttnov(o)) {
case LUA_TTABLE:
mt = hvalue(o)->metatable;
break;
case LUA_TUSERDATA:
mt = uvalue(o)->metatable;
break;
default:
mt = G(L)->mt[ttnov(o)];
}
return (mt ? luaH_getstr(mt, G(L)->tmname[event]) : luaO_nilobject);
}

只有在数据类型为 Table 和 udata 的时候,才能拿到对象的 metatable 表,其他时候是到 global_State结构体的成员 mt 中获取的,但是这对于其他数据类型而言,一直是空值。

fasttm宏的作用是从这个数据的元表中查询相应的对象返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

((et)->flags & (1u<<(e))) ? NULL : luaT_gettm(et, e, (g)->tmname[e]))

#define fasttm(l,et,e) gfasttm(G(l), et, e)

/*
** function to be used with macro "fasttm": optimized for absence of
** tag methods
*/
const TValue *luaT_gettm (Table *events, TMS event, TString *ename) {
const TValue *tm = luaH_getstr(events, ename);
lua_assert(event <= TM_EQ);
if (ttisnil(tm)) { /* no tag method? */
events->flags |= cast_byte(1u<<event); /* cache this fact */
return NULL;
}
else return tm;
}