Skip to content

Latest commit

 

History

History
316 lines (251 loc) · 12.9 KB

ch04-表相关操作指令.md

File metadata and controls

316 lines (251 loc) · 12.9 KB

表相关操作指令

创建表

首先来看创建一个空表,对应的测试Lua代码是:

local p = {}

对应的OPCODE是OP_NEWTABLE,它的格式是:

OP_NEWTABLE,/*  A B C R(A) := {} (size = B,C)       */
OPCODE 作用
OP_NEWTABLE 创建一个表,将结果存入寄存器
参数 说明
参数A 创建好的表存入寄存器的索引
参数B 表的数组部分大小
参数C 表的hash部分大小

使用ChunkSpy反编译出来的指令是:

; function [0] definition (level 1)
; 0 upvalues, 0 params, 2 stacks
.function  0 0 2 2
.local  "p"  ; 0
; (1)  local p = {}
[1] newtable   0   0   0    ; array=0, hash=0
[2] return     0   1
; end of function

与这部分代码相关的EBNF:

chunk -> { stat [`;'] }
stat -> localstat
localstat -> LOCAL NAME {`,' NAME} [`=' explist1]
explist1 -> expr {`,' expr}
exp -> subexpr
subexpr -> simpleexp
simpleexp -> NUMBER
local a = 10

回到前面提过的Lua的EBNF词法,来看看解析这一句代码,Lua解释器都走过了哪些函数,这里仅截取与这行代码相关的部分:

chunk -> { stat [`;'] }
stat -> localstat
localstat -> LOCAL NAME {`,' NAME} [`=' explist1]
explist1 -> expr {`,' expr}
exp -> subexpr
subexpr -> simpleexp
simpleexp -> constructor
constructor -> `{' [ fieldlist ]  `}'

可以看到,与前面的区别在于最后这部分走到了constructor函数中,这个函数就是专门负责构造表的工作。 在前面讲到表达式的解析时,谈到会有一个数据结构expdesc,会将解析后的表达式的信息存储到这里。而在解析表的信息时,会存放在ConsControl结构体中,它包括如下几个字段:

  • expdesc v:存储表构造过程中最后一个表达式的信息。
  • expdesc *t:构造表相关的表达式信息,与上一个字段的区别在于这里使用的是指针,因为这个字段是由外部传入的。
  • int nh:初始化表时hash部分数据的数量。
  • int na:初始化表时数组部分数据的数量。
  • int tostore:Lua解析器中定义了一个叫LFIELDS_PER_FLUSH的常量,当前的值是50,这个值的意义在于,当前构造表时,内部的数组部分的数据,如果超过这个值,就首先调用一次OP_SETLIST函数写入寄存器中,后面讲到数组部分的初始化时会再展开讨论这个策略。

接下来进入这里的核心constructor函数,以这里最简单的情况来看,裁剪之后的代码是:

498 static void constructor (LexState *ls, expdesc *t) {

502   int pc = luaK_codeABC(fs, OP_NEWTABLE, 0, 0, 0);
503   struct ConsControl cc;
504   cc.na = cc.nh = cc.tostore = 0;
505   cc.t = t;
506   init_exp(t, VRELOCABLE, pc);
507   init_exp(&cc.v, VVOID, 0);  /* no value (yet) */
508   luaK_exp2nextreg(ls->fs, t);  /* fix it at stack top (for gc) */

535   SETARG_B(fs->f->code[pc], luaO_int2fb(cc.na)); /* set initial array size */
536   SETARG_C(fs->f->code[pc], luaO_int2fb(cc.nh));  /* set initial table size */
537 }

可以看到在这个最简单的情况下,主要做了如下几个工作:

  1. 502:生成一条OP_NEWTABLE指令,注意在前面关于这个指令的说明中,这条指令创建的表,最终会根据指令中的参数A存储的寄存器地址,赋值给本函数栈内的寄存器,所以很显然,这条指令是需要重定向的,于是506行的代码就不难理解了。
  2. 503-507:初始化前面提过的ConsControl结构体,都比较好理解,需要说明一下的是507行,此时将ConsControl结构体中的对象v初始化为VVOID,前面提到这个数据存储的表构造过程中最后一个表达式的信息,因为这里还没有解析到表构造中的信息,所以这个表达式的类型为VVOID。
  3. 508:调用前面提到的解析表达式到寄存器的函数luaK_exp2nextreg,将寄存器地址赋值修正第一步中创建的OP_NEWTABLE指令的参数A。
  4. 535-536:将ConsControl结构体中存放的hash和数组部分的大小,写入第一步中生成的OP_NEWTABLE指令的B,C部分。

以上完成了一个简单的表创建的构造过程,下面在此基础上做一些修改,添加上数组部分的创建:

local p = {1,2}

ChunkSpy看到的指令变成了这样:

; function [0] definition (level 1)
; 0 upvalues, 0 params, 3 stacks
.function  0 0 2 3
.local  "p"  ; 0
.const  1  ; 0
.const  2  ; 1
; (1)  local p = {1,2}
[1] newtable   0   2   0    ; array=2, hash=0
[2] loadk      1   0        ; 1
[3] loadk      2   1        ; 2
[4] setlist    0   2   1    ; index 1 to 2
[5] return     0   1
; end of function

可以看到,与前面想必,在newtable指令之后,还跟着两条loadk指令和一条setlist。setlist指令的格式是:

OP_SETLIST,/* A B C R(A)[(C-1)*FPF+i] := R(A+i), 1 <= i <= B  */
OPCODE 作用
OP_SETLIST 以一个基地址和数量来将数据写入表的数组部分
参数 说明
参数A OP_NEWTABLE指令中创建好的表所在的寄存器,它后面紧跟着待写入的数据。
参数B 待写入数据的数量
参数C FPF(也就是前面提到的LFIELDS_PER_FLUSH常量)索引,即每次写入最多是LFIELDS_PER_FLUSH

下面将前面constructor函数被省略的涉及到hash、数组部分解析构造的内容列出来:

498 static void constructor (LexState *ls, expdesc *t) {

510   do {
511     lua_assert(cc.v.k == VVOID || cc.tostore > 0);
512     if (ls->t.token == '}') break;
513     closelistfield(fs, &cc);
514     switch(ls->t.token) {
515       case TK_NAME: {  /* may be listfields or recfields */
516         luaX_lookahead(ls);
517         if (ls->lookahead.token != '=')  /* expression? */
518           listfield(ls, &cc);
519         else
520           recfield(ls, &cc);
521         break;
522       }
523       case '[': {  /* constructor_item -> recfield */
524         recfield(ls, &cc);
525         break;
526       }
527       default: {  /* constructor_part -> listfield */
528         listfield(ls, &cc);
529         break;
530       }
531     }
532   } while (testnext(ls, ',') || testnext(ls, ';'));

537 }

可以看到这里的流程大体是:

  1. 当没有解析到符号‘}’时,有一个解析表达式的循环一直执行下去。
  2. 513:调用closelistfield生成上一个表达式的相关的指令,容易想到肯定又会调用luaK_exp2nextreg函数。注意在上面提到过,最开始初始化ConsControl表达式时,其成员变量v的表达式类型是VVOID,因此这种情况下进入这个函数并不会有什么效果,这样就把循环和前面的初始化语句衔接统一在了一起。
  3. 514-531:针对具体的类型来做解析。有如下几种类型,1)如果解析到一个变量,那么看紧跟着这个符号的是不是"=",如果没有”=“那么就是一个数组方式的赋值,否则就是hash方式的赋值;2)如果看到的是”[“符号,那么认为这是一个hash部分的构造;3)否则就是数组部分的构造了。其中,如果是数组部分的构造,那么进入的是listfield函数,否则就是recfield函数了。

就我们的例子而言,这里还是数组部分的构造,于是接下来看看listfield函数,它主要做了如下的工作:

  1. 调用expr函数解析这个表达式,得到对应的ConsControl结构体中成员v的数据,前面提过这个对象会暂存表构造过程中当前表达式的结果。
  2. 检查当前表中数组部分的数据梳理是否超过限制了。
  3. 依次将ConsControl结构体中的成员na,tostore加1.

每解析完一个表达式,在前面循环语句中的513行会调用closelistfield,它做的工作,从其命名可以看出,是针对数组部分的:

  1. 调用luaK_exp2nextreg将前面得到的ConsControl结构体中成员v的信息存入寄存器中。
  2. 如果此时tostore成员的值等于LFIELDS_PER_FLUSH,那么生成一个OP_SETLIST指令,将当前寄存器上的数据写入表的数组部分。需要注意的是,这个地方存取的数据,其位置是紧跟着OP_NEWTABLE指令中的参数A,这样的话使用一个参数既可以得到表的地址,又可以知道待存入的数据是哪些。之所以需要限制每次调用OP_SETLIST指令中的数据量不超过LFIELDS_PER_FLUSH,是因为如果不做这个限制,那么会导致当数组部分数据过多时,会占用过多的寄存器,而Lua栈对寄存器数量是有限制的。

读者可以自己尝试一下使用一个非常多数据的数组来初始化表是如何处理的。

接下来看如果是hash部分是如何做的,将前面的Lua代码修改为:

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

ChunkSpy看到的指令变成了这样:

; function [0] definition (level 1)
; 0 upvalues, 0 params, 2 stacks
.function  0 0 2 2
.local  "p"  ; 0
.const  "a"  ; 0
.const  1  ; 1
; (1)  local p = {["a"]=1}
[1] newtable   0   0   1    ; array=0, hash=1
[2] settable   0   256 257  ; "a" 1
[3] return     0   1
; end of function

可以看到,紧跟着newtable的是settable,使用这个指令来完成hash部分的初始化,其格式如下:

OP_SETTABLE,/*  A B C R(A)[RK(B)] := RK(C)        */
OPCODE 作用
OP_SETTABLE 向一个表的hash部分赋值
参数 说明
参数A 表所在的寄存器。
参数B key存放的位置,注意其格式是RK,也就是说这个值可能来自寄存器,也可能来自常量数组。
参数C value存放的位置,注意其格式是RK,也就是说这个值可能来自寄存器,也可能来自常量数组。

这里提到的RK格式,在前面分析Lua虚拟机指令格式的部分有阐述,还不熟悉的可以回头看看前面的描述。

在前面的分析中,初始化hash部分代码会走入recfield函数中,但是需要注意hash的初始化,分为两种情况:

  1. key是一个常量。
  2. key是一个变量,需要首先去获取这个变量的值。

在上面给出的例子代码中,是第一种情况,这种情况比较简单,分为以下几个步骤:

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

可以看到,主要的步骤就是查找表达式,这一步在前面的赋值部分已经解释的很清楚了,然后转换为对应的RK值写入OPCODE中。

我们继续修改Lua代码,看一看key是变量时如何处理:

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

先不看ChunkSpy生成的代码,思考一下应该是如何的。显然,这里首先需要一条语句将常量”a“加载到局部变量a中,这里需要对应一条loadk指令;另外区别于前面例子的是,这里的key来自局部变量,那么对应的RK格式也会有差异,因为此时不是从常量数组中获取key的数据,而是从寄存器中。

来看看我们的猜测是不是对的:

; function [0] definition (level 1)
; 0 upvalues, 0 params, 2 stacks
.function  0 0 2 2
.local  "a"  ; 0
.local  "p"  ; 1
.const  "a"  ; 0
.const  1  ; 1
; (1)  local a = "a"
[1] loadk      0   0        ; "a"
; (2)  local p = {[a]=1}
[2] newtable   1   0   1    ; array=0, hash=1
[3] settable   1   0   257  ; 1
[4] return     0   1
; end of function

确实这里在最开始多了loadk指令,将常量"a"加载到寄存器0中;然后settable指令中的key值小于255,也就是这个值来自于寄存器0.

有了这个准备,其实再回头理解代码就很容易了,解析一个以变量为key的工作在yindex函数中进行,不难理解它的工作还是前面提到的:

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

以上,完成了表相关的三个指令OP_NEWTABLE,OP_SETLIST和OP_SETTABLE的分析,来看最后一个表相关的指令OP_GETTABLE,其格式如下:

OP_SETTABLE,/*  A B C R(A)[RK(B)] := RK(C)        */
OPCODE 作用
OP_GETTABLE 根据key从表中获取数据存入寄存器中
参数 说明
参数A 存放结果的寄存器。
参数B 表所在的寄存器。
参数C key存放的位置,注意其格式是RK,也就是说这个值可能来自寄存器,也可能来自常量数组。

修改前面的Lua代码,加上一条从表中获取数据的语句:

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

使用ChunkSpy反编译出来的指令是:

; function [0] definition (level 1)
; 0 upvalues, 0 params, 2 stacks
.function  0 0 2 2
.local  "p"  ; 0
.local  "b"  ; 1
.const  "a"  ; 0
.const  1  ; 1
; (1)  local p = {["a"]=1}
[1] newtable   0   0   1    ; array=0, hash=1
[2] settable   0   256 257  ; "a" 1
; (2)  local b = p["a"]
[3] gettable   1   0   256  ; "a"
[4] return     0   1
; end of function