ToLua:逐行分析源碼,搞清楚Wrap文件原理

閱前提示

該篇文章主要結合tolua.c 源碼,逐行逐句分析ToLua中Wrap文件的運作原理。
適合人羣:lua使用人羣
閱讀方式:瀏覽
擴展閱讀:Lua C語言API


Tolua

開門見山,Tolua懂的都懂,這裏就不贅述了。
有關Tolua介紹和使用的文章很多,例如:點擊查看

Wrap

在Tolua生成的Wrap文件中,經常遇見如此寫法。

// XXXWrap.cs
L.BeginStaticLibs("XXX");
L.RegFunction("Log", Log); 
L.EndStaticLibs();

就很神奇的可以使用Lua來調用這些C#類的方法了。
接下來作者便深入其中,逐一分析這三句話究竟做了些什麼。


BeginStaticLibs

首先BeginStaticLibs最終會走到這條語句上來LuaDLL.tolua_beginstaticclass(L, name);
這語句會執行tolua dll 的C語言對應方法,tolua_beginstaticclass ,該方法 最終會在lua_State棧頂生成一個名爲name的table

/* tolua.c */
LUALIB_API void tolua_beginstaticclass(lua_State *L, const char *name)
{    
    lua_pushstring(L, name);  /* 將name 壓入棧中,即 XXX */ 
    lua_newtable(L);/* 創建一個table 壓入棧中*/
    _addtoloaded(L);
    lua_pushvalue(L, -1);  
    /* 這裏將棧頂表複製了一份壓入棧中 即top [XXX(table),XXX(emptytable)] bottom    */
	/* 以上操作相當於生成了名爲name的table :XXX = {}  */
    
    /* 以下操作抽象理解 */
    /* XXX["userdata"] = &tag  */
    lua_pushlightuserdata(L, &tag);
    lua_pushnumber(L, 1);
    lua_rawset(L, -3);

    /* XXX[".name"] = XXX  */
    lua_pushstring(L, ".name");
    _pushfullname(L, -4);
    lua_rawset(L, -3);
	
    /* XXX["__index"] = static_index_event  */
    lua_pushstring(L, "__index");
    lua_pushcfunction(L, static_index_event);
    lua_rawset(L, -3);

    /* XXX["__newindex"] = static_newindex_event  */
    lua_pushstring(L, "__newindex");
    lua_pushcfunction(L, static_newindex_event);
    lua_rawset(L, -3);      
}

L.BeginStaticLibs("XXX");
這裏最終會在lua_State棧頂會被壓入一個名爲XXX的table

棧頂
XXX(table)
XXX(emptytable)
棧底

EndStaticLibs

接着先把尾給收掉,EndStaticLibs最後會來到tolua.c這邊的 tolua_endstaticclass 方法,該方法最終會將棧頂元素彈出並將其設置爲 - 2位置的元表

/* tolua.c */
LUALIB_API void tolua_endstaticclass(lua_State *L)
{
    lua_setmetatable(L, -2);
    lua_rawset(L, -3);    
}

L.EndStaticLibs();
//結束該table,彈出棧頂元素,將其設置爲XXX(emptytable)的元表
like:top(棧頂) [XXX(metatable)] bottom(棧底)

棧頂
XXX(metatable)
棧底

RegFunction

重點來了,C#方法的註冊
RegFunction先是將要註冊的方法轉換成了供平臺使用的指針,傳遞到C中生成可以供lua使用的LuaCSFunction函數

// LuaState.cs
public void RegFunction(string name, LuaCSFunction func)
{
    IntPtr fn = Marshal.GetFunctionPointerForDelegate(func); 
    LuaDLL.tolua_function(L, name, fn);            
}

tolua.c文件中tolua_function對傳入進的函數進行了綁定。

/* tolua.c */
LUALIB_API void tolua_function(lua_State *L, const char *name, lua_CFunction fn)
{
  	lua_pushstring(L, name);
    tolua_pushcfunction(L, fn);
  	lua_rawset(L, -3);
}
  • lua_pushstring(L, name):壓入name => top [name,XXX(table)] bottom
  • tolua_pushcfunction(L, fn) : 壓入方法fn
    棧: [CClosure(f:tolua_closure,upvalue[0(false),CClosure(f:fn)]),name,XXX(table)] 下面內容會重點分析這部分究竟做了些什麼
  • lua_rawset(L, -3) :賦值操作:將棧頂作爲v:CClosure,倒數第二位作爲k:name,-3位置作爲table進行賦值 ,即 XXX[name] = CClosure

C函數只要滿足lua_CFunction的樣子就可以被lua所使用。

/* tolua.c */
LUA_API int tolua_pushcfunction(lua_State *L, lua_CFunction fn)
{        
    lua_pushboolean(L, 0); 
    lua_pushcfunction(L, fn);
    lua_pushcclosure(L, tolua_closure, 2);
    return 0;
}
  • lua_pushboolean(L, 0):壓入布爾值false top [0(false),name,XXX(table)] bottom
  • lua_pushcfunction(L, fn) :壓入函數,這裏會以 CClosure結構體的形式被壓入棧
  • lua_pushcclosure(L, tolua_closure, 2) : 這裏會創建一個新的CClosure結構,並將棧頂2個元素(一個布爾值與一個存放了fn的Closure)彈出並壓入CClosure結構體中,最終將新的CClosure壓入棧中

此時,lua_State棧中的表現是這樣的

棧頂
CClosure(f:tolua_closure,upvalue[0(false),CClosure(f:fn)])
name:Log
XXX(table)
棧底

最終在lua_rawset(L, -3)的作用下,變爲了這樣

棧頂
XXX : {Log = CClosure …}
棧底

到此,Tolua Wrap文件如何向Lua中註冊C#方法的過程就完畢了。實際上我們會發現,一個C#方法的指針其實被封裝了兩層:

  • lua_pushcfunction(L, fn) 時 將fn封裝進了CClosure中。
  • lua_pushcclosure(L, tolua_closure, 2) 時將封裝了fnCClosure再一次封裝進了新的CClosure中。

當我們在lua中調用註冊的方法時,實際上是在調用最外層的CClosure結構體,它其中的方法是 tolua_closure,而我們的C#方法指針fn作爲該結構體棧中的值被存放着(upvalue)

static int tolua_closure(lua_State *L)
{
	/*  獲取到我們所註冊的C#方法指針 */
    lua_CFunction fn = (lua_CFunction)lua_tocfunction(L, lua_upvalueindex(2));
    /*  運行 ,因爲lua跨語言的數據交互都藉助棧來完成,所以運行結果都是通過獲取棧中元素來獲得 */
    int r = fn(L);    
    
    if (lua_toboolean(L, lua_upvalueindex(1)))
    {
        lua_pushboolean(L, 0);
        lua_replace(L, lua_upvalueindex(1));
        return lua_error(L);
    }
    
    return r;
}

lua_pushcfunction

實際上壓入函數的過程就是形成閉包的過程,在lua中函數是以閉包的形式被保存的

/* lua.h */
#define lua_pushcfunction(L,f)	lua_pushcclosure(L, (f), 0) /* 宏 壓入閉包方法 n爲0 */

lua_pushcclosure

void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n);

Pushes a new C closure onto the stack.

When a C function is created, it is possible to associate some values with it, thus creating a C closure (see §3.4); these values are then accessible to the function whenever it is called. To associate values with a C function, first these values should be pushed onto the stack (when there are multiple values, the first value is pushed first). Then lua_pushcclosure is called to create and push the C function onto the stack, with the argument n telling how many values should be associated with the function. lua_pushcclosure also pops these values from the stack.
官方地址

lua_pushcclosure:生成閉包,將函數存放在閉包結構體中,並將棧頂n個元素一同壓入閉包內的棧

/* lua.h */
LUA_API void  (lua_pushcclosure) (lua_State *L, lua_CFunction fn, int n);
/* lapi.c */
LUA_API void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n) {
  lua_lock(L);
  if (n == 0) {
    setfvalue(L->top, fn);
    api_incr_top(L);
  }
  else {
    CClosure *cl;  
    api_checknelems(L, n);
    api_check(L, n <= MAXUPVAL, "upvalue index too large");  /* #define MAXUPVAL 255  */
    cl = luaF_newCclosure(L, n);/* 創建了閉包結構體  */
    cl->f = fn;
    L->top -= n;  /* 將棧頂n個元素移除並壓入閉包的棧中 upvalue */
    while (n--) {
      setobj2n(L, &cl->upvalue[n], L->top + n);
      /* does not need barrier because closure is white */
    }
    setclCvalue(L, L->top, cl);
    api_incr_top(L);
    luaC_checkGC(L);
  }
  lua_unlock(L);
}
typedef struct CClosure {
  ClosureHeader;
  lua_CFunction f;
  TValue upvalue[1];  /* list of upvalues */
} CClosure;

.
.
.
.
.


嗨,我是作者Vin129,逐兒時之夢正在遊戲製作的技術海洋中漂泊。知道的越多,不知道的也越多。希望我的文章對你有所幫助:)


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章