經過lua棧瞭解lua與c的交互

lua是如何執行的


其中分析、執行部分都是c語言實現的。函數

lua與c的關係

lua的虛擬機是用c語言實現的,換句話說一段lua指令最終在執行時都是看成c語言來執行的,lua的global表,函數調用棧也都是存在c語言中的一個叫lua_State的結構體中的。
舉個例子,來看下lua中的加指令 OP_ADD a b c 是如何實現的:
lua在運行時,會在c語言中的一個叫luaV_excute的函數中不斷執行翻譯後的lua指令,OP_ADD就是其中的一條指令(luaV_excute函數太長了,因此只在這裏截取OP_ADD的部分,有興趣能夠直接去看lua的源碼)測試

case OP_ADD: {
        arith_op(luai_numadd, TM_ADD);
        continue;
      }

相關的一些宏定義:ui

#define luai_numadd(a,b)    ((a)+(b))

//運算操做,op是運算的宏定義(加法、減法、乘法等),tm是元方法對應的枚舉
#define arith_op(op,tm) { \
        //獲取b,c的值
        TValue *rb = RKB(i); \
        TValue *rc = RKC(i); \
        //判斷是不是b,c數字
        if (ttisnumber(rb) && ttisnumber(rc)) { \
          //從b,c指向的TValue中將數字字段取出來
          lua_Number nb = nvalue(rb), nc = nvalue(rc); \
          //進行op運算後放入a中
          setnvalue(ra, op(nb, nc)); \
        } \
        //若是不是數字,則嘗試調用ra,rb的元方法
        else \
          Protect(Arith(L, ra, rb, rc, tm)); \
      }

//判斷一個Tvalue是不是數字
#define ttisnumber(o)   (ttype(o) == LUA_TNUMBER)
#define ttype(o)    ((o)->tt)

//根據b的類型獲取b對應的TValue
#define RKB(i)  check_exp(getBMode(GET_OPCODE(i)) == OpArgK, \
    ISK(GETARG_B(i)) ? k+INDEXK(GETARG_B(i)) : base+GETARG_B(i))

能夠看到,OP_ADD其實就是把b,c(b,c是須要進行加運算的兩個數字在函數常量中表中的位置中或調用棧中的位置上的TValue)的值加到了a中。
也就是說,每一個lua指令最終的實現仍是在經過執行c語言語句實現的。this

lua中進行函數調用時棧的狀態

lua和c的交互徹底經過棧來進行交互,爲了瞭解lua與c的交互必定要先了解lua的棧。
咱們常說的lua棧有兩種:
1.一個總的數據棧:每一個lua環境惟一的棧,全部的調用信息都存在這上面。
2.每一個函數的調用棧:函數的調用棧,其實並非一個獨立的棧,只是數據棧上的一小段。lua

與棧相關的結構體

lua_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 *end_ci;  /* points after end of ci array*/
  CallInfo *base_ci;  /* array of CallInfo's */
  int stacksize;
  int size_ci;  /* size of array `base_ci' */
  unsigned short nCcalls;  /* number of nested C calls */
  lu_byte hookmask;
  lu_byte allowhook;
  int basehookcount;
  int hookcount;
  lua_Hook hook;
  TValue l_gt;  /* table of globals */
  TValue env;  /* temporary place for environments */
  GCObject *openupval;  /* list of open upvalues in this stack */
  GCObject *gclist;
  struct lua_longjmp *errorJmp;  /* current error recover point */
  ptrdiff_t errfunc;  /* current error handling function (stack index) */
};

lua_State保存lua運行相關全部信息的結構體,就是lua的運行環境,lua的棧和棧相關的信息都存在這個結構體裏。這裏說明幾個和lua棧緊密相關的幾個變量:
stack: lua棧的實體,在每一個lua環境中是惟一的,每一個函數的調用棧只是stack上的一小段。
top,base: 指向當前調用棧的棧頂和棧底。
ci: 當前函數的調用信息,具體結構下面會講到。
base_ci: 函數調用信息的列表,用來記錄和恢復當前的調用信息。翻譯

CallInfo

/*
** informations about a call
*/
typedef struct CallInfo {
  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;

CallInfo是每一次函數調用的調用信息,這裏也說明幾個和棧緊密相關的變量。
base: 當前調用棧的棧底
top: 當前調用棧的棧頂
func: 當前調用的函數在stack中的位置
nresults: 當前函數預期會返回的結果個數,若是返回的數量不夠,會用nil補齊3d

經過一個例子說明一下

咱們這裏在c語言中操做lua來調用c寫的函數進行舉例,由於用c語言去操做lua的流程更接近lua編譯成指令後的過程,看的更清晰一些,又不至於像直接閱讀虛擬機指令那麼吃力。code

使用的代碼

供lua調用的c函數庫(addlib.c):orm

#include <stdio.h>
#include <lua5.1/lua.h>
#include <lua5.1/lualib.h>
#include <lua5.1/lauxlib.h>

static int addc(lua_State *L)
{
    //輸出一下當前調用棧的元素個數
    printf("get top in addc: %d\n",lua_gettop(L));
    int a,b,c;
    a = lua_tonumber(L,-1);
    b = lua_tonumber(L,-2);
    c = a + b;
    //壓入結果
    lua_pushnumber(L,c);
    //輸出壓入結果後的調用棧的元素個數
    printf("get top in addc,after push result: %d\n",lua_gettop(L));
    return 1;
}

static const struct luaL_Reg lib[] =
{
    {"addc",addc},
    {NULL,NULL}
};

int luaopen_addlib(lua_State *L)
{
    luaL_register(L,"testadd",lib);
    return 1;
}

調用代碼:blog

#include <stdio.h>
#include <lua5.1/lua.h>
#include <lua5.1/lualib.h>
#include <lua5.1/lauxlib.h>

int main()
{
    lua_State* luaEnv = lua_open();
    //載入基礎庫
    luaopen_base(luaEnv);
    luaL_openlibs(luaEnv);
    //輸出一下載入庫以後的棧中元素個數,lua_gettop(luaEnv)輸出luaEnv->top - luaEnv->base,也就是當前調用棧中元素的個數
    printf("get top after openlibs: %d\n",lua_gettop(luaEnv));
    //載入addlib庫
    lua_getglobal(luaEnv,"require");
    lua_pushstring(luaEnv,"addlib");
    lua_pcall(luaEnv,1,0,0);
    //輸出載入addlib庫後的棧中元素個數
    printf("get top after require addlib: %d\n",lua_gettop(luaEnv));
    //壓入須要調用的函數和參數
    lua_getglobal(luaEnv,"testadd");
    lua_getfield(luaEnv,-1,"addc");
    lua_pushinteger(luaEnv,10);
    lua_pushinteger(luaEnv,12);
    //輸出壓入後的棧中元素個數
    printf("get top after push function and args: %d\n",lua_gettop(luaEnv));
    //調用addc函數
    lua_pcall(luaEnv,2,1,0);
    //輸出調用後的棧中元素的個數
    printf("get top after pcall addc: %d\n",lua_gettop(luaEnv));
    int result = lua_tonumber(luaEnv,-1);
    //輸出結果
    printf("addc's result is : %d\n",result);
    return 0;
}

調用結果:

過程說明

在c語言中調用一個lua函數的流程:

棧的詳細變化過程

1.經過lua_open建立lua_State,並進行棧的初始化,棧的初始化操做在state_init()函數中。在state_init()中,建立棧的實例和函數調用列表實例,並設置了初始的ci信息,和初始ci的調用棧信息。此時棧是空的,base,top都指向棧的第一個元素。


2.打開基本的庫後的棧(打開基本庫後,棧中會剩餘兩張表格,但不影響以後的流程,因此先無視掉),由於在打開基本庫的時候載入了兩張表,全部top增長了2。


3.載入addlib庫,用於函數調用(由於調用了返回0個返回值的pcall,因此對棧的內容沒有影響,後面說),棧的狀態徹底沒變。
4.壓入要調用的函數及參數,準備進行調用。


5.經過lua_pcall(真正對addc函數的調用是在luaD_precall中)調用addc函數,在luaD_precall函數中會建立新的ci來用於保存addc的調用信息,將base移動到func+1的位置,做爲新的ci的棧底。top不動,這樣新的調用棧中就有addc的參數信息了。


6.在addc中讀取調用棧中的兩個參數,計算出結果,並壓入棧中。


7.在addc調用結束後,lua會調用luaD_poscall迴歸到上一層,在迴歸時lua會根據return n的個數將addc調用棧棧頂的n個元素拷貝到,從addc位置開始的nresults個位置中,若n < nresults,少的部分補nil,並從新計算上一層棧頂。


小結:
①lua用於調用,交互的棧只有一個,每次進行函數調用時並不會新開一個棧來存儲調用信息,而是新建立一個ci用於保存被調用函數的信息,並根據被調用函數的位置、參數個數將stack上的一段做爲新的ci的調用棧,並將當前的調用信息(當前的函數位置,當前使用的棧的區間等信息)保存爲一個callinfo,用於調用後的恢復。
②每次在lua中調用一個c函數時,會以函數在棧中的位置加1做爲被調用函數callinfo的base,top位置保持不變做爲新的調用棧的棧頂(調用lua函數略有不一樣,調用lua函數會將函數的參數新複製一份,但原理跟調用c函數差很少,因此很少作說明)。
③一次函數調用結束後,棧會根據以前保存的callinfo恢復棧的狀態,並將函數調用的結果複製到當前棧頂的位置。

lua與c的交互

c調用lua函數

咱們寫一個接收一個參數,返回兩個結果的PrintHello函數供c語言調用。
被調用的lua函數(PrintHello.lua):

--接收一個參數,返回兩個結果的函數
function PrintHello(name)
    --輸出Hello
    print("Hello "..name);
    --給要返回的兩個結果複製
    result1 = "the name : "..name;
    result2 = "something else...";
    --返回結果
    return result1,result2;
end

測試用c代碼:

# include <lua5.1/lua.h>
# include <lua5.1/lualib.h>
# include <lua5.1/lauxlib.h>

int main()
{
    //建立lua運行環境
    lua_State* luaEnv = lua_open();
    //打開基礎庫
    luaopen_base(luaEnv);
    luaL_openlibs(luaEnv);
    //載入PrintHello.lua
    luaL_loadfile(luaEnv,"PrintHello.lua");
    //執行PrintHello.lua,將PrintHello加入Global表中
    lua_pcall(luaEnv,0,0,0);
    //將PrintHello函數、參數壓棧,準備調用
    lua_getglobal(luaEnv,"PrintHello");
    lua_pushstring(luaEnv,"bard");
    //調用PrintHello函數
    lua_pcall(luaEnv,1,2,0);
    //取出返回的兩個結果並輸出
    char* result1 = lua_tostring(luaEnv,-2);
    printf("%s\n",result1);
    char* result2 = lua_tostring(luaEnv,-1);
    printf("%s\n",result2);
    return 1;
}

調用結果:

小結:
①沒有看到PrintHello函數有顯式從棧中取參數和壓入結果的操做,那麼取參數和將結果壓棧的操做是在哪裏進行的:
取參數: 在luaD_precall函數中,會建立新的ci供PrintHello使用,同時將棧頂的1個參數PrintHello的調用棧中,而後luaV_execute執行對應的PrintHello對應的一段指令,這些指令中會把name參數對應到調用棧的第一個位置去,這樣就可使用這個參數了。
壓棧: PrintHello中的return會被翻譯成兩個OP_MOVE指令和一條OP_RETURN指令,進行結果的壓棧和棧的恢復。
②其餘操做就和前面說明調用棧的操做差很少了,這裏就不作重複的解釋。

lua調用c函數

咱們寫一個接收一個參數,返回兩個結果的函數
供lua調用的c代碼(testlib.c):

#include <lua5.1/lua.h>
#include <lua5.1/lualib.h>
#include <lua5.1/lauxlib.h>
#include <stdio.h>

static int printHelloInC(lua_State* L)
{
  //取出棧中的參數
  //若是想實現函數重載,能夠對棧中的參數數量和參數類型進行判斷後進行分別處理,這裏只是爲了展現lua對c的調用,
  //因此沒有進行參數檢查
  char* arg0 = lua_tostring(L,-1);
  //輸出Hello
  printf("Hello %s\n",arg0);
  //壓入兩個要返回的值
  char* result1 = "this is result1";
  lua_pushstring(L,result1);
  char* result2 = "this is result2";
  lua_pushstring(L,result2);
  //表示這個函數有2個返回值
  return 2;
}

static const struct luaL_Reg lib[] =
{
    {"printHelloInC",printHelloInC},
    {NULL,NULL}
};

int luaopen_testlib(lua_State *L)
{
    luaL_register(L,"testlib",lib);
    return 1;
}

lua測試代碼:

--載入testlib庫
require "testlib"
--調用testlib.printHelloInC函數
a,b = testlib.printHelloInC("bard")
--輸出返回的兩個結果
print(a)
print(b)

調用結果:

小結: ①一樣的在lua中調用函數時,翻譯的時候也會把一個函數的調用分紅參數的壓棧和函數調用兩個部分,因此在c函數中取參數的時候能夠直接取到。 ②其餘部分也和說明調用棧的變化過程當中的例子差很少,也不作重複的解釋了。

相關文章
相關標籤/搜索