Lua源码阅读:表操作的指令
表操作的相关指令。
创建表:
创建一个空表,代码:
1 | local p = {} |
对应的OPCODE为OP_NEWTABLE,用于创建一个表,将结果存入寄存器。
1 | OP_NEWTABLE, |
simpleexp最终调用Constructor 函数,这个函数专门负责构造表。
解析表的信息会存放在 ConsControl 结构体中:
1 | struct { |
接下来进入 Constructor 函数:
1 | static void constructor (LexState *ls, expdesc *t) { |
- 第六行:生成一条 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 | static void field (LexState *ls, struct ConsControl *cc) { |
- 当没有解析到符号
}
时,有一个解析表达式的循环会一直执行(上层函数constructor执行)。 - 调用closelistfield 函数生成上一个表达式的相关指令。这里调用了
luaK_exp2nextreg(fs, &cc->v);
,注意上面提到过,最开始初始化 ConsControl 表达式时,其成员变量 v 的表达式类型是 VVOID,因此这种情况下进入这个函数并不会有什么效果,这就把循环和前面的初始化语句衔接在了一起。 - 针对具体的类型来做解析:
- 如果解析到一个变量,那么看紧跟着这个符号的是不是
=
,如果不是,就是一个数组方式的赋值,否则就是散列方式的赋值。 - 如果看到的是
[
符号,就认为这是一个散列部分的构造。 - 否则就是数组部分的构造了。如果是数组部分的构造,那么进入的是listfiled函数,否则就是 recfield 函数了。
- 如果解析到一个变量,那么看紧跟着这个符号的是不是
关于 listfiled 函数:
1 | static void listfield (LexState *ls, struct ConsControl *cc) { |
- 调用 expr 函数解析这个表达式,得到对应的 ConsControl 结构体中成员 v 的数据。前面提过,这个对象会暂存表构造过程中当前表达式的结果。
- 检查当前表中数组部分的数据梳理是否超过限制了。
- 依次将 ConsControl 结构体中的成员 na 和 toshore 加 1。
每解析完一个表达式,会调用 closelistfield,它的工作是针对数组部分的。
1 | static void closelistfield (FuncState *fs, struct ConsControl *cc) { |
- 调用 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 中,需要注意:
- key 是一个常量。
- key 是一个变量,需要首先去获取这个变量的值。
1 | static void recfield (LexState *ls, struct ConsControl *cc) { |
上面的 lua 代码是第一种情况,具体如下:
- 得到 key 常量在常量数组中的索引,根据这个值调用 luaK_exp2RK 函数生成 RK 值。
- 得到 value 表达式的索引,根据这个值调用 luaK_exp2RK 函数生成 RK 值。
- 将前两步的值以及表在寄存器中的索引,写入 OP_SETTABLE 的参数中。
主要的步骤就是查找表达式,然后转换为对应的RK值写入OPCODE中。
当键是变量时,情况如下:
1 | local a = "a" |
首先需要一条语句将常量“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,意味着这个值可能来自寄存器,也可能来自常量数组
查询表中的数据分为以下两步:
- 将待查询的字符串变量赋值到栈上的一个位置中。
- 以第一步中已经存储了该变量字符串值的数据作为键,在表中进行查询。
元表的实现原理:
在lua 初始化时,首先会调用 luaT_init 函数初始化其中定义的几种元方法对应的字符串。这些都是全局共用的,在初始化完毕之后只可读不可写,也不能回收:
1 | void luaT_init (lua_State *L) { |
这里将遍历前面定义的枚举类型 TMS,将每一个类型对应的字符串赋值给 global_State 结构体中的 tmname,同时调用函数 luaC_fix 将这些字符串设置为不可回收的。因为在这个系统运行的过程中,这些字符串会一直用到,至于如何让它们变成不可回收的,后面 GC 的部分会做分析。
Lua 虚拟机从一个表中查询数据的过程如下:
1 | /* |
这个函数根据该对象的元方法表中的_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 | const TValue *luaT_gettmbyobj (lua_State *L, const TValue *o, TMS event) { |
只有在数据类型为 Table 和 udata 的时候,才能拿到对象的 metatable 表,其他时候是到 global_State结构体的成员 mt 中获取的,但是这对于其他数据类型而言,一直是空值。
fasttm宏的作用是从这个数据的元表中查询相应的对象返回:
1 |
|