c++對象與lua綁定

2015.1.29 wqchen.html

轉載請註明出處 http://www.cnblogs.com/wqchen/p/4261396.htmlc++

 

本文主要探討c++的類對象和lua腳本的綁定使用,讀者須要有必定的lua以及lua的c api接口知識:)。

若是你使用過c/c++和lua混合編程,那麼確定會熟悉宿主(c/c++)與腳本(lua)之間函數的註冊與調用、userdata等等方面知識。宿主對象與腳本的綁定使用,其實能夠看做是
userdata與註冊函數的整合,只不過多了一層語法糖。下面咱們一塊兒來分析一下這層語法糖是怎樣實現的。git

咱們拿lua官方網站的c++對象綁定的示例代碼來分析,你可能曾經或者正在項目裏使用Lunar.h(網址),它是一個輕量級的c++對象綁定接口,簡明易用。本文基於它展開分析。
另外,本文的代碼示例託管在這裏(cppLuaBinder),咱們主要是以代碼加註釋來進行分析。github

首先,來看看Lunar類,它是一個c++模板類:編程

1 //Lunar.h
2 template <typename T> class Lunar {
3   typedef struct { T *pT; } userdataType;
4  public:
5   typedef int (T::*mfp)(lua_State *L); //定義綁定類的成員函數的類型 
6   typedef struct { const char *name; mfp mfunc; } RegType; //一個以結構體數組的形式,把綁定類的須要被註冊的成員函數組合到lua table裏
7   ......
8 };

接下來看看咱們示例代碼裏的要註冊到lua的c++類對象(下文稱之爲綁定類):api

 1 //MsgEx.h
 2 class LuaMsgEx
 3 {
 4 public:
 5   LuaMsgEx(lua_State *pL){
 6     m_pMsgEx = (MsgEx*)lua_touserdata(pL, -1);
 7   }
 8 
 9   int ReadMsg(lua_State* pL); //要註冊的函數符合Luanr模板類的 mfp 類型,事實上這種函數類型也是c與lua之間指定的函數互調協議
10   int WriteMsg(lua_State* pL); //這個也是
11   int Print(lua_State* pL); //這個也是
12   void NotRegisted(); //這個函數打醬油的,不用註冊
13 
14   const static char className [] ; //這是Luar裏指定的綁定類的名字,稍後咱們會知道,lua層會以這個名字註冊一個metatable的
15   static Lunar<LuaMsgEx>::RegType methods[]; //綁定類成員函數的結構體數組形式,包裝是爲了循環處理函數註冊
16 
17 private:
18   MsgEx* m_pMsgEx; //綁定類的實際操做數據對象
19 };

能夠開始註冊類對象了,但在這以前,咱們先要完成綁定類的methods靜態成員的初始化:數組

1 //MsgEx.cpp
2 const char LuaMsgEx::className[] = "LuaMsgEx";
3 Lunar<LuaMsgEx>::RegType LuaMsgEx::methods[] = {
4   LUNAR_DECLARE_METHOD(LuaMsgEx, ReadMsg),
5   LUNAR_DECLARE_METHOD(LuaMsgEx, WriteMsg),
6   LUNAR_DECLARE_METHOD(LuaMsgEx,Print),
7   {0, 0} //這是必須的
8 };

好了,執行類對象註冊綁定:
//main.cpp
Lunar<LuaMsgEx>::Register(L);ide

注:L是指lua虛擬機。上文開始時,咱們提到了「宿主」這個詞,由於lua是「膠水」語言(咱們稱之爲腳本層),因此要「寄宿」在宿主層(c/c++)。在宿主層,咱們要先開啓lua虛擬機也就是lua_State,而後才能在虛擬機上執行lua腳本。函數

使用Lunar.h這麼簡單就完成了對象綁定!可是精彩在於細節啊!下面讓咱們看看詳細註冊細節,這纔是重點。Register()作了什麼事情?請看代碼以及其註釋:網站

 1 static void Register(lua_State *L) {
 2   lua_newtable(L); //1.在虛擬機上註冊table對象,這個能夠看做咱們的綁定類在lua層的映射
 3   int methods = lua_gettop(L); //2.記錄這個新建table的index
 4 
 5   luaL_newmetatable(L, T::className); //3.建立一個userdata元表,到底做爲誰的元表,看下去就知道了
 6   int metatable = lua_gettop(L);//4.記錄元表index
 7 
 8   // store method table in globals so that
 9   // scripts can add functions written in Lua.
10   lua_pushvalue(L, methods); //5.將綁定類table副本壓入棧頂
11   set(L, LUA_GLOBALSINDEX, T::className);//6.將綁定類的名字和副本註冊到lua虛擬機的G[T:className] = methods,此時棧頂還原到4
12 
13   // hide metatable from Lua getmetatable()
14   lua_pushvalue(L, methods);
15   set(L, metatable, "__metatable"); //7.同理,metatable["__metatable"] = methods
16 
17   lua_pushvalue(L, methods);
18   set(L, metatable, "__index");//8.metatable["__index"] = methods
19 
20   lua_pushcfunction(L, tostring_T);
21   set(L, metatable, "__tostring"); //9.metatable["__tostring"] = tostring_T
22 
23   lua_pushcfunction(L, gc_T);
24   set(L, metatable, "__gc"); //10.metatable["__gc"] = gc_T
25 
26   //mt
27   lua_newtable(L); // 11.mt for method table
28   lua_pushcfunction(L, new_T);    // 12.把new_T放到棧頂,後面會用來調用綁定類的構造函數
29   lua_pushvalue(L, -1); // 13.拷貝一個new_T副本到棧頂
30   set(L, methods, "new"); // 14.methods["new"] = new_T,這時棧頂還有一個new_T
31   set(L, -3, "__call"); // 15.index = -3,傳進去set函數後由於要pushstring,因此對應的是11建立的mt,因而mt["__call"] = new_T
32   lua_setmetatable(L, methods);    // 16.把棧頂的mt設置爲methods的元表methods["__metatable"] = mt,此時mt出棧,棧頂就是4建立的metatable了
33 
34   // fill method table with methods from class T,17.註冊成員函數,還記得那個綁定類的那個靜態數組成員變量吧
35   for (RegType *l = T::methods; l->name; l++) {
36     lua_pushstring(L, l->name); 
37     lua_pushlightuserdata(L, (void*)l); //18.把對應的函數壓棧
38     lua_pushcclosure(L, thunk, 1);//19.這裏把thunk函數壓入棧頂,而且把18這個函數做爲它的upvalue,18這個函數會出棧。稍後咱們會知道爲何要這樣作
39     lua_settable(L, methods);//20.好了,把對應的函數名和thunk函數set到methods裏去,完成以後,methods這個lua對象就能夠訪問綁定類的成員函數了
40   }
41 
42   lua_pop(L, 2); // drop metatable and method table, 21.把metatable和methods兩個table彈出棧
43 }

到此爲止,咱們已經完成了c++對象到lua對象的綁定了。那麼怎樣在腳本下使用呢?
這裏以本文的示例代碼爲例,在宿主層開啓虛擬機後,將實際操做的對象壓進虛擬機,這裏所說的「對象」就是你想要操做的數據對象,能夠是lua支持的各類對象類型了。

 1 //main.cpp
 2 int main(int argc, char **argv) {
 3   ......
 4   g_pMsgEx = new MsgEx();
 5   lua_pushlightuserdata(L, g_pMsgEx);//userdata與lightuserdata的區別看這裏
 6   lua_setglobal(L, "_Msg"); //把MsgEx* g_pMsgEx指針以「_Msg」的名字註冊到全局表
 7   ......
 8   luaL_loadfile(L, "my.lua"); //加載my.lua腳本
 9   ......
10   lua_pcall(L, 0, 0, g_nErrFuncIdx);//這一步將會執行my.lua的#1
11   ....
12   lua_getglobal(L, "Init");
13   lua_pcall(L, 0, 1, g_nErrFuncIdx); //g_nErrFuncIdx是lua錯誤處理函數的索引,請參考代碼
14   ...
15 }

下面來看下腳本層代碼:

 1 --my.lua
 2 
 3 g_MsgEx = g_MsgEx or LuaMsgEx(_Msg) --#1
 4 
 5 function NewMsg() 
 6   local o = {
 7     id = 0,
 8     buf = ""
 9   }
10 
11   return o
12 end
13 
14 function Init()
15   -- write 
16   local msg1 = NewMsg()
17   msg1.id = 111
18   msg1.buf = "hello world"
19 
20   g_MsgEx:WriteMsg(msg1) --#2
21   print("write:")
22   g_MsgEx:Print()
23 
24   --read 
25   local msg2 = NewMsg()
26   g_MsgEx:ReadMsg(msg2)
27 
28   print("read")
29   for k,v in pairs(msg2) do 
30     print(k,v)
31   end 
32 
33   g_MsgEx = nil
34   collectgarbage("collect")
35 end

首先有幾點咱們須要清楚的:
1.加載my.lua後,#1會先執行,_Msg就是宿主層註冊到全局表的MsgEx對象的lightuserdata,而LuaMsgEx就是咱們在Register()函數裏步驟6註冊到全局表的methods;
2.由Register()函數,咱們能夠知道methods, metatable,mt三者之間的關係(這三個變量名特指Register()裏在虛擬機中的索引,也指代對應的table):
metatable["__metatable"] = methods
metatable["__index"] = methods
methods["__metatable"] = mt
其中,
methods["new"] = new_T
mt["__call"] = new_T
並且,Register()步驟17把綁定類的成員函數做爲upvalue綁定到了methods下的各個thunk函數;
3.調用LuaMsgEx(_Msg),此時lua底層會觸發OP_CALL事件,而根據methods和mt的關係,會找到mt["__call"]也就是調用new_T函數。這個過程有點複雜,請看下面分析:

來看看new_T函數:

 1 //Lunar.h
 2 
 3 // create a new T object and
 4 // push onto the Lua stack a userdata containing a pointer to T object
 5 static int new_T(lua_State *L) {
 6   lua_remove(L, 1); // use classname:new(), instead of classname.new(),第一個參數是self(即lua層的LuaMsgEx),移除掉
 7   T *obj = new T(L); // call constructor for T objects
 8   push(L, obj, true); // gc_T will delete this object, 設置gc調用時是否能被delete
 9   return 1; // userdata containing pointer to T object
10 }

而後主要看Push()函數:

 1 //Lunar.h
 2 
 3 // push onto the Lua stack a userdata containing a pointer to T object
 4 static int push(lua_State *L, T *obj, bool gc=false) {
 5   if (!obj) { lua_pushnil(L); return 0; }
 6   luaL_getmetatable(L, T::className); // lookup metatable in Lua registry, 查找Register()函數裏註冊的metatable,把它放到棧頂
 7   if (lua_isnil(L, -1)) luaL_error(L, "%s missing metatable", T::className);
 8   int mt = lua_gettop(L); //把metatable的index拿出來
 9   subtable(L, mt, "userdata", "v"); //查找metatable有沒有"userdata",沒有的話建立一個table並設置它元素值爲弱引用,而且這個table的元表是它本身
10   //注意這時棧頂是metatable["userdata"]
11   userdataType *ud =
12     static_cast<userdataType*>(pushuserdata(L, obj, sizeof(userdataType)));//獲取metatable["userdata"][obj],沒有就建立一個userdata
13   //此時棧頂是metatable["userdata"][obj]
14   if (ud) {
15     ud->pT = obj; // store pointer to object in userdata,將咱們的c++對象指針綁定到userdata裏
16     lua_pushvalue(L, mt); //metatable再次入棧
17     lua_setmetatable(L, -2);//將metatable設置爲metatable["userdata"][obj]的元表,metatable彈出
18     if (gc == false) { //若是新建的userdata不被gc
19       lua_checkstack(L, 3);
20       subtable(L, mt, "do not trash", "k"); //metatable["do not trash"]的元素key爲弱引用,而且這個table的元表是它本身,此時棧頂也是它
21       lua_pushvalue(L, -2); //把XX = metatable["userdata"][obj]拷貝到棧頂
22       lua_pushboolean(L, 1);//再壓入一個true值
23       lua_settable(L, -3); //metatable["do not trash"][XX] = true,這是爲了gc啊
24       lua_pop(L, 1); //彈出metatable["do not trash"]
25     }
26   }
27   lua_replace(L, mt); //把metatable["userdata"][obj]彈出,並把它替換掉mt棧位置元素
28   lua_settop(L, mt); //把棧頂指針移動到mt,至關於彈出了metatable["userdata"],如今的棧頂metatable["userdata"][obj],它將被返回給lua層的調用者
29   return mt; // index of userdata containing pointer to T object
30 }

這時,new_T()的返回1,也就是返回棧頂的一個元素,也就是LuaMsgEx(_Msg)返回了metatable["userdata"][obj],即返回給lua層的g_MsgEx,

調用它的函數,其實就是經過它的metatable來實現。

到了這一步,大概弄明白了整個對象綁定是什麼原理了吧?仍是不明白的話,把示例代碼下載下來慢慢調試研究吧。那麼,咱們繼續把剩餘的調用也一併分析下(/頭暈)。

 

當腳本層Init()函數調用#2時,會發生什麼事呢?lua源碼會告訴你,其實它和調用new_T是一個原理。還記得咱們把c++綁定類的成員函數信息做爲upvalue逐個註冊到metatable裏嗎? lua裏調用 g_MsgEx:WriteMsg(msg1)實際上是調用了對應的thunk()。咱們來看看thunk()的實現:

 1 //Lunar.h
 2 
 3 // member function dispatcher
 4 static int thunk(lua_State *L) {
 5   // stack has userdata, followed by method args
 6   T *obj = check(L, 1); // 拿出lua層的LuaMsgEx的userdata,再返回userdataType->pT,也就是咱們調用new_T生成的T*
 7   lua_remove(L, 1); // remove self so member function args start at index 1, 調用棧彈出self後,剩餘的就是其他調用參數
 8   // get member function from upvalue
 9   RegType *l = static_cast<RegType*>(lua_touserdata(L, lua_upvalueindex(1)));//拿出thunk函數的upvalue,就是咱們註冊成員函數的地址
10   return (obj->*(l->mfunc))(L); // call member function, 而後就是用c++對象的函數調用啦,注意L已經移除了lua層的self,調用棧只剩餘其他參數
11 }

因而咱們最終進入到LuaMsgEx::WriteMsg()這個函數來了:

 1 //MsgEx.cpp
 2 int LuaMsgEx::WriteMsg(lua_State* pL) {
 3   Msg* pMsg = m_pMsgEx->GetMsg(); //new_T()已經完成了該成員變量的初始化
 4   assert(pMsg != NULL);
 5 
 6   int nTopOld = lua_gettop(pL); //當前調用棧裏是lua層的table對象,my.lua的#2
 7   //stack top is lua msg table
 8   lua_getfield(pL, -1, "id"); //拿到對應的value到棧頂
 9   int id = luaL_checkinteger(pL, -1);
10   lua_pop(pL, 1); //彈出
11   pMsg->m_msgId = id;
12 
13 
14   lua_getfield(pL, -1, "buf"); //再拿下一個,如此類推
15   const char *str = luaL_checkstring(pL, -1);
16   strcpy_s(pMsg->m_buf, MAX_BUF_LEN, str);
17   lua_pop(pL, 1);
18 
19   assert(lua_gettop(pL) == nTopOld); //注意要保持調用棧迴歸到最初調用的位置
20 
21   lua_pushboolean(pL, true);
22   return 1;
23 }

好了,c++對象與lua腳本的綁定和函數調用全過程大概就是這樣。示例代碼裏的gc和其餘剩餘接口函數暫不討論,也請讀者自行發揮吧。

相關文章
相關標籤/搜索