<p>Lua支持两种形式的函数调用,一种对调用过程的堆栈进行保护,即使中间过程出错,也不至于让进程退出,也就是pcall,一般在使用C调用Lua写的脚本函数时,都采用pcall方式。</p>

对比起一般的函数调用方式,pcall多做了这些事情:

对函数调用前的Lua堆栈进行保护在调用完毕之后恢复,支持传入出错时的函数在调用出错时调用。

来依次看这个过程。

  1. 首先看入口函数lua_pcall:
LUA_API int lua_pcall (lua_State *L, int nargs, int nresults, int errfunc) {
  struct CallS c;
  int status;
  ptrdiff_t func;
  lua_lock(L);
  api_checknelems(L, nargs+1);
  checkresults(L, nargs, nresults);
  if (errfunc == 0)
    func = 0;
  else {
    StkId o = index2adr(L, errfunc);
    api_checkvalidindex(L, o);
    func = savestack(L, o);
  }
  c.func = L->top - (nargs+1);  /* function to be called */
  c.nresults = nresults;
  status = luaD_pcall(L, f_call, &c, savestack(L, c.func), func);
  adjustresults(L, nresults);
  lua_unlock(L);
  return status;
}

lua_pcall函数与lua_call相比,多了第四个参数,该函数用于传入错误处理函数在Lua栈中的地址。所以第一步将根据传入的参数得到它的值在函数栈中的地址。然后根据这些参数调用函数luaD_pcall函数。

  1. luaD_pcall的实现
int luaD_pcall (lua_State *L, Pfunc func, void *u,
                ptrdiff_t old_top, ptrdiff_t ef) {
  int status;
  unsigned short oldnCcalls = L->nCcalls;
  ptrdiff_t old_ci = saveci(L, L->ci);
  lu_byte old_allowhooks = L->allowhook;
  ptrdiff_t old_errfunc = L->errfunc;
  L->errfunc = ef;
  status = luaD_rawrunprotected(L, func, u);
  if (status != 0) {  /* an error occurred? */
    StkId oldtop = restorestack(L, old_top);
    luaF_close(L, oldtop);  /* close eventual pending closures */
    luaD_seterrorobj(L, status, oldtop);
    L->nCcalls = oldnCcalls;
    L->ci = restoreci(L, old_ci);
    L->base = L->ci->base;
    L->savedpc = L->ci->savedpc;
    L->allowhook = old_allowhooks;
    restore_stack_limit(L);
  }
  L->errfunc = old_errfunc;
  return status;
}

这个函数首先将一些需要保存以便以后进行错误恢复的值保存,然后调用函数luaD_rawrunprotected。

  1. 在luaD_rawrunprotected中,
int luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud) {
  struct lua_longjmp lj;
  lj.status = 0;
  lj.previous = L->errorJmp;  /* chain new error handler */
  L->errorJmp = &lj;
  LUAI_TRY(L, &lj,
    (*f)(L, ud);
  );
  L->errorJmp = lj.previous;  /* restore old error handler */
  return lj.status;
}

可以看到的是,在Lua中,涉及到这些错误恢复的数据,实际上形成一个链条关系,这个函数首先将之前的错误链保存起来。而LUAI_TRY这个宏,会根据不同的编译器进行实现,比如C++中使用的try…catch,C中使用longjmp等。

  1. 再来看看真正出错的时候是如何处理的。
void luaG_errormsg (lua_State *L) {
  if (L->errfunc != 0) {  /* is there an error handling function? */
    StkId errfunc = restorestack(L, L->errfunc);
    if (!ttisfunction(errfunc)) luaD_throw(L, LUA_ERRERR);
    setobjs2s(L, L->top, L->top - 1);  /* move argument */
    setobjs2s(L, L->top - 1, errfunc);  /* push function */
    incr_top(L);
    luaD_call(L, L->top - 2, 1);  /* call it */
  }
  luaD_throw(L, LUA_ERRRUN);
}

首先如果之前保存的errfunc不为空,则首先从Lua栈中得到该函数,如果判断这个地址存放的不是一个函数则直接抛出错误。否则将错误参数压入栈中调用该错误处理函数。最后调用LuaD_throw函数,这个函数与前面的LUAI_TRY宏是对应的。这样就可以回到原来保存的错误恢复地点,恢复调用前的Lua栈,继续执行下去,而不是导致宿主进程退出。