從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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM