码迷,mamicode.com
首页 > 编程语言 > 详细

浅析C++绑定到Lua的方法

时间:2014-11-06 20:04:14      阅读:293      评论:0      收藏:0      [点我收藏+]

标签:lua   c   c++   绑定   

注:原文也在公司内部论坛上发了 
概述
      虽然将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<T>::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<T>::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<userdataType*>(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<userdataType*>(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<RegType*>(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<userdataType*>(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<userdataType*>(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/

浅析C++绑定到Lua的方法

标签:lua   c   c++   绑定   

原文地址:http://blog.csdn.net/maximuszhou/article/details/40866507

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!