程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 淺析C++綁定到Lua的方法

淺析C++綁定到Lua的方法

編輯:C++入門知識

淺析C++綁定到Lua的方法


注:原文也在公司內部論壇上發了
概述
雖然將C++對象綁定到Lua已經有tolua++(Cocos2d-x 3.0用的就是這個)、LuaBridge(我們游戲客戶端對這個庫進行了改進)和luabind等各種庫可以直接使用了(lua-users.org上有對各種語言綁定到lua庫的匯總),但弄清楚C++對象綁定到Lua的常見方法,不但有助於更深的了解Lua的機制,還可以方便修改第三方庫以滿足實際項目需求。本文通過分析第三方庫Lunar(我們游戲服務端用的是Luna,Lunar是Luna增加版,但仍然足夠簡潔)的實現,來理解C++對象綁定到Lua的通常方法。Lunar的測試代碼放在我的github上。 Lunar實現的功能以及原理
Lunar實現非常簡潔,同時實現了C++綁定到Lua主要功能。使用Lunar至少可以做到以下幾點: 1、在腳本中,可以使用注冊過的類創建C++對象,此類C++對象,由Lua控制何時釋放。 2、在C++創建的對象,可以壓入棧中,供腳本使用這個對象,並且提供一個可選參數,來決定這個對象是由C++控制釋放,還是Lua控制釋放。 3、腳本中的C++類對象,可以調用注冊過的類成員函數。 4、在C++中,可以獲取在腳本中創建的對象,並且在C++中可以調用這個對象的成員函數。 5、可以在腳本中定義對象的成員函數,並且能在C++中調用這些用腳本實現的成員函數。 在腳本中創建C++對象,實質返回給腳本是一個userdata類型的值ud,ud中保存一個指針,該指針指向所創建的C++對象。這時候Lua控制對象何時釋放,即在Lua在回收userdata時,同時利用userdata的元表__gc字段來回收動態分配的對象, 這時候就不需要C++釋放內存了。注意這裡返回給腳本不能是lightuserdata,因為lightuserdata實質上只是一個指針,不受垃圾回收收集器的管理,並且它也沒有元表。 在腳本中創建的對象是由Lua來維護對象的生命周期。在Lunar中還可以使用Lunar::push(lua_State *L, T *obj, bool gc=false)向棧中壓入對象供腳本使用,其中第三個參數可以決定創建的對象是由C++控制釋放,還是Lua控制釋放。 其原理是在把要傳遞給Lua的對象obj壓入棧時,它首先利用對象地址在lookup表中查找(lookup是一個mode為"v"的weak table,保存所有在腳本中使用的對象,該表的key是對象地址,value是對象對應的userdata),若不在lookup中,則會創建一個新的userdata,並把它保存在lookup中,若第三個參數為false,即由C++控制對象釋放,還會把上面的userdata保存在一個nottrash表中,nottrash是一個mode為"k"的weak table,保存所有不會隨userdata回收其相應對象也釋放的userdata,該表key為userdata,value為true。這樣處理後,在Lua回收userdata時,首先檢測userdata是否在nottrash中,若不在,則刪除userdata所指向對象,否則需要C++自己釋放所創建的對象。 在調用Lunar::Register(lua_State *L)向腳本注冊類時,會創建兩個表,一個是methods表,該表的key為函數名,value為函數地址(函數可以是C++中定義類的方法,也可以在Lua中定義函數),在Lua中調用的對象方法,都保存在該表中;另一個是metatable表,做為userdata的元表,該metatable表保存了上面的lookup表和nottrash表,並且設置了metatable.__metatable = methods,這樣在腳本中就隱藏了metatable表,也就是說在Lua中,調用getmetatable(userdata)得到的是methods表,而不是metatable表,在腳本中,給對象添加成員方法也是會保存在methods表中。 Lunar源碼分析
下面逐行分析了Luanr的實現,在附件中是Lunar的測試代碼,如下: 001 extern "C" { 002 #include "lua.h" 003 #include "lauxlib.h" 004 } 005 006 #define LUNAR_DECLARE_METHOD(Class, Name) {#Name, &Class::Name} 007 008 template <typename T> class Lunar { 009 public: 010 011 //C++可以向Lua注冊的函數類型 012 typedef int (T::*mfp)(lua_State *L); 013 014 //向Lua中注冊的函數名字以及對應的函數地址 015 typedef struct { const char *name; mfp mfunc; } RegType; 016 017 //用來注冊C++定義的類供Lua使用 018 static void Register(lua_State *L) { 019 //創建method table,該table key為函數名, 020 //value為函數地址(函數可以是C++中定義類的方法,也可以在Lua中定義函數) 021 //在Lua中,以table的key為函數名,調用相應的方法。 022 lua_newtable(L); 023 int methods = lua_gettop(L); 024 025 //創建userdata的元表 026 luaL_newmetatable(L, T::className); 027 int metatable = lua_gettop(L); 028 029 //把method table注冊到全局表中,這樣在Lua中可以直接使用該table, 030 //這樣可以在這個table中增加Lua實現的函數 031 lua_pushvalue(L, methods); 032 set(L, LUA_GLOBALSINDEX, T::className); 033 034 //隱藏userdata的實質的元表,也就是說在Lua中 035 //調用getmetatable(userdata)得到的是methods table,而不是metatable table 036 lua_pushvalue(L, methods); 037 set(L, metatable, "__metatable"); 038 039 //設置metatable table的元方法 040 lua_pushvalue(L, methods); 041 set(L, metatable, "__index"); 042 043 lua_pushcfunction(L, tostring_T); 044 set(L, metatable, "__tostring"); 045 046 //設置__gc元方法,這樣方便在Lua回收userdata時, 047 //可以做一些其他操作,比如釋放其相應的對象 048 lua_pushcfunction(L, gc_T); 049 set(L, metatable, "__gc"); 050 051 lua_newtable(L); //創建methods的元表mt 052 lua_pushcfunction(L, new_T); 053 lua_pushvalue(L, -1); // 把new_T再次壓入棧頂 054 set(L, methods, "new"); // 把new_T函數加入到methods中,這樣腳本可通過調用T:new()來創建C++對象 055 set(L, -3, "__call"); // mt.__call = mt,這樣腳本可以通過調用T()來創建C++對象 056 lua_setmetatable(L, methods); //設置methods的元表為mt 057 058 //把類T中的方法保存到method table中,供Lua腳本使用 059 for (RegType *l = T::methods; l->name; l++) { 060 lua_pushstring(L, l->name); 061 lua_pushlightuserdata(L, (void*)l); //以注冊函數在數組的位置作為cclosure的upvalue 062 lua_pushcclosure(L, thunk, 1); //在Lua調用的類方法,調用的都是c closure thunk,thunk通過upvalue獲取實質調用的函數地址 063 lua_settable(L, methods); 064 } 065 066 lua_pop(L, 2); //彈出methods和metatable table,保證Register調用後,棧的空間大小不變 067 } 068 069 //調用保存在method table中的函數 070 //在調用call之前,需要向棧中壓入userdata和參數, 071 //並把最後的調用結果壓入棧中,參數method傳入要調用的函數名 072 static int call(lua_State *L, const char *method, 073 int nargs=0, int nresults=LUA_MULTRET, int errfunc=0) 074 { 075 int base = lua_gettop(L) - nargs; //獲取userdata在棧中的索引 076 if (!luaL_checkudata(L, base, T::className)) { 077 //如果用錯誤的類型調用相應的方法,則從棧中彈出userdata和參數 078 //並且壓入相應的錯誤信息 079 lua_settop(L, base-1); 080 lua_pushfstring(L, "not a valid %s userdata", T::className); 081 return -1; 082 } 083 084 lua_pushstring(L, method); //壓入方法名,通過該名字在userdata method table中獲取實質要調用的c closure 085 086 //獲取對應的函數地址,其流程是從userdata的元表metatable查找, 087 //而metatable.__index=methods,在methods中通過方法名,獲取相應的方法 088 lua_gettable(L, base); 089 if (lua_isnil(L, -1)) {//若不存在相應的方法 090 lua_settop(L, base-1); 091 lua_pushfstring(L, "%s missing method '%s'", T::className, method); 092 return -1; 093 } 094 lua_insert(L, base); // 把方法移到userdata和args下面 095 096 int status = lua_pcall(L, 1+nargs, nresults, errfunc); // 調用方法 097 if (status) { 098 const char *msg = lua_tostring(L, -1); 099 if (msg == NULL) msg = "(error with no message)"; 100 lua_pushfstring(L, "%s:%s status = %d\n%s", 101 T::className, method, status, msg); 102 lua_remove(L, base); // remove old message 103 return -1; 104 } 105 return lua_gettop(L) - base + 1; // 調用的方法,返回值的個數 106 } 107 108 //向棧中壓入userdata,該userdata包含一個指針,該指針指向一個類型為T的對象 109 //參數obj為指向對象的指針,參數gc默認為false,即Lua在回收userdata時,不會主動是釋放obj對應的對象,此時應用程序負責相應對象釋放 110 //若為true,則Lua在回收userdata時,會釋放相應的對象 111 static int push(lua_State *L, T *obj, bool gc=false) { 112 if (!obj) { lua_pushnil(L); return 0; } 113 luaL_getmetatable(L, T::className); //在注冊表中獲取類名的對應的table mt,以作為下面userdata的元表 114 if (lua_isnil(L, -1)) luaL_error(L, "%s missing metatable", T::className); 115 int mt = lua_gettop(L); 116 117 //設置mt["userdata"] = lookup,並向棧頂壓入lookup,lookup是一個mode為"v"的weak table,保存所有類對象對應的userdata 118 //key是對象地址,value是userdata 119 subtable(L, mt, "userdata", "v"); 120 userdataType *ud = 121 static_cast(pushuserdata(L, obj, sizeof(userdataType))); //向棧頂壓入一個userdata 122 if (ud) { 123 ud->pT = obj; //把對象的地址obj保存到userdata中 124 lua_pushvalue(L, mt); //壓入注冊表中類名對應的table mt 125 lua_setmetatable(L, -2); //設置userdata的元表 126 if (gc == false) { 127 //gc為false,Lua在回收userdata時,不會主動是釋放obj對應的對象,此時應用程序負責相應對象釋放 128 lua_checkstack(L, 3); 129 130 //mt["do not trash"] = nottrash,nottrash是一個mode為"k"的weak table,保存所有不會隨userdata回收其相應對象也釋放的userdata 131 //key是userdata,value是true,向棧頂壓入nottrash 132 subtable(L, mt, "do not trash", "k"); 133 lua_pushvalue(L, -2); //再次壓入userdata 134 lua_pushboolean(L, 1); 135 lua_settable(L, -3); //nottrash[userdata] = true 136 lua_pop(L, 1); //把nottrash從棧中彈出 137 } 138 } 139 lua_replace(L, mt); //把索引mt出元表值替換為userdata 140 lua_settop(L, mt); //設置棧的大小,即通過調用push()調用,棧頂元素為userdata,該userdata包含指向對象的指針 141 return mt; //返回userdata在棧中的索引 142 } 143 144 //檢測索引narg處的值是否為相應的userdata,若是則返回一個指針,該指針指向類型T的對象 145 static T *check(lua_State *L, int narg) { 146 userdataType *ud = 147 static_cast(luaL_checkudata(L, narg, T::className)); 148 if(!ud) { 149 luaL_typerror(L, narg, T::className); 150 return NULL; 151 } 152 return ud->pT; 153 } 154 155 private: 156 157 typedef struct { T *pT; } userdataType; 158 159 Lunar(); //隱藏默認的構造函數 160 161 //Lua中調用類的成員函數,都是通過調用該函數,然後使用userdataType的upvalue來調用實質的成員函數 162 static int thunk(lua_State *L) { 163 //此時棧中元素是userdata和參數 164 T *obj = check(L, 1); //檢測是否是相應的userdata,若是,返回指向T對象的指針 165 lua_remove(L, 1); //從棧中刪除userdata,以便成員函數的參數的索引從1開始 166 //利用upvalue獲取相應的成員函數 167 RegType *l = static_cast(lua_touserdata(L, lua_upvalueindex(1))); 168 return (obj->*(l->mfunc))(L); //調用實質的成員函數 169 } 170 171 //創建一個新的對象T,在腳本中調用T()或T:new(),實質調用的都是該函數 172 //調用後,棧頂元素為userdata,該userdata包含指向對象的指針 173 static int new_T(lua_State *L) { 174 lua_remove(L, 1); // 要求在腳本中使用T:new(),而不能是T.new() 175 T *obj = new T(L); // 調用類T的構造函數 176 push(L, obj, true); // 傳入true,表明Lua回收userdata時,相應的對象也會刪除 177 return 1; 178 } 179 180 //Lua在回收userdata時,相應的也會調用該函數 181 //根據userdata是否保存在nottrash(即mt["do not trash"],mt為注冊表中T:classname對應的table)中來決定 182 //是否釋放相應的對象,若在,則不釋放相應的對象,需要應用程序自己刪除,否則刪除相應的對象 183 static int gc_T(lua_State *L) { 184 if (luaL_getmetafield(L, 1, "do not trash")) { 185 lua_pushvalue(L, 1); //再次壓入userdata 186 lua_gettable(L, -2); //向棧中壓入nottrash[userdata] 187 if (!lua_isnil(L, -1)) return 0; //在nottrash中,不刪除相應的對象 188 } 189 userdataType *ud = static_cast(lua_touserdata(L, 1)); 190 T *obj = ud->pT; 191 if (obj) delete obj; //刪除相應的對象 192 return 0; 193 } 194 195 //在Lua中調用tostring(object)時,會調用該函數 196 static int tostring_T (lua_State *L) { 197 char buff[32]; 198 userdataType *ud = static_cast(lua_touserdata(L, 1)); 199 T *obj = ud->pT; 200 sprintf(buff, "%p", (void*)obj); 201 lua_pushfstring(L, "%s (%s)", T::className, buff); 202 203 return 1; 204 } 205 206 //設置t[key]=value,t是索引為table_index對應的值,value為棧頂元素 207 static void set(lua_State *L, int table_index, const char *key) { 208 lua_pushstring(L, key); 209 lua_insert(L, -2); //交換key和value 210 lua_settable(L, table_index); 211 } 212 213 //在棧頂壓入一個模式為mode的weak table 214 static void weaktable(lua_State *L, const char *mode) { 215 lua_newtable(L); 216 lua_pushvalue(L, -1); 217 lua_setmetatable(L, -2); //創建的weak table以自身作為元表 218 lua_pushliteral(L, "__mode"); 219 lua_pushstring(L, mode); 220 lua_settable(L, -3); // metatable.__mode = mode 221 } 222 223 //該函數向棧中壓入值t[name],t是給定索引的tindex的值, 224 //若原來t[name]值不存在,則創建一個模式為mode的weak table wt,並且賦值t[name] = wt 225 //最後棧頂中壓入這個weak table 226 static void subtable(lua_State *L, int tindex, const char *name, const char *mode) { 227 lua_pushstring(L, name); 228 lua_gettable(L, tindex); 229 if (lua_isnil(L, -1)) { 230 lua_pop(L, 1); //彈出nil 231 lua_checkstack(L, 3); //檢測棧的空間,是否足夠 232 weaktable(L, mode); //棧頂壓入一個指定模式的weak table 233 lua_pushstring(L, name); 234 lua_pushvalue(L, -2); //再次壓入創建的weak table 235 lua_settable(L, tindex); //t[name] = wt 236 } 237 } 238 239 //向棧頂壓入lookup[key],lookup是一個weak table,保存所有類對象對應的userdata 240 //key是對象地址,value是userdata,若不存在則創建一個userdata,並賦值lookup[key] = userdata 241 //這樣使得在腳本中引用過的對象,就不會創建新的userdata了 242 static void *pushuserdata(lua_State *L, void *key, size_t sz) { 243 void *ud = 0; 244 lua_pushlightuserdata(L, key); //創建一個light userdata 245 lua_gettable(L, -2); // 查找lookup[key]是否存在 246 if (lua_isnil(L, -1)) { 247 lua_pop(L, 1); //彈出nil 248 lua_checkstack(L, 3); //檢測棧的空間 249 ud = lua_newuserdata(L, sz); //創建一個userdata 250 lua_pushlightuserdata(L, key); 251 lua_pushvalue(L, -2); //再次壓入userdata 252 lua_settable(L, -4); //lookup[key] = userdata 253 } 254 return ud; 255 } 256 };

總結


根據實際項目需要,可能還需要其他功能,比如在腳本中獲取和設置對象的成員變量、處理類的繼承等需求。只要理解C++對象綁定到Lua方法,就可以方便按需要擴展,或者快速修改第三方庫以滿足需求。

參考資料


http://lua-users.org/wiki/CppBindingWithLunar http://lua-users.org/wiki/LunaFive http://lua-users.org/wiki/BindingCodeToLua https://github.com/vinniefalco/LuaBridge http://www.codenix.com/~tolua/

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved