Lua函數執行流程及函數延續點問題的研究



第一:Lua函數調用的總體流程

     在保護性調用的情況下,lua中函數調用的流程如下,非保護性調用的流程更加簡單,請追蹤lua_call函數


int docall (lua_State *L, int narg, int nres) | —— int lua_pcallk (lua_State *L, int nargs, int nresults, int errfunc ...) | —— luaD_pcall (lua_State *L, Pfunc func, void *u,ptrdiff_t old_top, ptrdiff_t ef) | -- luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud) | -- void f_call (lua_State *L, void *ud) | -- void luaD_call (lua_State *L, StkId func, int nResults, int allowyield) | -- int luaD_precall (lua_State *L, StkId func, int nresults) | -- luaD_poscall
|
---...

第二:函數調用的方式和異常處理

      可以看到 luaD_rawrunprotected 函數調用的實際上是 f_call,真正調用的函數在f_call中被調用,封裝這一層的意義就是為了實現保護性調用。保護性調用的情況下lua虛擬機使用lua_longjmp為函數實現堆棧續傳功能,也就是當錯誤發生的時候,在Lua內部能夠最終跳轉到調用點繼續向下執行。所有使用luaD_rawrunprotected函數的的調用都不會因為錯誤直接導致程序退出,而是回到調用點然后將狀態返回給外層邏輯處理。 

//保護性調用
int luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud) {
  unsigned short oldnCcalls = L->nCcalls;
  struct lua_longjmp lj;
  lj.status = LUA_OK;
  lj.previous = L->errorJmp;  /* chain new error handler */
  L->errorJmp = &lj;
  LUAI_TRY(L, &lj,
    (*f)(L, ud); 當f函數調用出異常會回到這里繼續向下走
  );
  L->errorJmp = lj.previous;  /* restore old error handler */
  L->nCcalls = oldnCcalls;
  return lj.status;
}

    對於Lua而言,只有LUA_YIELD是被視為可恢復的異常  #define errorstatus(s)  ((s) > LUA_YIELD),對於其他的錯誤就要報錯了。

    其實對於調用一個函數,無論是lua函數還是c函數,可以使用lua_pacall(lua_call):這種方式的調用我們可以看到,在調用到 luaD_call 這個流程是,allowyield傳的是0,也就是說是不允許掛起的,因此如果你在函數中如果使用了yield相關的函數試圖掛起程序時候,再lua_yieldk中會報錯:attempt to yield from outside a coroutine。因此我是不是可以理解為,如果你需要在函數中yield,就不能通過lua_pcall和lua_call的形式發起函數調用。當然還是有一種形式是使用lua_resume發起函數調用:我們知道resume的功能是喚醒一個掛起的線程(coroutine),當第一次調用的時候他只是簡單的執行函數體,只有在之前有過yield掛起的記錄之后再次調用resume才具備恢復線程的功能,這種方式是允許函數讓出線程(yield掛起)的,下面會介紹到。

LUA_API int lua_yieldk (lua_State *L, int nresults, lua_KContext ctx,
                        lua_KFunction k) {
  CallInfo *ci = L->ci;
  luai_userstateyield(L, nresults);
  lua_lock(L);
  api_checknelems(L, nresults);
  if (L->nny > 0) {
    if (L != G(L)->mainthread)
      luaG_runerror(L, "attempt to yield across a C-call boundary");
    else
      luaG_runerror(L, "attempt to yield from outside a coroutine");  //這里報錯!!
  }
  L->status = LUA_YIELD;
  ci->extra = savestack(L, ci->func);  /* save current 'func' */
  if (isLua(ci)) {  /* inside a hook? */
    api_check(L, k == NULL, "hooks cannot continue after yielding");
  }
  else {
    if ((ci->u.c.k = k) != NULL)  /* is there a continuation? */
      ci->u.c.ctx = ctx;  /* save context */
    ci->func = L->top - nresults - 1;  /* protect stack below results */
    luaD_throw(L, LUA_YIELD);
  }
  lua_assert(ci->callstatus & CIST_HOOKED);  /* must be inside a hook */
  lua_unlock(L);
  return 0;  /* return to 'luaD_hook' */
}

       歸納一下上面的內容:如果你調用的是不會掛起線程的函數體或者函數塊,使用lua_pcall(lua_call)以及lua_resume都能夠正常執行函數,如果函數體中含有掛起線程的流程,必須使用lua_resume發起函數調用。

 第三:函數調用的核心函數

      lua_precall是函數調用的前半部分,lua_postcall顧名思義對應函數調用的后半部分。如果調用的是C函數,那么在lua_precall中調整調整就直接調用了,然后直接調用lua_postcall函數調用就算結束了,然而如果是lua函數,需要交給lua虛擬機執行指令集調用,因此lua_precall只是將堆棧調整妥當,等到lvm執行完畢之后在執行lua_postcall,調整返回值。

 int luaD_precall (lua_State *L, StkId func, int nresults) {
   lua_CFunction f;
   CallInfo *ci;
   int n;  /* number of arguments (Lua) or returns (C) */
   ptrdiff_t funcr = savestack(L, func);
   switch (ttype(func)) {
     case LUA_TLCF:  /* light C function */
       f = fvalue(func);
       goto Cfunc;
     case LUA_TCCL: {  /* C closure */
       f = clCvalue(func)->f;
      Cfunc:
       luaC_checkGC(L);  /* stack grow uses memory */
       luaD_checkstack(L, LUA_MINSTACK);  /* ensure minimum stack size */
       ci = next_ci(L);  /* now 'enter' new function */  //新創建調用鏈,將調用信息錄入
       ci->nresults = nresults;
       ci->func = restorestack(L, funcr);
       ci->top = L->top + LUA_MINSTACK;
       lua_assert(ci->top <= L->stack_last);
       ci->callstatus = 0;
       if (L->hookmask & LUA_MASKCALL)
         luaD_hook(L, LUA_HOOKCALL, -1);
       lua_unlock(L);
       n = (*f)(L);  /* do the actual call */    //如果是c閉包函數或者c函數,則直接調用
       lua_lock(L);
       api_checknelems(L, n);
       luaD_poscall(L, L->top - n, n);           //調整堆棧
       return 1;
     }
     case LUA_TLCL: {  /* Lua function: prepare its call */
       StkId base;
       Proto *p = clLvalue(func)->p;
       n = cast_int(L->top - func) - 1;  /* number of real arguments */
       luaC_checkGC(L);  /* stack grow uses memory */
       luaD_checkstack(L, p->maxstacksize);
       for (; n < p->numparams; n++) //如果函數定義的參數個數大於實際的參數個數,則用nil值補足 (可以看出來越靠后的參數越靠近棧頂部)
         setnilvalue(L->top++);  /* complete missing arguments */
       if (!p->is_vararg) {   //非缺省參數的函數 函數定義中不帶 ...
         func = restorestack(L, funcr);
         base = func + 1;
       }
       else { //帶缺省參數的函數,函數定義中帶 ...
         base = adjust_varargs(L, p, n);
         func = restorestack(L, funcr);  /* previous call can change stack */
       }
       ci = next_ci(L);  /* now 'enter' new function */
       ci->nresults = nresults;
       ci->func = func;
       ci->u.l.base = base;
       ci->top = base + p->maxstacksize;
       lua_assert(ci->top <= L->stack_last);
       ci->u.l.savedpc = p->code;  /* starting point */
       ci->callstatus = CIST_LUA;
       L->top = ci->top;
       if (L->hookmask & LUA_MASKCALL)
         callhook(L, ci);
       return 0;
     }
     //元表驅動的函數調用,"call": 函數調用操作 func(args)。 當 Lua 嘗試調用一個非函數的值的時候會觸發這個事件 (即 func 不是一個函數)。 查找 func 的元方法, 如果找得到,就調用這個元方法, func 作為第一個參數傳
 入,原來調用的參數(args)后依次排在后面。
     default: {  /* not a function */
       luaD_checkstack(L, 1);  /* ensure space for metamethod */
       func = restorestack(L, funcr);  /* previous call may change stack */
       tryfuncTM(L, func);  /* try to get '__call' metamethod */
       return luaD_precall(L, func, nresults);  /* now it must be a function */
     }
   }
 }                                                                                                                                                                                          

  lua_postcall主要是調整函數調用后的堆棧,特別是調整返回值和函數調用鏈,代碼描述還是挺清楚的。

int luaD_poscall (lua_State *L, StkId firstResult, int nres) {
  StkId res;
  int wanted, i;
  CallInfo *ci = L->ci;
  if (L->hookmask & (LUA_MASKRET | LUA_MASKLINE)) {
    if (L->hookmask & LUA_MASKRET) {
      ptrdiff_t fr = savestack(L, firstResult);  /* hook may change stack */
      luaD_hook(L, LUA_HOOKRET, -1);
      firstResult = restorestack(L, fr);
    }
    L->oldpc = ci->previous->u.l.savedpc;  /* 'oldpc' for caller function */
  }
  res = ci->func;  /* res == final position of 1st result */
  wanted = ci->nresults;
  L->ci = ci->previous;  /* back to caller */
  /* move results to correct place */
  for (i = wanted; i != 0 && nres-- > 0; i--)
    setobjs2s(L, res++, firstResult++);
  while (i-- > 0)
    setnilvalue(res++);
  L->top = res;
  return (wanted - LUA_MULTRET);  /* 0 iff wanted == LUA_MULTRET */
}

  第四:關於續傳函數的使用

     上面提到了lua中函數調用的異常處理,依賴於ljmp進行異常恢復,但是如果調用鏈中在c函數中掛起,那么再次使用lua_resume試圖恢復調用棧的時候,C中的堆棧已經丟失了。通俗點講就是:你在一個函數A中yield,函數B中第一次resume開始執行A函數,當遇到yield時候調用流程被打斷,線程被掛起,當你再次調用resume的時候,你希望的是回到A函數中繼續執行A在yield函數下面的代碼段,但是這個是做不到的,因為C的堆棧在Lua虛擬機中已經無從查找了!因此lua提供了續點函數來間接處理這個難題,你可以在lua_pcallk或者lua_callk中傳入一個k函數,也就是續點函數,當你的調用中某個yield被resume喚醒的時候,由於並不能夠回到這個C函數中繼續執行,但是他回到你提供的k函數,讓你作為一個中間的跳板做一下事情!這就是續點函數。lua_pcallk和lua_callk函數不能在最外層調用的,還是上面提到的這個問題,最外層的函數調用如果不是用lua_resume發起的話就會出現上面提到的錯誤。其實這個也好理解,因為你的函數中含有yield相關的代碼段,因此你的function就是allowyield的,但是通過lua_pcallk和lua_callk實際上調用的都是luaD_call不允許allowyield的版本。

LUA_API void lua_callk (lua_State *L, int nargs, int nresults,
                        lua_KContext ctx, lua_KFunction k) {
  StkId func;
  lua_lock(L);
  api_check(L, k == NULL || !isLua(L->ci),
    "cannot use continuations inside hooks");
  api_checknelems(L, nargs+1);
  api_check(L, L->status == LUA_OK, "cannot do calls on non-normal thread");
  checkresults(L, nargs, nresults);
  func = L->top - (nargs+1);
  if (k != NULL && L->nny == 0) {  /* need to prepare continuation? */
    L->ci->u.c.k = k;  /* save continuation */
    L->ci->u.c.ctx = ctx;  /* save context */
    luaD_call(L, func, nresults, 1);  /* do the call */  //yield版本
  }
  else  /* no continuation or no yieldable */
    luaD_call(L, func, nresults, 0);  /* just do the call */   //notyield版本
  adjustresults(L, nresults);
  lua_unlock(L);
}

  也許大家會有疑問,我傳入了k函數,為什么不是調用yield版本,原因就在於L->nny這個值在luaState初始化的時候就不是0而是1,因此總會進noyield的版本。而用lua_resume的時候發起函數調用的時候,在lua_resume這個函數一開始就將L->nny重置為0,所以在lua_resume的外層保護下,lua_pcallk和luacallk能夠順利進入yield版本。   

//這里已經調整好參數和函數位置, p3,p2,p1,func.errfunc 為棧上從上而下的排布
LUA_API int lua_pcallk (lua_State *L, int nargs, int nresults, int errfunc,
                        lua_KContext ctx, lua_KFunction k) {
  struct CallS c;
  int status;
  ptrdiff_t func;
  lua_lock(L);
  api_check(L, k == NULL || !isLua(L->ci),
    "cannot use continuations inside hooks");
  api_checknelems(L, nargs+1);
  api_check(L, L->status == LUA_OK, "cannot do calls on non-normal thread");
  checkresults(L, nargs, nresults);
  if (errfunc == 0)
    func = 0;
  else {
    StkId o = index2addr(L, errfunc);
    api_checkstackindex(L, errfunc, o);
    func = savestack(L, o);
  }
  c.func = L->top - (nargs+1);  /* function to be called */  //指向函數位置
  if (k == NULL || L->nny > 0) {  /* no continuation or no yieldable? */
    c.nresults = nresults;  /* do a 'conventional' protected call */
    status = luaD_pcall(L, f_call, &c, savestack(L, c.func), func); //調用f_call
  }
  else {  /* prepare continuation (call is already protected by 'resume') */
    CallInfo *ci = L->ci;
    ci->u.c.k = k;  /* save continuation */
    ci->u.c.ctx = ctx;  /* save context */
    /* save information for error recovery */
    ci->extra = savestack(L, c.func);
    ci->u.c.old_errfunc = L->errfunc;
    L->errfunc = func;
    setoah(ci->callstatus, L->allowhook);  /* save value of 'allowhook' */
    ci->callstatus |= CIST_YPCALL;  /* function can do error recovery */
    luaD_call(L, c.func, nresults, 1);  /* do the call */
    ci->callstatus &= ~CIST_YPCALL;
    L->errfunc = ci->u.c.old_errfunc;
    status = LUA_OK;  /* if it is here, there were no errors */
  }
  adjustresults(L, nresults);
  lua_unlock(L);
  return status;
}

  下面是一個測試代碼用於驗證上面的結論,注釋部分是不可運行因為外層直接使用lua_pcallk進行函數調用。

#include <stdio.h>
#include <string.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <dlfcn.h>
#include <math.h>

static int cont(lua_State *L, int status, lua_KContext ctx) {
  printf("error occurred!!\n");
  return 0;
}

static int pcall_test(lua_State *L) {
  return lua_yield(L,0);
}

static int mytest(lua_State *L) {
  printf("mytest\n");
  lua_pushcfunction(L, pcall_test);
  int ret = lua_pcallk(L, 0, 0, 0, 0, cont);
  return 1;
}

int main(void) {
  lua_State *L = luaL_newstate();
  luaL_openlibs(L);
  lua_pushcfunction(L, mytest);

  //lua_pushcfunction(L, pcall_test);
  //lua_callk(L, 0, 0, 0, cont);
  /*if(ret != 0)
    {
    const char* err = luaL_checkstring(L, -1);
  //err : attempt to yield from outside a coroutine
  printf("%s\n", err);
  }*/

  //lua_resume(L, NULL, 0);

  int ret = lua_resume(L, NULL, 0);
  if((ret!=LUA_OK) && (ret!=LUA_YIELD))
  {
    const char* err = luaL_checkstring(L, -1);
    printf("%s\n", err);
    return;
  }
  ret = lua_resume(L, NULL, 0);
  
  lua_close(L);
  return 0;
} 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM