浅析lua異常捕獲処理メカニズム

16988 ワード

異常キャプチャは高度な言語の大きな特性であり,異常のキャプチャと処理により,システムの安定性と丈夫性を効果的に向上させることができる.どんなにコードを改善しても、ファイルioエラー、ネットワークエラー、メモリエラーなどの異常は避けられないため、開発者が問題の処理にフォローするためにログを印刷すると同時に、エラーをキャプチャするコードが要求されます.もちろんluaは、ランタイム異常をキャプチャするためのインタフェースも提供します.
lua異常捕獲関数
luaには2つの関数があります.pcallとxpcallです.この2つの関数は似ています.保護モードで関数が実行されます.try-catchに似ています.異常をキャプチャして処理できます.
2つの関数のプロトタイプは次のとおりです.
pcall (func [, arg1, ···])
xpcall (func, errfunc [, arg1, ···])
2つの関数を比較すると、xpcallには異常処理関数パラメータerrfuncが1つ増えた.pcallの場合、例外処理が完了すると、エラー情報のみが簡単に記録され、呼び出しスタック空間が解放されますが、xpcallの場合、このパラメータは呼び出しスタックが解放される前にこれらのデータを追跡するために使用できます.効果は次のとおりです.
> f=function(...) error(...) end
> pcall(f, 123)
false   stdin:1: 123
> xpcall(f, function(e) print(debug.traceback()) return e end, 123)
stack traceback:
        stdin:1: in function <stdin:1>
        [C]: in function 'error'
        stdin:1: in function 'f'
        [C]: in function 'xpcall'
        stdin:1: in main chunk
        [C]: in ?
false   stdin:1: 123
注目すべきは、
errfuncの入力パラメータは異常データであり,pcallと同じ戻り値を実現するには関数終了時にこのデータを返さなければならない.
lua異常捕獲処理メカニズムは以下pcallで説明する
lua異常捕獲はどのように実現されたのか.ここで、luaコードバージョンは5.3.1を取る
pcallの実装pcallのc実装関数はluaB_pcall、次のようにします.
// lbaselib.c
static int luaB_pcall (lua_State *L) {
  int status;
  luaL_checkany(L, 1);
  lua_pushboolean(L, 1);  //        ,  true
  lua_insert(L, 1);  /* put it in place */
  status = lua_pcallk(L, lua_gettop(L) - 2, LUA_MULTRET, 0, 0, finishpcall);
  return finishpcall(L, status, 0);
}

lua_を見てpcallk、これはpcallの前処理関数です.
// lapi.c
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);  //   pcall      
  if (k == NULL || L->nny > 0) {  /* no continuation or no yieldable? */
    c.nresults = nresults;  /* do a 'conventional' protected call */
    /*   pcall(      ,      )*/
    status = luaD_pcall(L, f_call, &c, savestack(L, c.func), func); 
  }
  else {  // 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;  //           
    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;
}

ここでは重点的に
luaD_pcallの実装.これはpcallのコア処理関数です.
// ldo.c 
int luaD_pcall (lua_State *L, Pfunc func, void *u,
                ptrdiff_t old_top, ptrdiff_t ef) {
  int status;
  CallInfo *old_ci = L->ci;
  lu_byte old_allowhooks = L->allowhook;
  unsigned short old_nny = L->nny;
  ptrdiff_t old_errfunc = L->errfunc;
  L->errfunc = ef; //         
  status = luaD_rawrunprotected(L, func, u); //        
  if (status != LUA_OK) {  //       
    StkId oldtop = restorestack(L, old_top); // ‘  ’   
    luaF_close(L, oldtop);  /* close possible pending closures */
    seterrorobj(L, status, oldtop);
    L->ci = old_ci;
    L->allowhook = old_allowhooks;
    L->nny = old_nny;
    luaD_shrinkstack(L);
  }
  L->errfunc = old_errfunc;
  return status;
}

luaDを見てrawrunprotected関数の実装:
// ldo.c 
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);  ); //      
  L->errorJmp = lj.previous;  //        
  L->nCcalls = oldnCcalls;
  return lj.status;
}

LUAI_TRYと記事の後に出てくるLUAI_THROWはいずれもマクロ実装であり,try−catchの構文構造として表現される主流cまたはc++バージョンの異常処理構文を互換化することを目的とする.簡単に言えば、コードを実行する前にtryを行い、実行プロセスエラーが発生するとthrowを行い、catchの場所で異常を処理します.
/*
** LUAI_THROW/LUAI_TRY define how Lua does exception handling. By
** default, Lua handles errors with exceptions when compiling as
** C++ code, with _longjmp/_setjmp when asked to use them, and with
** longjmp/setjmp otherwise.
*/
#if !defined(LUAI_THROW)	 /* { */

#if defined(__cplusplus) && !defined(LUA_USE_LONGJMP)	/* { */

/* C++ exceptions */
#define LUAI_THROW(L,c)	 throw(c)
#define LUAI_TRY(L,c,a) \
 try { a } catch(...) { if ((c)->status == 0) (c)->status = -1; }
#define luai_jmpbuf	 int  /* dummy variable */

#elif defined(LUA_USE_POSIX)	 /* }{ */

/* in POSIX, try _longjmp/_setjmp (more efficient) */
#define LUAI_THROW(L,c)	 _longjmp((c)->b, 1)
#define LUAI_TRY(L,c,a)	 if (_setjmp((c)->b) == 0) { a }
#define luai_jmpbuf	 jmp_buf

#else	 /* }{ */

/* ISO C handling with long jumps */
#define LUAI_THROW(L,c)	 longjmp((c)->b, 1)
#define LUAI_TRY(L,c,a)	 if (setjmp((c)->b) == 0) { a }
#define luai_jmpbuf	 jmp_buf

#endif	 /* } */

#endif	 /* } */

異常が発生するとstatusは-1、すなわち等しくない
LUA_OK
xpcallとpcallの違いの比較の下でxpcallはpcallを比較して、どんな違いがありますか?
// lbaselib.c
static int luaB_pcall (lua_State *L) {
  int status;
  luaL_checkany(L, 1);
  lua_pushboolean(L, 1);  /* first result if no errors */
  lua_insert(L, 1);  /* put it in place */
  status = lua_pcallk(L, lua_gettop(L) - 2, LUA_MULTRET, 0, 0, finishpcall);
  return finishpcall(L, status, 0);
}


static int luaB_xpcall (lua_State *L) {
  int status;
  int n = lua_gettop(L);
  luaL_checktype(L, 2, LUA_TFUNCTION);  /* check error function */
  lua_pushboolean(L, 1);  /* first result */
  lua_pushvalue(L, 1);  //         errfunc      
  lua_rotate(L, 3, 2);  /* move them below function's arguments */
  status = lua_pcallk(L, n - 2, LUA_MULTRET, 2, 2, finishpcall);
  return finishpcall(L, status, 2);
}

振り返るとlua_pcallk 
// lapi.c
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; // pcall  func 0
  else {
    StkId o = index2addr(L, errfunc);
    api_checkstackindex(L, errfunc, o);
    func = savestack(L, o); // xpcall         errfunc
  }

そしてfuncはL->errfuncに値を与え,異常発生時にこの関数をチェックする.
このエラー処理関数はどこで実行されますか?
// ldebug.c
l_noret luaG_errormsg (lua_State *L) {
  if (L->errfunc != 0) {  //           errfunc 
    StkId errfunc = restorestack(L, L->errfunc); 
    setobjs2s(L, L->top, L->top - 1);  /* move argument */
    setobjs2s(L, L->top - 1, errfunc);  /* push function */
    L->top++;  /* assume EXTRA_STACK */
    luaD_call(L, L->top - 2, 1, 0);  /* call it */
  }
  luaD_throw(L, LUA_ERRRUN);
}

luaD_throwは呼び出しです
LUAI_THROW、前のcatchの位置にジャンプ
// ldo.c
l_noret luaD_throw (lua_State *L, int errcode) {
  if (L->errorJmp) {  /* thread has an error handler? */
    L->errorJmp->status = errcode;  /* set status */
    LUAI_THROW(L, L->errorJmp);  /* jump to it */
  }
  else {  /* thread has no error handler */
    global_State *g = G(L);
    L->status = cast_byte(errcode);  /* mark it as dead */
    if (g->mainthread->errorJmp) {  /* main thread has a handler? */
      setobjs2s(L, g->mainthread->top++, L->top - 1);  /* copy error obj. */
      luaD_throw(g->mainthread, errcode);  /* re-throw in main thread */
    }
    else {  /* no handler at all; abort */
      if (g->panic) {  /* panic function? */
        seterrorobj(L, errcode, L->top);  /* assume EXTRA_STACK */
        if (L->ci->top < L->top)
          L->ci->top = L->top;  /* pushing msg. can break this invariant */
        lua_unlock(L);
        g->panic(L);  /* call panic function (last chance to jump out) */
      }
      abort();
    }
  }
}

どちらの場合もこの関数をトリガーします
1、errorをアクティブに呼び出す場合:
// lapi.c
LUA_API int lua_error (lua_State *L) {
  lua_lock(L);
  api_checknelems(L, 1);
  luaG_errormsg(L);
  /* code unreachable; will unlock when control actually leaves the kernel */
  return 0;  /* to avoid warnings */
}

2、ランタイムエラーが発生した場合:
// ldebug.c
l_noret luaG_runerror (lua_State *L, const char *fmt, ...) {
  CallInfo *ci = L->ci;
  const char *msg;
  va_list argp;
  va_start(argp, fmt);
  msg = luaO_pushvfstring(L, fmt, argp);  /* format message */
  va_end(argp);
  if (isLua(ci))  /* if Lua function, add source:line information */
    luaG_addinfo(L, msg, ci_func(ci)->p->source, currentline(ci));
  luaG_errormsg(L);
}

拡張読書
lua stateのnny
lua stateでは、nnyは呼び出しスタックに掛けられない回数を記録し、lua_に定義するState構造:
struct lua_State {
  CommonHeader;
  lu_byte status;
  StkId top;  /* first free slot in the stack */
  global_State *l_G;
  CallInfo *ci;  /* call info for current function */
  const Instruction *oldpc;  /* last pc traced */
  StkId stack_last;  /* last free slot in the stack */
  StkId stack;  /* stack base */
  UpVal *openupval;  /* list of open upvalues in this stack */
  GCObject *gclist;
  struct lua_State *twups;  /* list of threads with open upvalues */
  struct lua_longjmp *errorJmp;  /* current error recover point */
  CallInfo base_ci;  /* CallInfo for first level (C calling Lua) */
  lua_Hook hook;
  ptrdiff_t errfunc;  /* current error handling function (stack index) */
  int stacksize;
  int basehookcount;
  int hookcount;
  unsigned short nny;  /* number of non-yieldable calls in stack */
  unsigned short nCcalls;  /* number of nested C calls */
  lu_byte hookmask;
  lu_byte allowhook;
};

なぜlua stateにこのフィールドが必要なのですか?
これはpcall呼び出し関数にpcallがあり、多層ネストをサポートするため、pcall実行前+1、終了後-1の二次数値を記録する必要があるためである.
/*
** Call a function (C or Lua). The function to be called is at *func.
** The arguments are on the stack, right after the function.
** When returns, all the results are on the stack, starting at the original
** function position.
*/
void luaD_call (lua_State *L, StkId func, int nResults, int allowyield) {
  fprintf(stderr, "luaD_call %d
", allowyield); if (++L->nCcalls >= LUAI_MAXCCALLS) { if (L->nCcalls == LUAI_MAXCCALLS) luaG_runerror(L, "C stack overflow"); else if (L->nCcalls >= (LUAI_MAXCCALLS + (LUAI_MAXCCALLS>>3))) luaD_throw(L, LUA_ERRERR); /* error while handing stack error */ } if (!allowyield) L->nny++; if (!luaD_precall(L, func, nResults)) /* is a Lua function? */ luaV_execute(L); /* call it */ if (!allowyield) L->nny--; L->nCcalls--; }

これにより,nnyの値を判断することで,現在のプロセスが保留されるかどうかを知ることができる.
細心の注意を払うと、
luaD_callはpcallの実行プロセスで呼び出され、
どうしてL->nnyは0より大きいのですか?
これはlua仮想マシン(lua state)の起動時に
L->nnyは1を割り当てます
// lstate.c
LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud) {
  int i;
  lua_State *L;
  global_State *g;
  LG *l = cast(LG *, (*f)(ud, NULL, LUA_TTHREAD, sizeof(LG)));
  if (l == NULL) return NULL;
  L = &l->l.l;
  g = &l->g;
  L->next = NULL;
  L->tt = LUA_TTHREAD;
  g->currentwhite = bitmask(WHITE0BIT);
  L->marked = luaC_white(g);
  preinit_thread(L, g);  //         L->nny   1
  g->frealloc = f;
  g->ud = ud;
  g->mainthread = L;
  g->seed = makeseed(L);
  g->gcrunning = 0;  /* no GC while building state */
  g->GCestimate = 0;
  g->strt.size = g->strt.nuse = 0;
  g->strt.hash = NULL;
  setnilvalue(&g->l_registry);
  luaZ_initbuffer(L, &g->buff);
  g->panic = NULL;
  g->version = NULL;
  g->gcstate = GCSpause;
  g->gckind = KGC_NORMAL;
  g->allgc = g->finobj = g->tobefnz = g->fixedgc = NULL;
  g->sweepgc = NULL;
  g->gray = g->grayagain = NULL;
  g->weak = g->ephemeron = g->allweak = NULL;
  g->twups = NULL;
  g->totalbytes = sizeof(LG);
  g->GCdebt = 0;
  g->gcfinnum = 0;
  g->gcpause = LUAI_GCPAUSE;
  g->gcstepmul = LUAI_GCMUL;
  for (i=0; i < LUA_NUMTAGS; i++) g->mt[i] = NULL;
  if (luaD_rawrunprotected(L, f_luaopen, NULL) != LUA_OK) {
    /* memory allocation error: free partial state */
    close_state(L);
    L = NULL;
  }
  return L;
}

見てごらん
preinit_thread関数:
// lstate.c
/*
** preinitialize a thread with consistent values without allocating
** any memory (to avoid errors)
*/
static void preinit_thread (lua_State *L, global_State *g) {
  G(L) = g;
  L->stack = NULL;
  L->ci = NULL;
  L->stacksize = 0;
  L->twups = L;  /* thread has no upvalues */
  L->errorJmp = NULL;
  L->nCcalls = 0;
  L->hook = NULL;
  L->hookmask = 0;
  L->basehookcount = 0;
  L->allowhook = 1;
  resethookcount(L);
  L->openupval = NULL;
  L->nny = 1; //  L->nny   1
  L->status = LUA_OK;
  L->errfunc = 0;
}

その協程lua_newthreadの場合も呼び出されます
preinit_thread
しかし、両者はpcallの実現には多くの違いがある.
次のように、コパス作成時に呼び出されます.
preinit_thread前処理
コンシステント:
// lstate.c
LUA_API lua_State *lua_newthread (lua_State *L) {
  global_State *g = G(L);
  lua_State *L1;
  lua_lock(L);
  luaC_checkGC(L);
  /* create new thread */
  L1 = &cast(LX *, luaM_newobject(L, LUA_TTHREAD, sizeof(LX)))->l;
  L1->marked = luaC_white(g);
  L1->tt = LUA_TTHREAD;
  /* link it on list 'allgc' */
  L1->next = g->allgc;
  g->allgc = obj2gco(L1);
  /* anchor it on L stack */
  setthvalue(L, L->top, L1);
  api_incr_top(L);
  preinit_thread(L1, g);  //         L->nny   1
  L1->hookmask = L->hookmask;
  L1->basehookcount = L->basehookcount;
  L1->hook = L->hook;
  resethookcount(L1);
  /* initialize L1 extra space */
  memcpy(lua_getextraspace(L1), lua_getextraspace(g->mainthread),
         LUA_EXTRASPACE);
  luai_userstatethread(L, L1);
  stack_init(L1, L);  /* init stack */
  lua_unlock(L);
  return L1;
}

ただし、協程はluaでこのように呼び出されます.
local co = coroutine.create(func)
coroutine.resume(co)

もう一度見て
resumeの実装:
LUA_API int lua_resume (lua_State *L, lua_State *from, int nargs) {
  int status;
  int oldnny = L->nny;  /* save "number of non-yieldable" calls */
  lua_lock(L);
  luai_userstateresume(L, nargs);
  L->nCcalls = (from) ? from->nCcalls + 1 : 1;
  L->nny = 0;  //   nny   0,    
  api_checknelems(L, (L->status == LUA_OK) ? nargs + 1 : nargs);
  status = luaD_rawrunprotected(L, resume, &nargs); //          
  if (status == -1)  /* error calling 'lua_resume'? */
    status = LUA_ERRRUN;
  else {  /* continue running after recoverable errors */
    while (errorstatus(status) && recover(L, status)) { //          
      /* unroll continuation */
      status = luaD_rawrunprotected(L, unroll, &status);
    }
    if (errorstatus(status)) {  /* unrecoverable error? */
      L->status = cast_byte(status);  /* mark thread as 'dead' */
      seterrorobj(L, status, L->top);  /* push error message */
      L->ci->top = L->top;
    }
    else lua_assert(status == L->status);  /* normal end or yield */
  }
  L->nny = oldnny;  //      nny 
  L->nCcalls--;
  lua_assert(L->nCcalls == ((from) ? from->nCcalls : 0));
  lua_unlock(L);
  return status;
}
協程のpcall処理が少し異なることがわかりますが、なぜそうなるのでしょうか.
これは、コヒーレントサスペンション(yield)、リカバリ可能コンテキスト(resume)、pcall実行プロセスにコヒーレントサスペンションがあるため、リストアポイントを記録し、コンテキストからリカバリする必要があるためです.
したがって、ここでは、作成されたコヒーレントに加えて、lua stateのオリジナルのコヒーレントは一時停止されず、通常、luaコードの実行が完了したときにのみ終了することがわかります.
参照先:http://blog.csdn.net/mycwq/article/details/49256003