zoukankan      html  css  js  c++  java
  • 浅析C++绑定到Lua的方法

    注:原文也在公司内部论坛上发了 
    概述
          尽管将C++对象绑定到Lua已经有tolua++(Cocos2d-x 3.0用的就是这个)、LuaBridge(我们游戏client对这个库进行了改进)和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 %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      查看全文
  • 相关阅读:
    javascript 笔记
    小程序组件 Vant Weapp 安装
    vue学习笔记——脚手架安装
    [二分] [计算几何] AtCoder Beginner Contest 144 D Water Bottle
    [单调队列][前缀和][滑窗][Codeforces] Round #594 (Div. 2) D1 The World Is Just a Programming Task
    [Codeforces] 592 div2 A B D E
    [Codeforces] Round #595 (Div. 3) A B1 B2 C1 C2 D1 D2 E
    [Codeforces] Global Round 5 A C1 C2 D
    [主席树单点更新区间极值动态开点][最长上升子序列] CodeForces 474 F. Pathwalks
    [思维]挖矿
  • 原文地址:https://www.cnblogs.com/yxysuanfa/p/7077574.html
  • Copyright © 2011-2022 走看看