上一篇记录了local变量的解析过程,今天整理下函数funciton在虚拟机的解析和调用过程;
主要目标:函数怎么存放,参数怎么存放,怎么执行函数和传递参数,返回值怎么传递。
来个简单的例子 ,在lua命令行输入如下代码
local myfun = function(a,b)
return a + b
end
一般来说虚拟机都是阻塞在读取输入的地方,当输入上面代码后,接下来看下函数在虚拟机里面会变成什么样子。
先介绍function在虚拟机的存储结构:FuncState 这里用fs表示,挑几个重要的讲下,这东西跟luaState概念类似(状态机???)
typedef struct FuncState{
Proto * f ; //类似c++代码段+函数栈,真正存储字节码和栈变量的地方
Table* h ; //哈希表,用于快速查找常量所在数组(f->k[])对应的下标index
int pc; //指向当前需要执行的字节码,类似于指令寄存器 ip,每执行一条+1
int freereg; //当前空闲的栈序号,从0开始,每入栈一个常量变量等,freereg+1,出栈-1
short nlocalvars; //指示在f->locvars[]数组中有多少个元素(这个数组存局部变量)
......其他的用到再说
}FuncState;
Proto* f在这里也介绍下:
typedef struct Proto{
TValue *k ; //保存常量用的数组(向量)
int sizek;
Instruction *code; //字节码真正保存处,fs->pc指向要执行的指令处
struct LocVar *locvars; //LocVar仅保存变量名和生命周期起始点和终点,用数组下标对应栈位置
int sizelocars; //仅表示locvars的分配的空间,变量个数在fs->nlocvars中保存
int numparams; //参数个数 function(a,b)中的a,b,会优先入栈占领1,2两个位置
。。。。。//其他留待后续研究
}Proto;
1、解析准备过程略过,直接进入statement函数,local对应的是 TK_LOCAL(词法单元还是语法单元?不必深究,管他呢)
case TK_LOCAL:{
luaX_next(ls); //解析local后面的一个元素,这里是myfun,对应TK_NAME
if(testnext(ls,TK_FUNCTION)
localfunc(ls)
else
localstat(ls);//局部变量,上一篇介绍过了
}
这里进入localstat(ls), 局部变量上一篇介绍过,这里略过,大致过程如下:用new_localvar,生成一个变量,并将freereg+1,表示占用一个栈位置,其实里面什么都没有,等到解析完(执行完?这里是模拟执行过程) "="后面的才是真正赋值。
static void localstat(LexState *ls) //LexState今天研究重点不在这,大致看成输入源和解析缓存
{
.......
do
{
new_localvar(ls,str_checkname(ls),nvars++);
}while(testnext(ls,",)); //local变量定义可一次定义多个,用“,”隔开,如local a,b,c = 1,2,3
//注意完成上面过程后 fs -> f->locvars[0].name = "myfun", fs->nlocvars = 1 了
//其实是TString类型的,这里直接用字符串"myfun"方便理解了
if (testnext(ls,"=")) //赋值表达式
nexps = explist1(ls,&e); //读取"="右边的数值列表,本次示例仅一个function(a,b)的定义
else
{
....
//仅占位,不赋值,如 local a,b,c
}
adjuxt_assign(ls,nvars,nexps,&e); //占用栈空间,调整freereg的数值
....
}
2、接着深入explist1(ls, &e),看看function是怎解解析的
explist1(ls,e)
{
.....
expr(ls, e)
{
subexpr(ls, e)
{
....
simpleexp(ls, e); //经过层层调用,做种进入这个函数
}
}
}
进过一系列的追踪后,发现进入了simpleexp(ls,e)函数,一般来说赋值,函数都是进入这个解析函数中
static void simpleexp(LexState* ls, expdesc * v)
{
switch(ls->t.token) //每个token都是解析到的一个单独的元素,如TK_NAME, TK_LOCAL等
{
.....
case TK_FUNCTION:
{
luaX_next(ls); //读取后面的“(”,返回
body(ls, v, 0 , ls->linenumber); //终于进入我们想要去的地方了,前面都是一直在函数外面打转转
}
.......
}
}
3、正式进入函数解析 body
static void body(LexState* ls, expdesc *e, int needSelf, int line)
{
FuncState fs; //圈起来,要考, 这就是我们一开始介绍的函数保存的形式了
open_func(ls, &fs); //给fs->f指针new一个Proto,各种变量赋初值如,fs->freereg = 0,
//fs->pc = 0; fs->nlocvars = 0; fa->h = newTable等,最后将
//fs->h压入栈,将fs->f压入栈(解析完会不会出栈我们拭目以待)
//其中fs->f->souce保存了源代码文件名,可用于调试使用
//注意,这里ls->fs已经是刚才圈起来的FuncState,不是一开始用的那个了
//ls->fs是链表结构,链表头已经换新了
checknext(ls,'('); //左括号则读入下一个token(这里是a),否则报错
parlist(ls); //解析参数列表,直到遇到右括号')';这里稍微展开一下
---------------------------------------------------------------
parlist展开
{
if(token != ')')
do
{
cast TK_NAME:
{
new_locvar(ls, str_checkname(ls), nparams++)
//熟悉的味道,只不过ls-fs换成了新的FuncState了
//表示给ls->fs 创建变量,占用当前函数的freereg(每个函数单独的栈)
break;
}
}while(token !=')');
//解析完 a,b两个参数后, fs->nlocvars = 2, freereg = 0(还未调整),
//fs->f->locvars[] = { "a", "b"}, fs->h[] = { {'a',1},{'b',1}}
//当然,fs->h中的a,b需要哈希成对应的key保存,这里仅用于好理解
//且value仅赋值为bool(1)类型,以防垃圾回收,如果是常量,则需存对应fs->k[]下标
//这个下标是常量组成字节码的一部分
。。。。
luaK_reserveregs(fs.fs->nactvar); // //里面仅是简单的 freereg += 2 这样
//子,表示a,b占用了两个栈层
}
----------------------------------------------------------------
checknext(ls,')'); //确保是又括号,然后读入下一个token,这里是“return”
chunk(ls); //进入函数内部,然后又进入刚开始的statment(ls),用于解析内部元素
----------------------------------------------------------------------
chunk(ls)展开:
。。。。。
while(!islast)
{
islast = statement(ls); //每次解析一行?
}
----------------------------------------------------------------------
}
4、接下来我们回到了statement(ls)中,追踪下一步
static int statment(LexState * ls)
{
int line = ls->linenumber; //当前所解析的行,在源码文件中的第几行
switch(ls->t.token) //读入的是return a+b中的return
{
。。。
case TK_RETURN:
retstat(ls);
--------------------------------------------------------------------
retstat展开:
{
fs = ls->fs
luaX_next(ls); //读取下一个token, "a+b"中的 a
...
nret = explist1(ls,&e);//好像我们来过?详见前面的 第二步
}
--------------------------------------------------------------------
return 1; //表示已经结束
......
}
}
5、经过跟第二步一样的过程,我们又来到了simpleexp(ls)这里;这里就有点不一样了
static void simpleexp(LexState* ls, expdesc * v)
{
switch(ls->t.token) //每个token都是解析到的一个单独的元素,如TK_NAME, TK_LOCAL等
{
.....
default:
{
primaryexp(ls,v);//这次没有进入TK_FUNCITON,而是TK_NAME ,进入default了,
return; //解析完 a+b直接返回
}
.......
}
}
6、 进入primaryexp:
static void primaryexp( LexState* ls, expdesc* v)
{
fs = ls->fs;
prefixexp(ls,v); //解析当前token,并读入下一个token,是“+”号
----------------------------------------------------
展开{
switch(token)
{
case TK_NAME:
singlevar(ls,v);
-----------------------------------------
展开:
{
singevaraaux(fs, varname, v ,1);//varname就是刚读入的'a'
//这个函数会调用searchvar搜索a,搜到后将变量在fs->f->locvars[]中
//的下标保存在v结构中,并返回VLOCAL,表示这是局部变量,在第几个
//栈位置上,在这是0
}
-----------------------------------------
return;
}
}
-----------------------------------------------------
//经过上面的函数后v已经是( k= VLOCAL, info= 0 )的状态了
for(;;)
{
Switch(token){
。。。
进入default: return;
:
}
}
7、从primaryexp(ls,v);返回后,返回到第4的explist1->expr->subexpr中
explist1(ls,e)
{
.....
expr(ls, e)
{
subexpr(ls, e)
{
....
simpleexp(ls, e);
//经过一系列追踪,刚从simpleexp中返回
op = getbinopr(token); //这里是“+”号,返回 OPR_ADD
while(op != OPR_NONE)
{
。。。。
luaX_next(ls); //读入下一个token, “b”
luaK_infix(fs, op, v);
------------------------------------------------------
luaK_infix展开:
。。。
case OPR_ADD:
luaK_exp2RK(fs, e); //终于要生成第一条字节码指令了
--------------------------------------------
篇幅不够,简单说来,就是生成了OP_ADD,a,b指令
然后将指令保存在fs->f->code[pc]中,pc++,同时保存该指
令对应的源文件行号 f->lineinfo[pc] = line
后面的大概就是生成一条OP_RETURN指令。。。
再开一篇介绍后面的步骤吧
--------------------------------------------
------------------------------------------------------
}
}
}
}