lua函數調用

1、問題
和C相比,Lua是一種限制比較鬆散的語言,這個在函數相關的處理中更加明顯。函數能夠有多個參數,函數返回值能夠被賦值給變量列表(Lua manual中的varlist),函數能夠return表達式列表(Lua manual中的explist),這些其實也不是很混亂,問題在於這些特性放在一塊兒的時候就可能有些讓人頭大了。考慮下面的函數實現:
tsecer@harry: cat lua-call.lua
function tsecer(x, y)
return x + x, x - x, x * x + x * x;
end
local l1,l2
l1, g1, l2, g2 = tsecer(1)
 
print(l1, l2, g1, g2)
tsecer@harry:  /home/tsecer/Download/lua-5.3.4/src/lua ./lua-call.lua
2 2 0 nil
tsecer@harry: 
 
是否是感受這個代碼寫的狗屁不通?那就對了!例如,函數tsecer定義的時候有兩個參數(x,y),函數return的表達式列表包涵3個表達式(x + x, x - x, x * x + x * x);在函數調用的地方,但願將結果賦值給4個變量(v1, v2, v3, v4),這個調用和函數的定義沒有一個地方是匹配的,可是這個程序依然能夠運行,這個在C++語言中是不可想象的,而且這個還不是Lua設計的缺陷,而是Lua的一個特性,在 Manual中有明確的說明
When a function is called, the list of arguments is adjusted to the length of the list of parameters, unless the function is a vararg function, which is indicated by three dots ('...') at the end of its parameter list. A vararg function does not adjust its argument list; instead, it collects all extra arguments and supplies them to the function through a vararg expression, which is also written as three dots. The value of this expression is a list of all actual extra arguments, similar to a function with multiple results. If a vararg expression is used inside another expression or in the middle of a list of expressions, then its return list is adjusted to one element. If the expression is used as the last element of a list of expressions, then no adjustment is made (unless that last expression is enclosed in parentheses).
既然這個地方是一個明確的語言特性,Lua在實現的時候應該是作過特殊處理的,具體如何處理,這個是一個比較有意思的問題。因爲函數調用在全部語言中都是最爲重要的一個基本功能,因此接下來嘗試分析下Lua對於這個功能的實現。
2、虛擬機代碼
爲了有一個直觀的認識,下面是對虛擬機指令的一個註釋,其中儘可能詳細解釋了每條指令的意義。雖然這些細節在絕大部分狀況下都是沒必要關注的,可是若是想細緻瞭解下這方面的內容,這些註釋能夠做爲一個備註,在須要的時候翻一下。
tsecer@harry:  /home/tsecer/Download/lua-5.3.4/src/luac -o lua-call.i ./lua-call.lua
tsecer@harry:  /home/tsecer/Download/lua-5.3.4/src/luac -l -l  lua-call.i 
main <./lua-call.lua:0,0> (17 instructions at 0x9ce5f30)
0+ params, 7 slots, 1 upvalue, 2 locals, 5 constants, 1 function
       建立tsecer函數的Closure,結果保存在寄存器0中,操做數0,0分別表示接收寄存器和函數內函數定義編號
1 [3] CLOSURE   0 0 ; 0x9ce60c8
            將建立的Closure保存到全局變量tsecer中,這也就是Manual中說函數定義的syntax sugur,
            也就是function f () body end           被轉換爲f = function () body end的實現
2 [1] SETTABUP  0 -1 0 ; _ENV "tsecer"
            將寄存器編號區間0-1置爲nil,這兩個寄存器分別對應代碼中定義的兩個局部變量 local l1,l2,Lua默認爲置位肯定的nil
3 [4] LOADNIL   0 1
            將tsecer的Closure從TABUP裝載到寄存器2中
4 [5] GETTABUP  2 0 -1 ; _ENV "tsecer" 
            將常量1的值裝載到寄存器3中
5 [5] LOADK     3 -4 ; 1
           函數調用tsecer,2表示函數調用的Closure放在寄存器2中,2表示參數的個數爲2-1=1個(即x),
           5表示返回值接收變量爲5-1=4個(即l1, g1, l2, g2)
6 [5] CALL      2 2 5
 
虛擬機在執行被調用函數的return指令時,會將return中的表達式列表的內容依次賦值到從call第一個參數開始的寄存器中,第6條指令CALL      2 2 5指明的第一個參數爲2,這表示在2這個寄存器中保存的是將要調用函數的Closure,在調用的時候,從該寄存器開始,以後的寄存器保存的是函數參數列表,在被調用函數return的時候,函數的返回值將會保存在從該地址開始的寄存器中。因此這個地方能夠看到的現象是,在函數調用的時候,函數的參數要按照寄存器序號連續的順序賦值,接收函數返回值的時候也要一樣如此。
       
         將寄存器5的內容賦值到全局變量g2中,其中0表示upvalue編號,對應下面upvalues (1) for 0x9ce5f30中第一項,也就是_ENV,-3的負數表示這個是一個常量(K,相對於寄存器R),取反以後對應constants (5) for 0x9ce5f30中第三項,也就是"g2",因此這個指令就是將寄存器5的值裝載到_ENV表中經過"g2"索引得到的變量中
7 [5] SETTABUP  0 -3 5 ; _ENV "g2" 。
           將寄存器4的內容賦值到寄存器1中,也就是l2變量中
8 [5] MOVE      1 4
          將寄存器3的內容賦值到全局變量g1中
9 [5] SETTABUP  0 -2 3 ; _ENV "g1"
          將寄存器2的內容賦值到寄存器1中,也就是l2變量中
10 [5] MOVE      0 2
 
 
                print的定義放入寄存器2中
11 [7] GETTABUP  2 0 -5 ; _ENV "print"
               寄存器0內容移入寄存器3中,其中寄存器0存儲的內容爲l1
12 [7] MOVE      3 0
               寄存器1內容移入寄存器4中,其中寄存器1存儲的內容爲l2
13 [7] MOVE      4 1
                ENV "g1" 全局變量g1的內容放入寄存器5中
14 [7] GETTABUP  5 0 -2 ; _
                全局變量g2的內容放入寄存器6中
15 [7] GETTABUP  6 0 -3 ; _ENV "g2" 
               調用print,寄存器2保存print函數Closure定義,調用參數數量爲5-1=4個,
              返回值接收個數爲1 -1 = 0個,由於沒有人接收print的返回值
16 [7] CALL      2 5 1
             函數沒有主動的執行return,因此這個地方編譯器自動加上一個return指令,
             其中的1表示返回值個數爲1-1=0個,也就是沒有返回值
17 [7] RETURN    0 1 
constants (5) for 0x9ce5f30:
1 "tsecer"
2 "g1"
3 "g2"
4 1
5 "print"
locals (2) for 0x9ce5f30:
0 l1 4 18
1 l2 4 18
upvalues (1) for 0x9ce5f30:
0 _ENV 1 0
 
function <./lua-call.lua:1,3> (7 instructions at 0x9ce60c8)
2 params, 6 slots, 0 upvalues, 2 locals, 0 constants, 0 functions
其中0表示參數x的內容
1 [2] ADD       2 0 0 x + x的結果放入寄存器2中
2 [2] SUB       3 0 0 x - x的結果放入寄存器3中
3 [2] MUL       4 0 0 x * x的結果放入急促請4中
4 [2] MUL       5 0 0 x * x的結果放入寄存器5中
5 [2] ADD       4 4 5 寄存器4(x * x) 和 寄存器5(x * x) 的和放入寄存器4中。
這裏要注意的是其中的結果在寄存器存放的時候始終是連續的,也就是3個返回值依次放入從2到4的寄存器中。接下來的返回指令RETURN    2 4表示從寄存器2開始,返回 4 -1 = 3個值。
6 [2] RETURN    2 4
7 [3] RETURN    0 1
constants (0) for 0x9ce60c8:
locals (2) for 0x9ce60c8:
0 x 1 8
1 y 1 8
upvalues (0) for 0x9ce60c8:
tsecer@harry: 
3、虛擬機對調用(CALL)時參數不一致的處理
一、call指令的虛擬機處理
luaV_execute:vmcase(OP_CALL)===>>>luaD_precall
    case LUA_TLCL: {  /* Lua function: prepare its call */
      StkId base;
      Proto *p = clLvalue(func)->p;
// 這裏的L->top在OP_CALL指令執行的時候已經設置爲L->top = ra+b;因此這個減法至關於還原出b的數值,也就是call指令中第二個參數的值,也就是調用時提供的參數個數
      int n = cast_int(L->top - func) - 1;  /* number of real arguments */
      int fsize = p->maxstacksize;  /* frame size */
      checkstackp(L, fsize, func);
      if (p->is_vararg)
        base = adjust_varargs(L, p, n);
      else {  /* non vararg function */
        for (; n < p->numparams; n++)
          setnilvalue(L->top++);  /* complete missing arguments */
        base = func + 1;
      }
      ci = next_ci(L);  /* now 'enter' new function */
      ci->nresults = nresults;
      ci->func = func;
      ci->u.l.base = base;
      L->top = ci->top = base + fsize;
      lua_assert(ci->top <= L->stack_last);
      ci->u.l.savedpc = p->code;  /* starting point */
      ci->callstatus = CIST_LUA;
      if (L->hookmask & LUA_MASKCALL)
        callhook(L, ci);
      return 0;
    }
二、當調用參數比函數定義參數少時
執行OP_CALL指令的時候,此時已經找到了調用函數的定義,所以也就知道了函數定義中的參數個數,這個值也就是p->numparams,在luaD_precall函數中:
        for (; n < p->numparams; n++)
          setnilvalue(L->top++);  /* complete missing arguments */
語句將調用時缺乏的參數設置爲nil,這也便是manual中說明的函數調用時不足參數補充爲肯定nil值的具體實現。
三、當調用時參數比函數定義的參數多時
這個地方其實不須要作任何處理,多出的參數對於被調用函數來講不可見,沒毛病。
四、被調用函數如何使用寄存器
因爲函數中經過base = func + 1;ci->u.l.base = base;設置了新調用函數的棧幀基地址,而且參數從func開始依次壓入堆棧,因此在被調用函數中經過寄存器0就能夠訪問到調用時壓入的第一個參數,並依次類推。
在解析函數定義時, funcstat===>>>body===>>>parlist===>>>new_localvar,依次爲參數綁定寄存器
static void new_localvar (LexState *ls, TString *name) {
  FuncState *fs = ls->fs;
  Dyndata *dyd = ls->dyd;
  int reg = registerlocalvar(ls, name);
  checklimit(fs, dyd->actvar.n + 1 - fs->firstlocal,
                  MAXVARS, "local variables");
  luaM_growvector(ls->L, dyd->actvar.arr, dyd->actvar.n + 1,
                  dyd->actvar.size, Vardesc, MAX_INT, "local variables");
  dyd->actvar.arr[dyd->actvar.n++].idx = cast(short, reg);
}
並在parlist中爲這些變量保留棧空間。
static void parlist (LexState *ls) {
……
  adjustlocalvars(ls, nparams);
  f->numparams = cast_byte(fs->nactvar);
   luaK_reserveregs(fs, fs->nactvar);  /* reserve register for parameters */
}
4、當函數返回(RETURN) 個數 和接收變量個數不一致時
一、RETURN如何知道接收變量個數
看函數返回的時候,仍是要再回顧下函數調用(CALL),在這個指令中,其實已經指明瞭函數返回值有多少個接收變量,它被編碼到CALL指令的操做數中。虛擬機在執行CALL指令時,luaD_precall函數經過ci->nresults = nresults;將接收變量個數保存在Call Info結構中,在執行RETURN指令時可使用這個信息。
二、接收變量數量從哪裏來
開始例子中接收變量nvars的值爲4,而表達式nexps的數量爲1,因此adjust_assign===>>>luaK_setreturns會從新修改以前call指令編碼中的接收參數個數
static void assignment (LexState *ls, struct LHS_assign *lh, int nvars) {
{  /* assignment -> '=' explist */
    int nexps;
    checknext(ls, '=');
    nexps = explist(ls, &e);
    if (nexps != nvars)
      adjust_assign(ls, nvars, nexps, &e);
    else {
      luaK_setoneret(ls->fs, &e);  /* close last expression */
      luaK_storevar(ls->fs, &lh->v, &e);
      return;  /* avoid default */
    }
  }
……
void luaK_setreturns (FuncState *fs, expdesc *e, int nresults) {
  if (e->k == VCALL) {  /* expression is an open function call? */
     SETARG_C(getinstruction(fs, e), nresults + 1);
  }
  else if (e->k == VVARARG) {
    Instruction *pc = &getinstruction(fs, e);
    SETARG_B(*pc, nresults + 1);
    SETARG_A(*pc, fs->freereg);
    luaK_reserveregs(fs, 1);
  }
  else lua_assert(nresults == LUA_MULTRET);
}
三、虛擬機OP_RETURN指令時不一致的處理
luaD_poscall===>>>moveresults
若是接收參數wanted小於返回值數量,只將須要的參數進行轉移;反過來,若是接收參數多於返回個數,會將有效返回值數量(nres)進行轉移,剩餘的內容使用setnilvalue置空。
    default: {
      int i;
      if (wanted <= nres) {  /* enough results? */
        for (i = 0; i < wanted; i++)  /* move wanted results to correct place */
          setobjs2s(L, res + i, firstResult + i);
      }
      else {  /* not enough results; use all of them plus nils */
        for (i = 0; i < nres; i++)  /* move all results to correct place */
          setobjs2s(L, res + i, firstResult + i);
         for (; i < wanted; i++)  /* complete wanted number of results */
          setnilvalue(res + i);
      }
      break;
這裏其實要注意:這些內容的轉移都是由RETURN指令觸發的,在生成的虛擬機指令中沒有體現。
再次回顧下CALL指令,CALL指令只是指定了接收寄存器組的起始位置,而RETURN指令指明瞭本身返回表達式從哪一個寄存器開始,到哪一個寄存器結束,這個也是一個區間,而這裏的寄存器組之間的轉移是由RETURN指令背後的虛擬機代碼完成。
四、接收變量的轉移
RETURN指令只是將函數返回值賦值到CALL指令指定的(連續)寄存器區間中,可是,接收變量多是局部變量或者全局變量等,這些變量其實已經有官方地址,例如對於l1, g1, l2, g2 = tsecer(1)這樣的指令,在tsecer(1)函數返回以後,還須要將連續寄存器組中的變量逐個賦值給接收變量(l1, g1, l2, g2)中。這個對應指令的生成在函數assignment===>>>luaK_storevar(ls->fs, &lh->v, &e);函數將會根據接收變量進行轉移,這些賦值指令在虛擬機代碼中能夠看到。這一點也不難理解,由於接收變量是由調用函數肯定的,須要各個調用場景本身將函數生成的表達式列表進行手動轉移。
5、如何保證一組編號連續的寄存器
一、一個可能會破壞寄存器編號連續的場景
在前面的流程中,其實有一個重要的隱含限制:要有一組編號連續的寄存器,這一點在CALL函數各個參數、RETURN中返回的表達式列表,接收變量的賦值等均是一個隱含前提。咱們以返回列表爲例來看下這個問題
x + x, x - x, x * x + x * x;
這個地方中要求這三個表達式放在連續的寄存器中,而比較有表明意義的是x * x + x * x,這個地方須要兩個臨時寄存器來存放中間結果,那麼若是這個中間變量佔用了兩個寄存器的話,是不是最終生成的寄存器是不連續的(由於兩個乘積必然要佔用兩個中間寄存器來存放)?下面是生成的機器指令中的內容:
3 [2] MUL       4 0 0 x * x的結果放入急促請4中
4 [2] MUL       5 0 0 x * x的結果放入寄存器5中
5 [2] ADD       4 4 5 寄存器4(x * x) 和 寄存器5(x * x) 的和放入寄存器4中。
能夠看到,的確是同時使用兩個中間變量4和5來保存加法的兩個操做數,可是在保存最終的加法結果的時候仍是保存在了連續的寄存器4中,也就是保證了寄存器的連續。
二、如何規避
對於這裏遇到的算數運算問題,這個邏輯是經過subexpr函數來完成的。
static BinOpr subexpr (LexState *ls, expdesc *v, int limit) {
……
  while (op != OPR_NOBINOPR && priority[op].left > limit) {
    expdesc v2;
    BinOpr nextop;
    int line = ls->linenumber;
    luaX_next(ls);
    luaK_infix(ls->fs, op, v);
    /* read sub-expression with higher priority */
    nextop = subexpr(ls, &v2, priority[op].right);
    luaK_posfix(ls->fs, op, v, &v2, line);
    op = nextop;
  }
……
}
在執行加法運算時luaK_posfix===>>>codebinexpval
static void codebinexpval (FuncState *fs, OpCode op,
                           expdesc *e1, expdesc *e2, int line) {
  int rk2 = luaK_exp2RK(fs, e2);  /* both operands are "RK" */
  int rk1 = luaK_exp2RK(fs, e1);
   freeexps(fs, e1, e2);
  e1->u.info = luaK_codeABC(fs, op, 0, rk1, rk2);  /* generate opcode */
  e1->k = VRELOCABLE;  /* all those operations are relocatable */
  luaK_fixline(fs, line);
}
在這個執行流程中,luaK_exp2RK(fs, e2) 和 luaK_exp2RK(fs, e1) 會爲這些沒有分配臨時寄存器的變量分配接收寄存器(固然若是已經有對應的寄存器的話就不用再分配了,例如x * x 這個運算的的兩個操做數x都是已經有寄存器了,因此不用分配新的臨時寄存器),而後立馬釋放這兩個中間結果使用的臨時變量(一樣,非臨時變量就不用釋放),這樣在爲最終結果分配寄存器以前,全部的中間寄存器都已經釋放。
三、以x * x + x * x爲例來講明寄存器分配
第一個 x * x 經過codebinexpval生成一個VRELOCABLE類型的表達式,後一個  x * x 生成一個VRELOCABLE的表達式,可是這兩個表達式都尚未真正分配臨時寄存器,在執行x * x + x * x時luaK_exp2RK(fs, e2)爲左邊表達式分配第一個空閒寄存器4,luaK_exp2RK(fs, e1)爲第二個VRELOCABLE類型的表達式分配寄存器5,而後freeexps(fs, e1, e2)動態釋放這兩個寄存器(4和5),從而下一個空閒的寄存器就是4,也就是
5 [2] ADD       4 4 5
中最終接收寄存器的值4。按照這個流程,直觀上想是能夠保證最終結果會保存在該表達式執行以前的那個空閒寄存器。固然這個只是一個直觀上的感受,具體是否嚴謹可能有些地方已經有說明,這裏就再也不繼續深刻了。
相關文章
相關標籤/搜索