函数调用过程在lua2.1中的实现异常难读懂,所以这里专门以一篇文章来分析。
已有知识:
比如func1(a, b) body end
func1(1, 2)时依次执行的指令是PUSHGLOBAL index(func1) PUSH1 PUSH2 CALLFUNC 2 1
执行到指令CALLFUNC的时候,栈上自底向上有函数对象f,参数1和参数2.
我们可以用 (栈顶-1-参数数量)得到函数对象的位置。
假设栈顶是位置是4,第二参数的位置是3, 第一个参数的位置是2,那么函数对象的位置就是1。
接着再求一个“base”,base的计算公式是(栈顶-栈底) - 参数数量,上面这个例子中base为(4 - 0) - 2 = 2。
也就是说base含义是表示第一参数相对于栈顶的偏移量(偏移了多少个对象)。
紧接着就是调用函数了,这里会调用do_call函数。
do_call的原型如下:
void do_call(Object * func, StkId base, int nResult, StkId whereRes);
我们看lua2.1对do_call添加的注释:
调用一个函数(C或lua)。
参数必须在栈上,并且在[stack + base, top)域内。
返回值必须也在栈上,在[stack + whereRes, top)域内。
当nResult不等于MULT_RET的时候,表示返回值的数量。
我们再次回到CALLFUNC指令的执行过程。它将whereRes置为base - 1,而base - 1恰好是函数对象f,
这就意味着,函数执行结束之后对象f,函数的参数将不复存在,取而代之的是返回值。显然lua2.1中还可
以有意地将返回值放到其他位置,不过现在暂时还没看到这种使用方法。
do_call函数有主要的两个部分:
第一部分是找到函数对应的指令通过lua_execute或callC函数来执行指令串。这个部分后面再详细说明。
第二部分则是调整返回值:
不管是callC和lua_execute函数,他们都有一个共同点是返回值的含义,都是指第一个返回值的
偏移值。这两个函数虽然有base作为参数传入,但是并非“老老实实”地将返回值放到[stack + whereRes, top)
域内,这主要的原因是他们并不知晓whereRes。do_call函数在目标指令执行完之后将返回值的数量调整到
nResult,多则截断,少则补充nil对象。然后再将调整好的结果移动到以偏移whereRes开始的地方。在移动之后,
栈顶将变成调整之后的返回值中的最后一个返回值的下一个对象。
callC和lua_execute:
首先看函数lua_execute,这个函数其实就是lua2.1的心脏了,真正地去执行指令的就是这个函数。
它产生的结果是在栈顶,假设调用lua_execute之前的base = 1,栈顶top = 4,调用之后top = 10;那么可
以推断参数有3个,返回值有6个,而它向do_call返回的值为1 + 3 = 4。在这里我们可以看出RETCODE0和
RETCODE指令的作用,他们指出了返回值与base之间的偏移量,千万不要误会诚“指出了返回值的数量”。
接着再看函数callC,这个函数是实际上是去调用一个类型为lua_CFunction的C函数,我们先看这个
函数的相关注释:
调用一个C函数,CBase将指向栈顶,CnResult代表参数数量,返回第一个返回值在栈中的偏移。
callC中调用的C函数 func可能会有返回值,这个返回值以CBase为开始,所以返回值为CBase,那么CBase
是否会在函数func执行的时候变化呢,通过查代码我们知道在可供C调用的API中有四个会对CBase进行修改:
lua_getsubscript
lua_createtable
lua_getlocked
lua_getglobal
这为了将栈顶对象整合到栈中,都对top和CBase进行加1操作。那么接下来的问题是为什么要整合呢?这个问题没想好,
这个地方会有一个问题,就是当函数func需要向lua返回一个值并调用了lua_pushnumber(2.1)之后,然后再调用
lua_getglobal,lua会得不到这个值,得到的是一个nil对象。所以在C函数开始向lua返回值的过程中,就不能再
出现这四个函数中的任何一个。并且C函数不能向lua返回一个table(第一,通过lua_getglobal获得的table不能
作为返回值;第二,创建一个表也不能作为返回值,因为它被整合到栈中了)。