c++对象与lua绑定

2015.1.29 wqchen.

转载请注明出处 http://www.cnblogs.com/wqchen/

本文主要探讨c++的类对象和lua脚本的绑定使用,读者需要有一定的lua以及lua的c api接口知识:)。

如果你使用过c/c++和lua混合编程,那么肯定会熟悉宿主(c/c++)与脚本(lua)之间函数的注册与调用、userdata等等方面知识。宿主对象与脚本的绑定使用,其实可以看作是
userdata与注册函数的整合,只不过多了一层语法糖。下面我们一起来分析一下这层语法糖是怎样实现的。

我们拿lua官方网站的c++对象绑定的示例代码来分析,你可能曾经或者正在项目里使用Lunar.h(网址),它是一个轻量级的c++对象绑定接口,简明易用。本文基于它展开分析。
另外,本文的代码示例托管在这里(cppLuaBinder),我们主要是以代码加注释来进行分析。

首先,来看看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++类对象(下文称之为绑定类):

 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);

注: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和其他剩余接口函数暂不讨论,也请读者自行发挥吧。

时间: 2024-10-12 14:31:40

c++对象与lua绑定的相关文章

用LuaBridge为Lua绑定C/C++对象

最近为了总结Lua绑定C/C++对象的各种方法.第三方库和原理,学习了LuaBridge库为Lua绑定C/C++对象,下面是学习笔记,实质是对该库的Reference Manual基本上翻译了一遍,学习过程中测试代码,放在我的github上. LuaBridge的主要特点 源码只有头文件,没有.cpp文件,没有MakeFile,使用时只需一个#include即可. 支持不同的对象生命周期管理模式. 对Lua栈访问方便并且是类型安全的(type-safe). Automatic function

lua绑定C++对象系列一——基础知识

本文主要介绍lua绑定C++对象的原理和方法,并能在C/C++定义类和方法,在lua中创建C++类的句柄实例,像面向对象一样去使用C++类实例.为了便于大家理解,系列文章会从基础知识讲解,并通过多个版本的进化,一步步完成从基础到多版本实践的完美结合和深入,彻底理解lua绑定C++对象的原理方法.在阅读本系列文章前,需要具备一定的lua开发经验以及lua与C/C++相互调用操作的知识. 1.基础C/C++和Lua的相互引用调用 我们知道C和lua相互调用,是通过虚拟栈进行数据传递通信的,基础介绍介

cocos2dx v3.x lua绑定分析

打算新项目转到cocos2dx v3上了,下载代码浏览过后发现改动真是非常大,结构性调整很多. 比如tolua绑定这一块,就几乎全翻新了. 胶水代码的生成,改成了全自动式的,通过clang来分析c++代码,可以准确的知道每一个类.函数.参数的信息,再也不用手动写pkg文件了. 运行期对象管理这块,似乎也有了不少改动,至少我原来的一些扩展代码运行不了了,还没来得及细看,待看完再一一录下. 先记录一下目前已看清楚的[类名表.类元表.对象实例]之间的关系: 1.类元表:最核心的表,在lua代码里是不可

cocos2d-x lua绑定解析

花了几天时间看了下cocos2d-x lua绑定那块,总算是基本搞明白了,下面分三部分解析lua绑定: 一.lua绑定主要用到的底层函数 lua绑定其本质就是有一个公用的lua_Stack来进行C和Lua之间的值传递,在路径[项目根目录]\frameworks\cocos2d-x\external\lua\luajit\include下有个lua.h文件,大部分lua绑定底层函数以及相关的常量都在这里. 1.lua堆栈常量 #define LUA_REGISTRYINDEX (-10000) /

cocos2dx lua 绑定之二:手动绑定自定义类中的函数

cococs2dx 3.13.1 + vs2013 + win10 1.首先按照<cocos2dx lua 绑定之一:自动绑定自定义类>绑定Student类 2.在Student类中增加一个用于测试手动绑定的函数manual_call ①Student.h中增加函数 //手动绑定调用函数 void manual_call(); ②Student.cpp中增加函数实现 //和自动绑定相比,只增加了这个函数 void Student::manual_call() { std::cout <&

使用cocos2d脚本生成lua绑定

这几天要老大要求把DragonBones移到cocos2dx 3.0 里边,并且绑定lua使用接口.因为刚学lua,使用的引擎也刚从2.2改为3.0,各种不熟悉,折腾了好几天才弄完,有空了总结一下 这篇先说一下cocos2d生成lua绑定的修改,有空的话再写一篇lua中注册回调到c++中方法 我的目录结构 假设我的目录名称是DragonBones -Cocosdx目录 -DragonBones  -c代码 -c代码头 -tools  db_DragonBones.ini genbindings.

[tolua++]tolua++中暴露对象给lua时,一定要把析构函数暴露给lua

题目不知道怎么取才好,但是意思很简单: 如果你暴露一个复杂对象给Lua,实现类似于OO编程的话,那么也要把析构函数暴露给Lua. 否则的话,lua gc的时候,回收垃圾对象,没有找到回收函数,就直接free掉了,这在C++中,是相当致命的. tolua++中的tolua_cclass函数,用来注册lua对象, TOLUA_API void tolua_cclass (lua_State* L, const char* lname, const char* name, const char* ba

【转】如何做dragonbones的lua绑定(VisualStudio)

原文:<如何做dragonbones的lua绑定(VisualStudio)>(不完善和错误的地方我已做红字修改) 最近好多同学在QQ群里问怎么在lua项目中使用DB(DrgonBones,龙骨),为了帮助更多的人,同时也好让更多的人跟容易使用DB,这里详细记录coco2dx-3.2版本对应DB的lua绑定. 首先要说明下,本文章对应的cocos2dx-3.2版本,其他cocos2dx-3.x版本跟3.2版本类似.这里假设自己使用cocos命令行创建的lua项目,而且没有修改过目录结构,如果修

【转】如何做dragonbones的lua绑定(Android)

这篇写dragonbones的lua绑定之Android部分,不知道怎么在VS(Visual Studio)中绑定的话请看如何在lua项目中使用dragonbones.有了上篇的基础,这次再做Android就比较简单了.注意:ndk9b不能编译通过,我这里使用的是ndk9d,其他版面没有测试. 修改 Application.mk 文件路径:MyLuaGame/frameworks/runtime-src/proj.android/jni/Application.mk 添加预定义宏 APP_CPP