首页 » 技术文章 » 【源码解读】lua源码分析之编译器部分II(lua编译函数)

【源码解读】lua源码分析之编译器部分II(lua编译函数)

 

介绍完表达式部分,继续介绍下lua编译器对于函数的编译过程。 我们以如下代码为例(注意这里没使用local开头):

function f()
   local a = 1
end

执行statement最终会调用funcstat,源码如下:

static void funcstat (LexState *ls, int line) {
  /* funcstat -> FUNCTION funcname body */
  int needself;
  expdesc v, b;
  luaX_next(ls);  /* skip FUNCTION */
  needself = funcname(ls, &v);
  body(ls, &b, needself, line);
  luaK_storevar(ls->fs, &v, &b);
  luaK_fixline(ls->fs, line);  /* definition `happens' in the first line */
}

首先调用funcname,最终得到v是一个全局变量的表达式(注意还未为其分配寄存器reg)。然后调用body来编译函数体部分。

static void body (LexState *ls, expdesc *e, int needself, int line) {
  /* body ->  `(' parlist `)' chunk END */
  FuncState new_fs;
  open_func(ls, &new_fs);
  new_fs.f->linedefined = line;
  checknext(ls, '(');
  if (needself) {
    new_localvarliteral(ls, "self", 0);
    adjustlocalvars(ls, 1);
  }
  parlist(ls);
  checknext(ls, ')');
  chunk(ls);
  new_fs.f->lastlinedefined = ls->linenumber;
  check_match(ls, TK_END, TK_FUNCTION, line);
  close_func(ls);
  pushclosure(ls, &new_fs, e);
}

首选创建一个FuncState即我们在前面文章说的变量fs。接下来调用parlist,用来为函数参数预留空间(同时会为其预留寄存器):

static void parlist (LexState *ls) {
  /* parlist -> [ param { `,' param } ] */
  FuncState *fs = ls->fs;
  Proto *f = fs->f;
  int nparams = 0;
  f->is_vararg = 0;
  if (ls->t.token != ')') {  /* is `parlist' not empty? */
    do {
      switch (ls->t.token) {
        case TK_NAME: {  /* param -> NAME */
          new_localvar(ls, str_checkname(ls), nparams++); 将参数名保存到proto中的locvar数组里
          break;
        }
        case TK_DOTS: {  /* param -> `...' */
          luaX_next(ls);
#if defined(LUA_COMPAT_VARARG)
          /* use `arg' as default name */
          new_localvarliteral(ls, "arg", nparams++);
          f->is_vararg = VARARG_HASARG | VARARG_NEEDSARG;
#endif
          f->is_vararg |= VARARG_ISVARARG;
          break;
        }
        default: luaX_syntaxerror(ls, "<name> or " LUA_QL("...") " expected");
      }
    } while (!f->is_vararg && testnext(ls, ','));
  }
  adjustlocalvars(ls, nparams);
  f->numparams = cast_byte(fs->nactvar - (f->is_vararg & VARARG_HASARG));
  luaK_reserveregs(fs, fs->nactvar);  /* reserve register for parameters */ 预留寄存器
}

然后调用chunk,这个函数就不再重新介绍了(一个while循环不停的解释lua代码并生成指令到proto的code数组中)。 接下来会调用close_func,该函数主要做一些收尾工作,并且生成OP_RETURN指令,设置LexState(全局编译状态机)中当前正在编译的函数返回到上一层proto。最后调用pushclosure,将函数编译后生成OP_CLOSURE指令(之前有介绍过,定义一个函数时,最终会将生成的OP_CLOSURE指令的位置赋值给相关变量)。

static void pushclosure (LexState *ls, FuncState *func, expdesc *v) {
  FuncState *fs = ls->fs;
  Proto *f = fs->f;
  int oldsize = f->sizep;
  int i;
  luaM_growvector(ls->L, f->p, fs->np, f->sizep, Proto *,
                  MAXARG_Bx, "constant table overflow");
  while (oldsize < f->sizep) f->p[oldsize++] = NULL;
  f->p[fs->np++] = func->f;
  luaC_objbarrier(ls->L, f, func->f);
  init_exp(v, VRELOCABLE, luaK_codeABx(fs, OP_CLOSURE, 0, fs->np-1));
  for (i=0; i<func->f->nups; i++) {
    OpCode o = (func->upvalues[i].k == VLOCAL) ? OP_MOVE : OP_GETUPVAL;
    luaK_codeABC(fs, o, 0, func->upvalues[i].info, 0);
  }
}

pushclosure通过f->p[fs->np++] = func->f赋值将新编译的proto链到自己的子proto数组中,表明当前函数包含该子函数。然后将表达式的值赋值为OP_CLOSURE指令指针,最后设置UPVALS。最后一段设置upvalues部分指令,在lvm.c中解释OP_CLOSURE指令时,会用到该部分代码,其作用是从上往下按照调用层级关系将子函数引用的外部变量正确的设置。upvalues的引用关系如下图所示假设我们定义的代码如下:

function f1()
     local a = 1
     function f2()
          function f3()
              print(a)
          end
     end
end

f3引用了f1中的a,那么a对f3来说是一个外部对象,即一个upval,虽然f2中并没有直接引用a,但也会在f2中生成一个upval的指针,最终该指针指向的local a。生成这种层级引用upval的关系是变量索引过程中实现的,具体代码如下:

static int searchvar (FuncState *fs, TString *n) {
  int i;
  for (i=fs->nactvar-1; i >= 0; i--) {
    if (n == getlocvar(fs, i).varname)
      return i;
  }
  return -1;  /* not found */
}


static void markupval (FuncState *fs, int level) {
  BlockCnt *bl = fs->bl;
  while (bl && bl->nactvar > level) bl = bl->previous;
  if (bl) bl->upval = 1;
}


static int singlevaraux (FuncState *fs, TString *n, expdesc *var, int base) {
  if (fs == NULL) {  /* no more levels? */
    init_exp(var, VGLOBAL, NO_REG);  /* default is global variable */
    return VGLOBAL;
  }
  else {
    int v = searchvar(fs, n);  /* look up at current level */
    if (v >= 0) {
      init_exp(var, VLOCAL, v);
      if (!base)
        markupval(fs, v);  /* local will be used as an upval */
      return VLOCAL;
    }
    else {  /* not found at current level; try upper one */
      if (singlevaraux(fs->prev, n, var, 0) == VGLOBAL)
        return VGLOBAL;
      var->u.s.info = indexupvalue(fs, n, var);  /* else was LOCAL or UPVAL */
      var->k = VUPVAL;  /* upvalue in this level */
      return VUPVAL;
    }
  }
}

singlevaraux是用来查找一个变量的,在碰到表达式时,如果表达式是一个变量,都会调用到该函数,该函数首先在当前函数的局部变量中查找,若无法找到,则进一步在其父proto中查找(递归调用singlevaraux(fs->prev, n, var, 0) ),若在外层某个函数中找到,则返回UPVAL,否则会返回VGLOBAL。递归过程中会调用indexupvalue函数,该函数源码如下:

static int indexupvalue (FuncState *fs, TString *name, expdesc *v) {
  int i;
  Proto *f = fs->f;
  int oldsize = f->sizeupvalues;
  for (i=0; i<f->nups; i++) {
    if (fs->upvalues[i].k == v->k && fs->upvalues[i].info == v->u.s.info) {
      lua_assert(f->upvalues[i] == name);
      return i;
    }
  }
  /* new one */
  luaY_checklimit(fs, f->nups + 1, LUAI_MAXUPVALUES, "upvalues");
  luaM_growvector(fs->L, f->upvalues, f->nups, f->sizeupvalues,
                  TString *, MAX_INT, "");
  while (oldsize < f->sizeupvalues) f->upvalues[oldsize++] = NULL;
  f->upvalues[f->nups] = name;
  luaC_objbarrier(fs->L, f, name);
  lua_assert(v->k == VLOCAL || v->k == VUPVAL);
  fs->upvalues[f->nups].k = cast_byte(v->k);
  fs->upvalues[f->nups].info = cast_byte(v->u.s.info);
  return f->nups++;
}

首先该函数尝试在函数已生成的upvalues数组中查找,若未找到,则生成一个新的upval,并使其指向上层返回的表达式的值。因此f2函数中也会在数组中生成一个upval的索引,保存该upval的名字。同时会在对应的fs中存储上层返回的表达式的值。 f2调用singlevaraux时,被查找的函数为f1, 在f1的局部变量中成功找到了local a,此时返回的表达式k=VLOCAL,而f3调用singlevaraux时,由于在f2中未能找到,因此最终返回给f3的k=VUPVAL(info均为寄存器值),这也是在pushclosure函数中对两种不同方式处理有差别的原因。再回过头来看pushclosure这一部分代码:

for (i=0; i<func->f->nups; i++) {
    OpCode o = (func->upvalues[i].k == VLOCAL) ? OP_MOVE : OP_GETUPVAL;
    luaK_codeABC(fs, o, 0, func->upvalues[i].info, 0);
  }

上述代码确实处理了两种不同类型的值,并生成不同的指令,结合lvm.c对OP_CLOSURE指令的处理逻辑可以理解这部分指令。对OP_CLOSURE指令的执行操作部分代码如下:

case OP_CLOSURE: {
    Proto *p;
    Closure *ncl;
    int nup, j;
    p = cl->p->p[GETARG_Bx(i)];
    nup = p->nups;
    ncl = luaF_newLclosure(L, nup, cl->env);
    ncl->l.p = p;
    for (j=0; j<nup; j++, pc++) {
      if (GET_OPCODE(*pc) == OP_GETUPVAL)
        ncl->l.upvals[j] = cl->upvals[GETARG_B(*pc)];
      else {
        lua_assert(GET_OPCODE(*pc) == OP_MOVE);
        ncl->l.upvals[j] = luaF_findupval(L, base + GETARG_B(*pc));
      }
    }
    setclvalue(L, ra, ncl);
    Protect(luaC_checkGC(L));
    continue;
  }

代码在生成一个闭包时,也需要顺带将闭包引用的对象给处理好,如果碰到是OP_MOVE,则会调用luaF_findupval函数,luaF_findupval尝试在lua_state的openupval链表中查找,若未找到该upval,则会新建一个upval,并将该upval与local变量进行关联;如果是upval,则直接将当前函数闭包中的对应upval数组项设置到待生成的闭包中即可。因此当执行函数f1的OP_CLOSURE f2时,将执行OP_MOVE指令,在lua_state的openupval链表中新建一个节点,并将该变量的v指针指向a,在closure f2闭包中的数组保存新建的upval指针;当执行函数f2的OP_CLOSURE f3时,将执行OP_GETUPVAL指令,直接将upvals数组项进行复制即可,此时f3也存在对openupval节点的指针了。对该部分如果需要加深理解,可以继续查看OP_GETUPVAL及OP_SETUPVAL相关处理。同时,还可以关注当离开某个作用域时,调用luaF_close对于lua_state的openupval的调整(会将变量的值拷贝到openupval中的节点中)。 到这里,基本上对于函数部分的编译过程有一个大概的了解了,编译完的函数,其实最终返回给变量的值是一个指令地址,即OP_CLOSURE指令的地址。

原文链接:【源码解读】lua源码分析之编译器部分II(lua编译函数),转载请注明来源!

0