从lua的c源码了解lua栈结构和函数调用流程

Posted winsons

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从lua的c源码了解lua栈结构和函数调用流程相关的知识,希望对你有一定的参考价值。

因为实习需要用到lua所以最近在学习lua,在学习过程中我使用C++与lua做交互。正常来说,如果lua要调用C++的函数,C++需要返回一个整数,这个整数的值是这个C++函数需要返回给lua调用的值的个数。这样的做法才是正确的,只是我突然间想了下,如果我返回一个不正确的值会怎样呢?于是我这么做了,然后数据如预料之中变得很不正常。然后我又在想,为什么我返回不正确的值lua会得到这样的数据呢。于是我开始了lua的C源码分析。其实就是给自己挖了个大坑23333,然后我又是属于那种有问题没解决心里仿佛有块石头压着的那种人。没办法,只好硬着头皮上了。

一、问题建立

实验代码:

main.cpp

#include <iostream>
#include <string>
#include "lua.hpp"
#include "Utils.hpp"

int CGetPow(lua_State *l)
{
    lua_pushstring(l, "hello");
    lua_pushstring(l, "world");
    StackDump(l);
    return 2;
}



int main(int argc, const char *argv[])
{
    using namespace std;
    int error,error1,error2;
    string fname;
    fname = argv[1]; 
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);
    lua_pushstring(L, "fuck");
    lua_pushcfunction(L, CGetPow);
    lua_setglobal(L, "pow");
    error1 = luaL_loadfile(L, fname.c_str());
    error2 = lua_pcall(L, 0, 0, 0);
    error = error1 || error2;
    if (error)
    {
        std::cout << fname << std::endl;
        fprintf(stderr, "%s\\n", lua_tostring(L, -1));
        lua_pop(L, 1);
    }
    lua_close(L);
    return 0;
}

 StackDump的作用是打印栈上的内容:

void StackDump(lua_State *l)
{
    int top = lua_gettop(l);
    std::cout << "Stack: ";
    for (int i = 1; i <= top; i++)
    {
        int t = lua_type(l, i);
        switch (t)
        {
        case LUA_TSTRING:
            std::cout << "" << lua_tostring(l, i) << "";
            break;
        case LUA_TBOOLEAN:
            std::cout << lua_toboolean(l, i) ? "true" : "false";
            break;
        case LUA_TNUMBER:
            std::cout << lua_tonumber(l, i);
            break;
        default:
            std::cout  << lua_typename(l, t);
            break;
        }
        std::cout << "\\t";
    }
    std::cout << std::endl;
}

test.lua

print(pow(2,3))--这句是最主要的,下面三个输出只是为了对比函数而已
print("print",print)
print("pow",pow)

print("test.lua",debug.getinfo(1, "f").func)

 

现在,CGetPow返回的整数是2,而函数中push的内容也的确只有两个,所以,它会很正常的返回"hellow"和"world"内容,输出:

技术图片

如上图所示,print(pow(2,3))的输出是 hello world(下面三行还没用到,可以先无视)。

然后第一次,把CGetPow的返回改为5,它会输出:

技术图片

这时候输出变成了function:xxxxx  2  3  hello  world,第一个function:xxxxxx可以由下面的函数对照发现是pow函数,也就是CGetPow,从这时候看好像看起来还是栈结构,虽然它返回来我们不需要的参数2和3和正在CGetPow函数.

第二次,把CGetPow的返回改为8,输出为:

技术图片

由函数对照发现,输出在重复:fuck,加载test.lua生成的函数,print函数。这时候它的输出又不像一个栈结构了,因为2,3,hello,world不见了。为什么会这样呢?lua是怎么push值到栈中的,怎么从栈中取值的,函数调用到底干了什么?我决定通过分析lua的c源码来弄明白这些问题。

 

二、源码分析

1、lua_State和栈的初始化

  lua_State由lua_newstate创建:

LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud) {
  int i;
  lua_State *L;
  global_State *g;
 //LG由传进来的f进行分配,其中就已经包含了lua_State
  LG *l = cast(LG *, (*f)(ud, NULL, LUA_TTHREAD, sizeof(LG)));
  if (l == NULL) return NULL;
 //从LG中取出要返回的lua_State
  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);
  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);
  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;
}

展开lua_rawrunprotected

int luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud) {
  ......
  LUAI_TRY(L, &lj,
    (*f)(L, ud);
  );
  ......
}

 

发现调用了传进来的f_luaopen函数,追踪这个函数

static void f_luaopen (lua_State *L, void *ud) {
  ......
  stack_init(L, L);  /* init stack */
  ......
}

 

由作者注释可知stack_init是初始化栈的函数,找到这个函数的实现

static void stack_init (lua_State *L1, lua_State *L) {
  int i; CallInfo *ci;
  /* initialize stack array */
  L1->stack = luaM_newvector(L, BASIC_STACK_SIZE, TValue);
  L1->stacksize = BASIC_STACK_SIZE;
  for (i = 0; i < BASIC_STACK_SIZE; i++)
    setnilvalue(L1->stack + i);  /* erase new stack */
  L1->top = L1->stack;
  L1->stack_last = L1->stack + L1->stacksize - EXTRA_STACK;
  /* initialize first ci */
  ci = &L1->base_ci;
  ci->next = ci->previous = NULL;
  ci->callstatus = 0;
  ci->func = L1->top;
  setnilvalue(L1->top++);  /* ‘function‘ entry for this ‘ci‘ */
  ci->top = L1->top + LUA_MINSTACK;
  L1->ci = ci;
}

分析得知:lua_State由luaM_newvector初始化并返回一个基地址给L1->stack,栈的大小为40(由BASIC_STACK_SIZE的宏定义得到),然后初始化把栈上的值全设置为nil,设置栈最后一个元素的地址L1->stack_last,初始化当前调用信息L1->ci,把lua_State的top设置为第一个栈上的第二个空元素(第一个是空元素已经被用作ci了,所以不能使用),设置ci的top(其实相当于ci作为栈基,然后ci这个栈的长度为LUA_MINSTACK,也就是20,在这个栈中push时不能超过这个长度,除非重新设置栈长度)。

 

2、lua从栈中取数据和往栈中push数据

首先是取数据,这里用lua_tointegerx作例子

LUA_API lua_Integer lua_tointegerx (lua_State *L, int idx, int *pisnum) {
  lua_Integer res;
  //重点是这句,TValue是lua中用的最多的数据结构,index2addr则是根据传入的栈索引从栈取出数据
  const TValue *o = index2addr(L, idx);
  int isnum = tointeger(o, &res);
  if (!isnum)
    res = 0;  /* call to ‘tointeger‘ may change ‘n‘ even if it fails */
  if (pisnum) *pisnum = isnum;
  return res;
}

查看TValue的数据结构

//Value是具体的值,tt_则是定义好的数据类型,数据类型在lua中也是使用宏定义设置的
#define TValuefields    Value value_; int tt_

typedef struct lua_TValue {
  TValuefields;
} TValue;

展开index2addr的实现:

static TValue *index2addr (lua_State *L, int idx) {
 //当前调用函数的信息
  CallInfo *ci = L->ci;
  if (idx > 0) {
    //在栈基(这里的栈基不是指整个lua_State的栈基,而是当前调用函数信息的栈基,也可以简单理解成就是当前函数在栈中的地址)基础上加上传进来的索引获取到正确的数据
    TValue *o = ci->func + idx;
    //下面是检测数据正确性
    api_check(L, idx <= ci->top - (ci->func + 1), "unacceptable index");
    if (o >= L->top) return NONVALIDVALUE;
    //返回获取到的数据
    else return o;
  }
   ......
}

通过index2addr从栈中获取到数据,然后在根据tt_获取到数据类型,得到对应的值。lua_tolstring,lua_toboolean等基本都是这样从栈中取数据。

然后是向栈中push数据。这里以lua_pushinteger做例子:

#define api_incr_top(L) {L->top++; api_check(L, L->top <= L->ci->top,                 "stack overflow");}
#define setivalue(obj,x) \\
{ TValue *io=(obj); val_(io).i=(x); settt_(io, LUA_TNUMINT); }


LUA_API void lua_pushinteger (lua_State *L, lua_Integer n) {
  lua_lock(L);
  setivalue(L->top, n);
  api_incr_top(L);
  lua_unlock(L);
}

这里所做的操作显示把L->top指向的空元素设置为想要设置的元素,然后再把L->top只向下一个空元素,期间同样会涉及到向栈推元素需要做的安全性检查。push其他类型元素的操作也和这个操作原理一样。

 

3、函数的调用

通常我们在C中调用函数,会先push要调用的函数,然后再按顺序push参数,最后使用lua_pcall指定lua_state,参数个数以及返回个数。先来分析源码

//lua_pcall是个宏定义,展开后是lua_pcallk()
#define lua_pcall(L,n,r,f)    lua_pcallk(L, (n), (r), (f), 0, NULL)

//看看lua_pcallk的实现,代码太多,只展示要说明的
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);
  //errfunc为0代表没指定错误处理函数
  if (errfunc == 0)
    func = 0;
  else {
    ......
  }
  //c.func通过栈的运算获得,是要调用的函数在栈上的位置
  c.func = L->top - (nargs+1);  /* function to be called */
  //k为空代表没有延续
  if (k == NULL || L->nny > 0) {  /* no continuation or no yieldable? */
    //设置返回结果个数
    c.nresults = nresults;  /* do a ‘conventional‘ protected call */
     //luaD_pcall才是重点,在这里传进去了f_call函数
    status = luaD_pcall(L, f_call, &c, savestack(L, c.func), func);
  }
  else {  /* prepare continuation (call is already protected by ‘resume‘) */
    ......    
  }
  //调整栈顶位置
  adjustresults(L, nresults);
  lua_unlock(L);
  return status;
}

//展开luaD_pcall
int luaD_pcall (lua_State *L, Pfunc func, void *u,
                ptrdiff_t old_top, ptrdiff_t ef) {
  ......
  //在这里调用了上一步传进来的f_call函数
  status = luaD_rawrunprotected(L, func, u);
  ......
}

//继续展开f_call函数
static void f_call (lua_State *L, void *ud) {
  struct CallS *c = cast(struct CallS *, ud);
  //f_call里有调用了luaD_callnoyield函数
  luaD_callnoyield(L, c->func, c->nresults);
}

//展开luaD_callnoyield函数
void luaD_callnoyield (lua_State *L, StkId func, int nResults) {
  L->nny++;
  //好吧,继续看看这个
  luaD_call(L, func, nResults);
  L->nny--;
}

void luaD_call (lua_State *L, StkId func, int nResults) {
  if (++L->nCcalls >= LUAI_MAXCCALLS)
    stackerror(L);
  //重点函数:luaD_precall,展开之
  if (!luaD_precall(L, func, nResults))  /* is a Lua function? */
    luaV_execute(L);  /* call it */
  L->nCcalls--;
}

int luaD_precall (lua_State *L, StkId func, int nresults) {
  lua_CFunction f;
  CallInfo *ci;
  switch (ttype(func)) {
    case LUA_TCCL:  /* C closure */
      f = clCvalue(func)->f;
      goto Cfunc;
    case LUA_TLCF:  /* light C function */
      f = fvalue(func);
     Cfunc: {
      //n是c函数中返回的值
      int n;  /* number of returns */
       //栈检查,这个没细看,都是些检查安全性的东西
      checkstackp(L, LUA_MINSTACK, func);  /* ensure minimum stack size */
      //创建并初始化新的调用函数信息,并进入新的函数
      ci = next_ci(L);  /* now ‘enter‘ new function */
      ci->nresults = nresults;
      ci->func = func;
      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);
      //终于来了!!妈的调用来调用去的你终于执行到了真正要执行的函数了啊!C函数的返回值赋给了n。
      n = (*f)(L);  /* do the actual call */
      lua_lock(L);
      api_checknelems(L, n);
      //这个函数也很重要,就是把执行函数后push进来的值重新放置到正确的位置,L->top-n是指第一个元素,n是指有多少个元素
      luaD_poscall(L, ci, L->top - n, n);
      return 1;
    }
  //这里只说明c函数部分,其他lua函数等部分的原理都是差不多的。
    ......
  }
}

//展开luaD_poscall
int luaD_poscall (lua_State *L, CallInfo *ci, StkId firstResult, int nres) {
  StkId res;
  int wanted = ci->nresults;
  ......
  //res指向当前调用函数在栈中的位置
  res = ci->func;  /* res == final position of 1st result */
  //通过调用函数信息链回到上一个调用函数信息中
  L->ci = ci->previous;  /* back to caller */
  /* move results to proper place */
  //moveresults真正执行移动元素的操作
  return moveresults(L, firstResult, res, nres, wanted);
}

//展开moveresults
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;
    }
    //如同print函数这些需要所有返回值的则调用这个,转移所有元素
    case LUA_MULTRET: {
      int i;
     //遍历nres个元素并转移到新位置,可以发现转移元素实际上使用到了setobjs2s()
      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;
}


//展开setobjs2s
//发现原来又是宏定义
#define setobjs2s    setob
//还是宏定义,原来就是直接把obj2的值赋给了obj1而已
#define setobj(L,obj1,obj2) \\
    { TValue *io1=(obj1); *io1 = *(obj2);       (void)L; checkliveness(L,io1); }

OK~到这里,就基本上明白了整个函数调用流程以及在流程中的栈变化了。

 

三、得出结论

为什么当CGetPow的返回值为5的时候它看起来还像是个栈结构,但是返回值为8的时候却会重复fuck  编译test.lua得出的函数  print这三个呢?

解:

  因为在调用print(pow(2,3))中,栈中的结构为1:nil(已通过实验确定这是最初的lua_state的ci->func),2:fuck, 3:function(加载test.lua得到的函数),4:print函数,5:pow函数,6:参数2,7:参数3.

当push了"hello"和"world“后,栈结构多了8:”hello“,9:”world“。

  然后如果这时候返回值为5,移动栈元素时会从L->top-5(当前的top在索引10上)的元素开始,移动5个元素到从当前函数(pow)在栈中的位置开始的位置上。也就是说把从第5个索引开始的5个元素转移到从pow函数所在位置开始的新位置,所以实际上栈的元素是没有变动过的。所以当调用第4个索引位置上的print输出他们的时候,输出结果为:pow  2  3  hello  world。

  然后当返回值为8时,移动栈元素时会从L->top-8,也就是索引2开始把8个元素转移到pow函数所在位置开始的新位置后。这里就会出现一个现象,因为2->5,3->6,4->7,所以实际上现在第5索引上的元素为最开始第2个索引上的元素,所以5->8后,第8索引上的元素为fuck,同理6->9实际上是3->9,7->10实际上是4->10,这就导致了print输出时一直重复fuck  test.lua编译出来的函数  print这3个元素。

  至此问题解决,并且还对lua的认识增加了许多。就是看lua的宏定义看得我脑壳疼了一天。233333

以上是关于从lua的c源码了解lua栈结构和函数调用流程的主要内容,如果未能解决你的问题,请参考以下文章

从Lua调用C

C与lua交互--lua调用栈分析

lua C API

Lua 和 C 交互中虚拟栈的操作

lua入门之二:c/c++ 调用lua及多个函数返回值的获取

lua栈