lua的类型,lua_State,函数调用,协程

数据类型

lua中的数据可以这样分为两位:值类型和引用类型。引用类型创建时需要从堆上分配内存,复制时只需要复制指针,分配的内存由GC负责维护生命期。

所有lua类型都用一个union来表示:

c 复制代码
/*
** Union of all Lua values
*/
typedef union {
  GCObject *gc;
  void *p; /* lightuserdata */
  lua_Number n;
  int b; /* boolean */
} Value;

引用类型用一个gc指针来引用,其他值类型都直接保存在Value中。

每个引用类型的开头都是CommonHeader;,所以都可强转成GCheader*使用:

c 复制代码
/*
** Common Header for all collectable objects (in macro form, to be
** included in other objects)
*/
#define CommonHeader GCObject *next; lu_byte tt; lu_byte marked

/*
** Common header in struct form
*/
typedef struct {
  CommonHeader;
} GCheader;

/*
** Union of all collectable objects
*/
union GCObject {
  GCheader gch;
  TString ts;
  Udata u;
  Closure cl;
  Table h;
  Proto p;
  UpVal uv;
  lua_State th; /* thread */
};

为了区分Value中存放的数据类型,再额外绑定一个类型字段:

c 复制代码
/*
** Tagged Values
*/
typedef struct {
  Value value;
  int tt;
} TValue;

lua_State、数据栈、调用栈

lua_State表示一个线程/协程(后面线程与协程通用)的状态,lua_newstate用于创建主线程:

c 复制代码
/*
** Main thread combines a thread state and the global state
*/
typedef struct {
  lua_State l;
  global_State g;
} LG;

LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud) {
  int i;
  lua_State *L;
  global_State *g;
  void *l = (*f)(ud, NULL, 0, state_size(LG));
  if (l == NULL) return NULL;
  ...

用lua_newstate创建主线程的时候,同时也创建了一个global_State,所有的线程共享这个global_State,luaE_newthread用于创建协程:

c 复制代码
lua_State *luaE_newthread (lua_State *L) {
  lua_State *L1 = tostate(luaM_malloc(L, state_size(lua_State)));
  luaC_link(L, obj2gco(L1), LUA_TTHREAD);
  preinit_state(L1, G(L));
  ...

lua的数据栈是一个TValue数组,代码中用StkId类型用来指代对TValue的引用:

c 复制代码
typedef TValue *StkId; /* index to stack elements */

调用栈放在CallInfo数组中,CallInfo保存着正在调用的函数的运行状态:

c 复制代码
/*
** informations about a call
*/
typedef struct {
  StkId base; /* base for this function */
  StkId func; /* function index in the stack */
  StkId top; /* top for this function */
  const Instruction *savedpc;
  int nresults; /* expected number of results from this function */
  int tailcalls; /* number of tail calls lost under this entry */
} CallInfo;

正在调用的函数一定存在于数据栈上,由func引用正在调用的函数对象。

[base,top)指示了正在调用的函数的堆栈在数据栈上的范围。

为什么没有堆栈的当前位置?lua_State的top就是正在调用的函数的堆栈的位置啊。

c 复制代码
/*
** `per thread' state
*/
struct lua_State {
  CommonHeader;
  lu_byte status;
  StkId top; /* first free slot in the stack */
  StkId base; /* base of current function */
  global_State *l_G;
  CallInfo *ci; /* call info for current function */
  const Instruction *savedpc; /* `savedpc' of current function */
  StkId stack_last; /* last free slot in the stack */
  StkId stack; /* stack base */
  CallInfo *last_ci; /* last free slot in the ci array*/
  CallInfo *base_ci; /* array of CallInfo's */
  int stacksize;
  int size_ci; /* size of array `base_ci' */
  ...

[stack,stack_last],[base_ci,last_ci]分别是数据栈数组和调用栈数组,stacksize,size_ci分别是两个数组的大小,在需要的时候它们会进行增长。

ci是当前正在调用的函数的运行状态,base是该函数的栈底指针。

lua_newstate和luaE_newthread都调用了stack_init来初始化堆栈:

c 复制代码
static void stack_init (lua_State *L1, lua_State *L) {
  /* initialize CallInfo array */
  L1->base_ci = luaM_newvector(L, BASIC_CI_SIZE, CallInfo);
  L1->ci = L1->base_ci;
  L1->size_ci = BASIC_CI_SIZE;
  L1->last_ci = L1->base_ci + L1->size_ci - 1;
  /* initialize stack array */
  L1->stack = luaM_newvector(L, BASIC_STACK_SIZE + EXTRA_STACK, TValue);
  L1->stacksize = BASIC_STACK_SIZE + EXTRA_STACK;
  L1->top = L1->stack;
  L1->stack_last = L1->stack + (L1->stacksize - EXTRA_STACK) - 1;
  /* initialize first ci */
  L1->ci->func = L1->top;
  setnilvalue(L1->top++); /* `function' entry for this `ci' */
  L1->base = L1->ci->base = L1->top;
  L1->ci->top = L1->top + LUA_MINSTACK;
}

可以看到first ci只是占了个位置,它的func只是一个空值。

c代码中的函数调用

c代码中调用函数(包括c函数和Lua函数)使用lua_call和lua_pcall。

c 复制代码
void (lua_call) (lua_State *L, int nargs, int nresults);
int (lua_pcall) (lua_State *L, int nargs, int nresults, int errfunc);

使用lua时一个愚蠢的错误中同时展示了这两个函数的用法。

二者最终都通过调用luaD_call来实现主要逻辑,但lua_pcall多了一层错误处理机制,如果函数调用过程中出错,lua_pcall会捕获错误,并调用用户通过errfunc参数传入的错误处理函数,而lua_call则会直接退出程序。这套错误处理机制是通过c的longjmp或c++的异常机制来实现的。

c 复制代码
/*
** 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) {
  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 (luaD_precall(L, func, nResults) == PCRLUA) /* is a Lua function? */
    luaV_execute(L, 1); /* call it */
  L->nCcalls--;
  luaC_checkGC(L);
}

luaD_precall执行的是函数调用部分的工作,而luaD_poscall做的是函数返回的工作。对于c函数整个函数调用是连续的,luaD_precall在调用完c函数后可直接调用luaD_poscall完成工作;而lua函数执行完luaD_precall后,只是切换了lua_State的执行状态,被调用的函数的字节码尚未运行,luaD_precall返回后,调用luaV_execute去运行被调用函数的字节码,待到虚拟机执行到对应的return指令时,才会去调用luaD_poscall完成整次调用。

c函数的参数传递

使用lua_call或lua_pcall调用lua函数前,应先在栈上准备好函数对象和函数的参数,如果没有错误发生,函数返回时,函数对象和函数参数会出栈,函数的返回值会留在栈上。

而如果调用的是c函数时,一般是先通过lua_pushcclosure来创建c闭包,然后再调用lua_call或lua_pcall。

c 复制代码
void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n) {
  Closure *cl;
  lua_lock(L);
  luaC_checkGC(L);
  api_checknelems(L, n);
  cl = luaF_newCclosure(L, n, getcurrenv(L));
  cl->c.f = fn;
  L->top -= n;
  while (n--)
    setobj2n(L, &cl->c.upvalue[n], L->top+n);
  setclvalue(L, L->top, cl);
  lua_assert(iswhite(obj2gco(cl)));
  api_incr_top(L);
  lua_unlock(L);
}

调用lua_pushcclosure前,fn需要的参数都在栈上,lua_pushcclosure会把栈上的参数转移到c闭包的上值中,然后把c闭包压入栈。在函数fn中,需要用lua_getupvalue来读取参数。

lua还提供了另一种调用c函数的方式:

c 复制代码
int lua_cpcall (lua_State *L, lua_CFunction func, void *ud);

在调用lua_cpcall前,不需要将参数压入栈,而是直接将参数通过ud传递给lua_cpcall。

lua_cpcall会创建一个没有上值的c闭包并压入栈中,然后将ud作为lightuserdata也压入栈,其余便和lua_pcall类似了。

在函数func中需要通过lua_touserdata取出参数。

lua_cpcall在调用luaD_call时nResults传入的是0,所以func并没有返回值。

协程

协程的创建就是新建了一个lua_State,然后将lua函数对象移到新建的lua_State上:

c 复制代码
int luaB_cocreate (lua_State *L) {
  lua_State *NL = lua_newthread(L);
  luaL_argcheck(L, lua_isfunction(L, 1) && !lua_iscfunction(L, 1), 1,
    "Lua function expected");
  lua_pushvalue(L, 1); /* move function to top */
  lua_xmove(L, NL, 1); /* move function from L to NL */
  return 1;
}

通过luaB_coresume启动/恢复协程,首个参数是协程对象:

c 复制代码
static int luaB_coresume (lua_State *L) {
  lua_State *co = lua_tothread(L, 1);
  int r;
  luaL_argcheck(L, co, 1, "coroutine expected");
  r = auxresume(L, co, lua_gettop(L) - 1);
  if (r < 0) {
    lua_pushboolean(L, 0);
    lua_insert(L, -2);
    return 2; /* return false + error message */
  }
  else {
    lua_pushboolean(L, 1);
    lua_insert(L, -(r + 1));
    return r + 1; /* return true + `resume' returns */
  }
}

在auxresume中将coroutine.resume的其余参数从主线程转移至协程:

c 复制代码
int auxresume (lua_State *L, lua_State *co, int narg) {
  int status = costatus(L, co);
  if (!lua_checkstack(co, narg))
    luaL_error(L, "too many arguments to resume");
  if (status != CO_SUS) {
    lua_pushfstring(L, "cannot resume %s coroutine", statnames[status]);
    return -1; /* error flag */
  }
  lua_xmove(L, co, narg);
  lua_setlevel(L, co);
  status = lua_resume(co, narg);
  ...

然后去调用lua_resume,后续最终会调用resume:

c 复制代码
void resume (lua_State *co, void *ud) {
  StkId firstArg = cast(StkId, ud);
  CallInfo *ci = co->ci;
  if (co->status == 0) { /* start coroutine? */
    lua_assert(ci == co->base_ci && firstArg - co->base == 1);
    lua_assert(isLfunction(co->base));
    if (luaD_precall(co, firstArg - 1, LUA_MULTRET) != PCRLUA)
      return;
  }
  else { /* resuming from previous yield */
    lua_assert(co->status == LUA_YIELD);
    co->status = 0;
    if (!f_isLua(ci)) { /* `common' yield? */
      /* finish interrupted execution of `OP_CALL' */
      lua_assert(GET_OPCODE(*((ci-1)->savedpc - 1)) == OP_CALL ||
                 GET_OPCODE(*((ci-1)->savedpc - 1)) == OP_TAILCALL);
      if (luaD_poscall(co, firstArg)) /* complete it... */
        co->top = co->ci->top; /* and correct top if not multiple results */
    }
    else /* yielded inside a hook: just continue its execution */
      co->base = co->ci->base;
  }
  luaV_execute(co, cast_int(co->ci - co->base_ci));
}

启动协程时,和调用普通lua函数一样先调用luaD_precall,再调用luaV_execute,luaV_execute中调用函数的代码如下:

c 复制代码
void luaV_execute (lua_State *L, int nexeccalls) {
  ...
  case OP_CALL: {
    int b = GETARG_B(i);
    int nresults = GETARG_C(i) - 1;
    if (b != 0) L->top = ra+b; /* else previous instruction set top */
    L->savedpc = pc;
    switch (luaD_precall(L, ra, nresults)) {
      case PCRLUA: {
        nexeccalls++;
        goto reentry; /* restart luaV_execute over new Lua function */
      }
      case PCRC: {
        /* it was a C function (`precall' called it); adjust results */
        if (nresults >= 0) L->top = L->ci->top;
        base = L->base;
        continue;
      }
      default: {
        return; /* yield */
      }
    }
  }
  ...
}

int luaD_precall (lua_State *L, StkId func, int nresults) {
  ...
  else { /* if is a C function, call it */
    CallInfo *ci;
    int n;
    luaD_checkstack(L, LUA_MINSTACK); /* ensure minimum stack size */
    ci = inc_ci(L); /* now `enter' new function */
    ci->func = restorestack(L, funcr);
    L->base = ci->base = ci->func + 1;
    ci->top = L->top + LUA_MINSTACK;
    lua_assert(ci->top <= L->stack_last);
    ci->nresults = nresults;
    if (L->hookmask & LUA_MASKCALL)
      luaD_callhook(L, LUA_HOOKCALL, -1);
    lua_unlock(L);
    n = (*ci_func(ci)->c.f)(L); /* do the actual call */
    lua_lock(L);
    if (n < 0) /* yielding? */
      return PCRYIELD;
    else {
      // n表示c函数留在栈上的结果数量,luaD_poscall会将结果调整为nresults个,放到func开始的位置
      // 相当于将func和所有参数出栈,再将前nresults的返回值入栈
      luaD_poscall(L, L->top - n);
      return PCRC;
    }
  }
  ...
}

当lua函数中碰到coroutine.yield时,经luaD_precall->luaB_yield,再调用到lua_yield:

c 复制代码
int lua_yield (lua_State *co, int nresults) {
  luai_userstateyield(co, nresults);
  lua_lock(co);
  if (co->nCcalls > co->baseCcalls)
    luaG_runerror(co, "attempt to yield across metamethod/C-call boundary");
  co->base = co->top - nresults; /* protect stack slots below */
  co->status = LUA_YIELD;
  lua_unlock(co);
  return -1;
}

lua_yield将协程置为LUA_YIELD状态,设置了co->base,然后一路返回到auxresume:

c 复制代码
static int auxresume (lua_State *L, lua_State *co, int narg) {
  ...
  status = lua_resume(co, narg);
  if (status == 0 || status == LUA_YIELD) {
    int nres = lua_gettop(co);
    if (!lua_checkstack(L, nres + 1))
      luaL_error(L, "too many results to resume");
    lua_xmove(co, L, nres); /* move yielded values */
    return nres;
  }
  else {
    lua_xmove(co, L, 1); /* move error message */
    return -1; /* error flag */
  }
}

因为lua_yield设置的co->base,lua_gettop(co)返回coroutine.yield的参数个数,将这些参数转移到主线程后,便从coroutine.resume返回了。

注意到luaD_precall中发生yield时,并没有调用luaD_poscall,当协程恢复时需要补上,至于luaD_precall中函数正常返回时,为什么没修正L->top,那是在lua_call中修正的。

相关推荐
okok__TXF16 小时前
Nginx + Lua脚本打配合
nginx·lua
miss writer16 小时前
Redis分布式锁释放锁是否必须用lua脚本?
redis·分布式·lua
努力--坚持1 天前
电商项目-网站首页高可用(一)
nginx·lua·openresty
黄名富1 天前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
染指11101 天前
50.第二阶段x86游戏实战2-lua获取本地寻路,跨地图寻路和获取当前地图id
c++·windows·lua·游戏安全·反游戏外挂·游戏逆向·luastudio
spencer_tseng2 天前
WeakAuras NES Script(lua)
lua·wow·nes·weakauras
红黑色的圣西罗2 天前
xlua中自定义lua文件加载的一种方式
lua
法外狂徒张三!2 天前
Roblox踩坑1——动画无法完整播放
lua·roblox
冒泡P3 天前
【Lua热更新】上篇
开发语言·数据结构·unity·c#·游戏引擎·lua
两水先木示4 天前
【LuaFramework】LuaFramework_UGUI_V2框架学习
学习·unity·lua·luaframework·tolua