risc-v中文社区

 找回密码
 立即注册
查看: 1410|回复: 0

Lua源码分析 - 主流程篇 - 函数调用栈的实现(08)

[复制链接]

347

主题

564

帖子

2237

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
2237
发表于 2022-6-20 14:47:41 | 显示全部楼层 |阅读模式
目录

一、函数调用栈 - 从main方法看函数调用栈

二、函数调用栈 - 函数调用主流程详解

三、调用栈操作 - 保护方式lua_pcallk函数调用

四、调用栈操作 - 主函数luaD_call逻辑详解

五、调用栈操作 - 结果集moveresults调整

前面几章我们介绍了Lua常用的最重要的几个数据结构。这章节开始,我们开始讲解主流程篇。

主流程,一般都是从lua.c的main方法开始。那我们就从main方法开始看整个链路和流程。

一、函数调用栈 - 从main方法看函数调用栈
从main方法中,创建完基础的lua_State *L结构后,我们就能看到Lua向数据栈上push了一个c语言的闭包方法。该方法:pmain。

pmain方法是整个Lua执行流最核心的方法。主要负责:命令行参数的解析、Lua语言默认库的加载、Lua脚本语言的解析和调用等。
Lua的栈操作主要靠lua_push*系列函数,往栈上不同压入不同的数据。
main函数中首先压入pmain方法,然后把命令行参数个数和命令行参数数据也压入到了Lua的栈上。然后通过lua_pcall方法,开始执行pmain函数。
lua_pcall方法为何调用的是pmain函数,而不是其他函数。其实调用的函数就是我们Lua栈执行函数= L->top - (nargs+1); 在lua_pcall中就能详细看到。
当调用pmain函数的时候,pmain首先会去解析命令行参数,然后加载默认的Lua语言的API函数库,再然后加载lua脚本文件以及解析lua语言,最后如果pmain方法执行成功,需要将status状态push放入栈顶(lua_pushboolean)。
main方法中,通过lua_toboolean从当前调用栈CallInfo上获取结果值。
看一下具体实现:

/**
* argc:行参数的个数
* argv:每个参数的值,指针结构
*/
int main (int argc, char **argv) {
  int status, result;
  /* 第一步:创建一个主线程栈数据结构 */
  lua_State *L = luaL_newstate();  /* create state */
  if (L == NULL) {
    l_message(argv[0], "cannot create state: not enough memory");
    return EXIT_FAILURE;
  }
  lua_pushcfunction(L, &pmain);  /* 将pmain放入L结构上 L->top值&pmain*/
  lua_pushinteger(L, argc);  /* 将argc 放入L结构上  L->top值argc*/
  lua_pushlightuserdata(L, argv); /* 将argv 放入L结构上 L->top值argv*/
  status = lua_pcall(L, 2, 1, 0);  /* 函数操作,执行pmain 函数 do the call */
  result = lua_toboolean(L, -1);  /* 获取pmain函数lua_pushboolean(L, 1) 的信号值get result */
  report(L, status);
  lua_close(L);
  return (result && status == LUA_OK) ? EXIT_SUCCESS : EXIT_FAILURE;
}

static int pmain (lua_State *L) {
   .......省.......

  /* 打开常规Lua的标准库 */
  luaL_openlibs(L);  /* open standard libraries */

  .......省.......
  else dofile(L, NULL);  /* executes stdin as a file */

  .......省.......
  lua_pushboolean(L, 1);  /* 向栈顶L->top PUSH 返回值signal no errors */
  return 1;
}
具体pmain函数的调用,可以看下图:





二、函数调用栈 - 函数调用主流程详解
具体的整个函数调用栈操作流程如下(可以参考图片中的文件顺序进行源码阅读):

首先Lua mian函数中,通过lua_push*系列函数将调用的函数以及参数数据进行入栈操作。
Lua会调用lua_pcall执行Lua函数的真正调用。lua_pcall函数通过参数的个数和结果个数进行数据校验,以及拿到函数栈的StkId地址。lua_pcallk和lua_callk函数都在lapi.c文件中,两个函数的区别是一个有异常保护,一个没有异常保护。无论是保护状态下,还是非保护状态下,最终都是调用luaD_call函数(Lua的函数调用执行操作都在ldo.c文件中)。
luaD_call函数中,首先会调用luaD_precall预处理函数。luaD_precall预处理函数主要会创建一个调用栈CallInfo,管理函数调用时的信息。CallInfo是一个双向链表形式管理。每次有新的函数进来,都会用新的CallInfo进行函数调用信息管理。
Lua的函数调用一共分三种:C语言闭包函数(例如pmain),Lua的API函数库(例如字符串strlen函数),Lua语言解析。
如果是c语言闭包函数和Lua的API函数:会直接执行C方法(方法入参都为L),并调用luaD_poscall,调整堆栈,返回上一个调用栈。(我们语言中,肯定是一个函数内会嵌套N个函数。每次执行完一个函数后,都会返回到上一层CallInfo(L->ci会变更为上一层CallInfo),然后继续执行该函数的其它事务。)
如果是Lua语言,则会调用luaV_execute执行语言字节码调用。在luaV_execute函数中,当遇到函数调用尾部OP_TAILCALL的时候,则会调整CallInfo堆栈,返回上一个调用栈
如果你有返回值,在被执行的函数中通过lua_push*系列函数,将函数结果返回到栈顶部即可。外部可以通过lua_to*系列函数,获取当前调用栈的栈顶数据信息。
最后,在luaD_poscall函数中,会调用moveresults方法,该方法主要用于调整返回结果,将结果集调整到CallInfo函数的起始位置ci->func,并调整L->top。说白了,一个CallInfo调整完毕之后,只需要将得到的结果集返回到堆栈上,并调整堆栈,保障整体的堆栈回调大小控制在一定范围内。




三、调用栈操作 - 保护方式lua_pcallk函数调用
在lapi.c文件中,lua_pcallk方法是受保护的调用方式,lua_callk为非受保护的调用方式。受保护方法的唯一区别:函数的调用都不会因为错误直接导致程序直接退出,而是退回到调用点,然后将状态返回到外层的逻辑处理。

我们可以看一下ldo.c文件中的luaD_pcall方法。该方法最终调用luaD_rawrunprotected,而luaD_rawrunprotected实际最终调用f_call函数。保护性调用的情况下lua虚拟机使用lua_longjmp为函数实现堆栈续传功能,也就是当错误发生的时候,在Lua内部能够最终跳转到调用点继续向下执行。详见:《Lua源码分析 - 主流程篇 - 异常处理机制实现(09)》

/**
* 函数调用主方法(异常保护方式)
* func:f_call方法
* u:CallS 调用的方法等信息
* old_top:函数调用前的栈顶 L->top
* ef:错误状态
*/
int luaD_pcall (lua_State *L, Pfunc func, void *u,
                ptrdiff_t old_top, ptrdiff_t ef) {
  int status;
  CallInfo *old_ci = L->ci; //老的函数回调CallInfo
  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) {  /* an error occurred? */
    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_callnoyield方法)
* f=luaD_callnoyield方法
* ud=CallS *c   ( c->func, c->nresults)
*/
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;  /* restore old error handler */
  L->nCcalls = oldnCcalls;
  return lj.status;
}

/**
* 该方法包了很多层,最终调用luaD_call方法
*/
static void f_call (lua_State *L, void *ud) {
  struct CallS *c = cast(struct CallS *, ud);
  luaD_callnoyield(L, c->func, c->nresults);
}

/*
** Similar to 'luaD_call', but does not allow yields during the call
** 不允许挂起
*/
void luaD_callnoyield (lua_State *L, StkId func, int nResults) {
  L->nny++;
  luaD_call(L, func, nResults);
  L->nny--;
}
四、调用栈操作 - 主函数luaD_call逻辑详解
无论是保护模式还是非保护模式下,最终会调用luaD_call方法。

该方法会先调用luaD_precall函数,对Lua函数执行作预处理。预处理主要作用是调整:调用栈CallInfo的堆栈

预处理函数主要逻辑:

检查栈信息,创建一个新的CallInfo 容器,填充相关的信息
如果是C语言闭包函数 or Lua的C语言 API函数库,则直接执行函数(PS:这些函数入参都统一为L)
如果是Lua语言,则进行CallInfo预处理之后,直接调用luaV_execute函数,执行虚拟机的指令集。
不论是C语言函数还是Lua语言函数,函数执行完毕后都会进行堆栈调整,将正在执行的操作站CallInfo指针会往上一层调用栈调整。C语言函数会调用luaD_poscall调整堆栈。而Lua语言则在虚拟机指令集运行中,找到函数调用尾部OP_TAILCALL的时候,会调整整体堆栈。
/*
** 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.
** 真正执行一个C语言方法 or 一个Lua方法
*/
void luaD_call (lua_State *L, StkId func, int nResults) {
  if (++L->nCcalls >= LUAI_MAXCCALLS)
    stackerror(L);
  if (!luaD_precall(L, func, nResults))  /* is a Lua function? */
    luaV_execute(L);  /* call it Lua方法,则执行字节码方式 */
  L->nCcalls--;
}

/*
** Prepares a function call: checks the stack, creates a new CallInfo
** entry, fills in the relevant information, calls hook if needed.
** If function is a C function, does the call, too. (Otherwise, leave
** the execution ('luaV_execute') to the caller, to allow stackless
** calls.) Returns true iff function has been executed (C function).
** 预处理一个方法call
** 1. 检查栈信息
** 2. 创建一个新的CallInfo 容器
** 3. 填充相关的信息
** 4. 如果需要,回调钩子函数
*/
int luaD_precall (lua_State *L, StkId func, int nresults) {
  lua_CFunction f;
  CallInfo *ci;
  switch (ttype(func)) {
    case LUA_TCCL:  /* C语言闭包 C closure */
      f = clCvalue(func)->f;
      goto Cfunc;
    case LUA_TLCF:  /* C语言函数 light C function */
      f = fvalue(func);
     Cfunc: {
      int n;  /* number of returns */
      checkstackp(L, LUA_MINSTACK, func);  /* ensure minimum stack size */
      ci = next_ci(L);  /* 创建一个新的CallInfo栈对象 now 'enter' new function */
      ci->nresults = nresults; //返回的结果个数
      ci->func = func; //指向需要调用的函数栈
      ci->top = L->top + LUA_MINSTACK; //C语言方法最小的调用栈允许20
      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);  /* 直接调用C语言闭包函数 do the actual call */
      lua_lock(L);
      api_checknelems(L, n);
      luaD_poscall(L, ci, L->top - n, n); //调整堆栈
      return 1; //返回1 C语言本身函数
    }
    case LUA_TLCL: {  /* Lua方法 Lua function: prepare its call */
      StkId base;
      Proto *p = clLvalue(func)->p;
      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; //返回0,Lua函数
    }
    default: {  /* not a function */
      checkstackp(L, 1, func);  /* ensure space for metamethod */
      tryfuncTM(L, func);  /* try to get '__call' metamethod */
      return luaD_precall(L, func, nresults);  /* now it must be a function */
    }
  }
}


五、调用栈操作 - 结果集moveresults调整
函数调用结束后,首先CI会回滚到上一层的调用,并调用moveresults函数,调整结果集数据。

moveresults会将结果集(0个/1个/多个),逐个根据顺序拷贝到ci->func位置,并调整L->top的指针位置。如下图。
一个函数调用完毕之后,只需要将得到的结果集返回给上一层,并调整堆栈top指针,保障整体的堆栈回调大小控制在一定范围内。
返回值是我们回调函数里面,通过lua_push*方法向栈顶设置返回值。如果回调函数中没有设定返回值,则首个返回的结果为最后一个参数。


/*
** Given 'nres' results at 'firstResult', move 'wanted' of them to 'res'.
** Handle most typical cases (zero results for commands, one result for
** expressions, multiple results for tail calls/single parameters)
** separated.
*/
static int moveresults (lua_State *L, const TValue *firstResult, StkId res,
                                      int nres, int wanted) {
  switch (wanted) {  /* handle typical cases separately */
    case 0: break;  /* nothing to move */
    case 1: {  /* one result needed */
      if (nres == 0)   /* no results? */
        firstResult = luaO_nilobject;  /* adjust with nil */
      setobjs2s(L, res, firstResult);  /* move it to proper place */
      break;
    }
    case LUA_MULTRET: {
      int i;
      for (i = 0; i < nres; i++)  /* move all results to correct place */
        setobjs2s(L, res + i, firstResult + i);
      L->top = res + nres;
      return 0;  /* wanted == LUA_MULTRET */
    }
    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;
    }
  }
  L->top = res + wanted;  /* top points after the last result */
  return 1;
}



————————————————
版权声明:本文为CSDN博主「老码农zhuli」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/initphp/article/details/104253083

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



Archiver|手机版|小黑屋|risc-v中文社区

GMT+8, 2024-4-24 08:47 , Processed in 0.014887 second(s), 17 queries .

risc-v中文社区论坛 官方网站

Copyright © 2018-2021, risc-v open source

快速回复 返回顶部 返回列表