探索Lua5.2内部实现:虚拟机指令(6)FUNCTION

原文链接

name args desc
OP_CALL A B C A B C R(A), … ,R(A+C-2) := R(A)(R(A+1), … ,R(A+B-1))
CALL执行一个函数调用。寄存器A中存放函数对象,所有参数按顺序放置在A后面的寄存器中。B-1表示参数个数 。如果参数列表的最后一个表达式是变长的,则B会设置为0,表示使用A+1到当前栈顶作为参数。函数调用的返回值会按顺序存放在从寄存器A开始的C-1个寄存器中。如果C为0,表示返回值的个数由函数决定。

1
f();

1
2
3
1    [1]    GETTABUP     0 0 -1    ; _ENV "f"
2 [1] CALL 0 1 1
3 [1] RETURN 0 1

第一行取得全局变量f的值,保存到寄存器0。第二行CALL调用寄存器0中的函数,参数和返回值都是0。

1
local t = {f(...)};

1
2
3
4
5
6
1    [1]    NEWTABLE     0 0 0
2 [1] GETTABUP 1 0 -1 ; _ENV "f"
3 [1] VARARG 2 0
4 [1] CALL 1 0 0
5 [1] SETLIST 0 0 1 ; 1
6 [1] RETURN 0 1

第一行NETTABLE创建一个表放到寄存器0中。第二行获取全局变量f放到寄存器1中。第三行VARARG表示使用当前函数的变长参数列表。第四行的CALL调用寄存器1中的函数,B为0,代表参数是变长的。前面讲过,如果表的构造的最后一项是多返回值的表达式,则这个表会接受所有的返回值。这里就是这种情况,表的构造会接受函数所有的返回值,所以C也为0。

name args desc
OP_TAILCALL A B C A B C return R(A)(R(A+1), … ,R(A+B-1))
如果一个return statement只有一个函数调用表达式,这个函数调用指令CALL会被改为TAILCALL指令。TAILCALL不会为要调用的函数增加调用堆栈的深度,而是直接使用当前调用信息。ABC操作数与CALL的意思一样,不过C永远都是0。TAILCALL在执行过程中,只对lua closure进行tail call处理,对于c closure,其实与CALL没什么区别。

1
return f();

1
2
3
4
1    [1]    GETTABUP     0 0 -1    ; _ENV "f"
2 [1] TAILCALL 0 1 0
3 [1] RETURN 0 0
4 [1] RETURN 0 1

上面如果f是一个lua closure,那么执行到第二行后,此函数就会返回了,不会执行到后面第三行的RETURN。如果f是一个c closure,那就和CALL一样调用这个函数,然后依赖第三行的RETURN返回。这就是为什么TAILCALL后面还会己跟着生成一个RETURN的原因。

name args desc
OP_RETURN A B return R(A), … ,R(A+B-2)
RETURE将返回结果存放到寄存器A到寄存器A+B-2中。如果返回的为变长表达式,则B会被设置为0,表示将寄存器A到当前栈顶的所有值返回。

1
return 1;

1
2
3
1    [1]    LOADK        0 -1    ; 1
2 [1] RETURN 0 2
3 [1] RETURN 0 1

RETURN只能从寄存器返回数据,所以第一行LOADK先将常量1装载道寄存器0,然后返回。

1
return ...;

1
2
3
1    [1]    VARARG       0 0
2 [1] RETURN 0 0
3 [1] RETURN 0 1

因为返回的为变长表达式,B为0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
name    args    desc
OP_CLOSURE A Bx R(A) := closure(KPROTO[Bx])
CLOSURE为指定的函数prototype创建一个closure,并将这个closure保存到寄存器A中。Bx用来指定函数prototype的id。

local function f()
end
main <test.lua:0,0> (2 instructions at 0x102a016f0)
0+ params, 2 slots, 1 upvalue, 1 local, 0 constants, 1 function
1 [2] CLOSURE 0 0 ; 0x102a019b0
2 [2] RETURN 0 1
constants (0) for 0x102a016f0:
locals (1) for 0x102a016f0:
0 f 2 3
upvalues (1) for 0x102a016f0:
0 _ENV 1 0

function <test.lua:1,2> (1 instruction at 0x102a019b0)
0 params, 2 slots, 0 upvalues, 0 locals, 0 constants, 0 functions
1 [2] RETURN 0 1
constants (0) for 0x102a019b0:
locals (0) for 0x102a019b0:
upvalues (0) for 0x102a019b0:

上面生成了一个主函数和一个子函数,CLOSURE将为这个索引为0的子函数生成一个closure,并保存到寄存器0中。

1
2
name    args    desc
OP_VARARG A B R(A), R(A+1), ..., R(A+B-2) = vararg

VARARG直接对应’…’运算符。VARARG拷贝B-1个参数到从A开始的寄存器中,如果不足,使用nil补充。如果B为0,表示拷贝实际的参数数量。

1
local a = ...;

1
2
1    [1]    VARARG       0 2
2 [1] RETURN 0 1

上面第一行表示拷贝B-1个,也就是1个变长参数到寄存器0,也就是local a中。

1
f(...);

1
2
3
4
1    [1]    GETTABUP     0 0 -1    ; _ENV "f"
2 [1] VARARG 1 0
3 [1] CALL 0 0 1
4 [1] RETURN 0 1

由于函数调用最后一个参数可以接受不定数量的参数,所以第二行生成的VARARG的B参数为0。

1
2
3
name    args    desc
OP_SELF A B C
R(A+1) := R(B); R(A) := R(B)[RK(C)]

SELF是专门为“:”运算符准备的指令。从寄存器B表示的table中,获取出C作为key的closure,存入寄存器A中,然后将table本身存入到寄存器A+1中,为接下来调用这个closure做准备。

1
a:b();

1
2
3
4
1    [1]    GETTABUP     0 0 -1    ; _ENV "a"
2 [1] SELF 0 0 -2 ; "b"
3 [1] CALL 0 2 1
4 [1] RETURN 0 1

看一下与上面语法等价的表示方法生成的指令:

1
a.b(a);

1
2
3
4
5
1    [1]    GETTABUP     0 0 -1    ; _ENV "a"
2 [1] GETTABLE 0 0 -2 ; "b"
3 [1] GETTABUP 1 0 -1 ; _ENV "a"
4 [1] CALL 0 2 1
5 [1] RETURN 0 1

比使用“:”操作符多使用了一个指令。所以,如果需要使用这种面向对象调用的语义时,应该尽量使用”:”。

-------------本文结束 感谢阅读-------------