tolua原理

6.5k 词

tolua++如何将c++对象导入到lua里?lua如何能够访问c++对象成员?

创建一个 userdata ,存放 C/C++ 对象指针,然后给 userdata 添加元表,用indexnewindex 元方法映射 C/C++ 中的对象方法。

  1. c++层新建一个元表作为类型(通过tolua_usertype)放在注册表中(_R[mt] = name),并且设置这些类型的继承关系(通过_R.tolua_super).
  2. 创建一个类表(newtable),并且设置_R中的原型表为元表(通过tolua_usertype),这个类表是放在全局表中的.
  3. 在注册类型的时候metatable里新建了.get和.set表。

​ 成员变量:在.set和.get表里存储以变量名为键一读取设置c函数为值的键值对。

​ 变量的读取赋值会在在metatable的index和newindex里,以变量名为键,从.get和.set表里取得读取设置函数并调用。

​ 成员函数:只需要以函数名为键,函数为值存储在metatable里。

  1. 因为c++类型已经在注册表里,所以可以直接访问。

​ lua里调用c++创建一个对象之后,c++需要返回这个对象:

​ 新建userdata,将c++指针放入userdata;以对象地址为key,userdata为值放入tolua_ubox表里;设置此类型对象的元表为userdata的元表。

经过上面4个步骤,就可以在lua里面调用类表。

传入c++对象的tolua++函数是tolua_pushusertype。一般情况下,第一次使用这个函数将一个c++对象push到lua堆栈上时,才会新建userdata。tolua++会以c++对象地址为键,userdata为值,将键值对存储在tolua_ubox表里。下次推入同样的c++对象时,从这个表里取出userdata推入堆栈即可。

tolua++如何处理类型的继承

父类的metatable,是子类metatable的metatable。这样调用父类方法时,就会去父类的metatable里查找了。

tolua++还维护了一个tolua_super表,这个表以c++类型的metatable为键,以一个表格为值。这个值表格以类型名称为键,以true为值,记录了metatable对应c++类型的父类有哪些。这个表格可以用来帮助判断对象是否是某一个类型。

tolua++如何管理对象的生命周期

一般情况下,当lua里对c++对象的引用变量可以被垃圾回收时,tolua++只是简单的释放userdata占用的4字节指针地址内存。但是也可以通过绑定或者代码指定的方式,让tolua++真正释放对象所占内存。

绑定的方式,是指在将c++类型构造函数使用tolua++导出到lua里时,tolua++会自动生成new_local方法。如果在lua代码里,用这个方法新建对象时,tolua++会调用tolua_register_gc方法,指明回收对象时回收对象内存。

在c++代码里,使用tolua_pushusertype_and_takeownership;在lua代码里,使用tolua.takeownership,都可以达到同样的目的。

对于这些指定由tolua++回收内存的对象,如果其类型的析构函数也通过tolua++导出了,则在回收内存时,会通过delete运算符,调用对象的析构函数。否则只会使用free方法回收。

tolua_register_gc方法,做的事情,是以对象指针为键,以对象metatable为值,将键值对存储在tolua_gc表里。在对象类型的metatable表的__gc方法里,tolua++会检查tolua_gc表是否包含以这个地址为键的表项。包含的话才会进行上述的内存回收工作。

实现

有的时候,在lua里取得一个c++对象后,我们想赋给它一些只在lua环境下有意义的属性。或者,我们想在lua里扩展一个c++类。tolua++也提供了实现这种需求的机制。

tolua++在LUA_REGISTRY里维护了一张tolua_peers表。这张表以表示c++对象的userdata为键,以一张表格t为值。t里面就记录了这个对象在lua里扩展的属性。

原理

C++在进行函数调用的时候是this指针+函数地址, Lua提供用户自定义的userdata.

一般lua中持有c++对象是使用userdata来实现的(userdata 类型用来将任意 C 数据保存在 Lua 变量中. 这个类型相当于一块原生的内存, 除了赋值和相同性判断, Lua 没有为之预定义任何操作.通过使用 metatable (元表), 可以为 userdata 自定义一组操作. metatable 中还可以定义一个函数gc让 userdata 作垃圾收集时调用它。

​ 因此,metatable可以用来模拟C++里面的函数,通过替换它来实现函数,类成员的查找。Userdata可以很方便的获取到转换成C++里面this指针。通过this指针+类的函数地址即可调用C++里面的类成员函数。

过程

tolua_open()创建很多用于管理的内部变量, 如记录所有基类;

tolua_usertype() 创建两个表分别问type和 const type类型, const type 继承自 type;

tolua_cclass()注册类设置元表建立父子类关系;

tolua_beginmodule()注册一个模块

tolua_function()函数绑定到lua表中

tolua_beginmodule()结束模块注册。

tolua_open是入口点,它创建很多用于管理的内部变量,以下用_G指代全局表,_R指定registry table:

1、_R.TOLUA_VALUE_ROOT={}, 这个表是cocos2dx自己加的,它把所有传入lua的cppobj/userdata都塞到这个表里,而且这还不是一个弱表,也就意味着cocos2dx创建的cpp obj,永远都不会被gc!只有在c++层面被delete时,才会去这个表里删除自己。因此,每一个cocos2dx cpp obj,是不可能依赖lua gc来释放的,必须纯手动管理(retain/release)。当然,根据cocos2dx自身的设计,每个obj在new出来refcount为1,并且会添加autorelease pool里去,因此只要它不在场景树上,下一帧就会被自动删除掉。通常情况下,lua代码是不会干涉其生命期的。但是有些时候我们会把某些节点从场景树上摘下,过一段时间又挂上去,这时就必须先调一下它的retain,如果之后忘记调release,那么它就永远不会销毁了。

2、_R.tolua_peers={},这是个弱表,用来缓存所有cppobj的ptr->peer table,所谓peer table,就是给每个cpp obj userdata关联的一个lua table,用来提供lua层面的额外的kv存储。tolua++把每个userdata的peer table设成该userdata的env,目的当然是为了方便找到它。因为在lua实现里,userdata的env是没有内定用途的,于是tolua++就拿来存peer table了。

3、_R.tolua_ubox={},也是个弱表,用来缓存所有cppobj的ptr->userdata映射,userdata里面存放的值其实就是ptr。这个表的用途是记录所有已经push到lua里的cppobj,每个cppobj第一次进入lua时,会去做创建userdata、关联metatable、设置peertable(env)等等一大堆操作,然后把ptr->userdata的映射关系记到这个表里,下次再被返回进lua时,就从这表里去查找,查到的话就直接拿已创建好的userdata用了。但是有一种特殊情况,就是第二次push时的类型是上一次的子类,也就是一个“特化类型”,那么需要改设metatable,以使子类的新函数能被访问到。

4、_R.tolua_super={},用来记录每个类型的所有基类,key是子类的mt,value则是个map,其中每个kv都是一个pair<基类名,1>。通过这个表可以快速知道两个类之间有无继承关系。

5、_R.tolua_gc={}

6、_R.tolua_gc_event = closure{ func:class_gc_event, upvalue:上述两个表格 }, 这是挂在每个类对应的metatable上的__gc方法。

7、_G.tolua={},里面存放tolua自己的一些工具函数

类的注册。

1、对每个用户类,首先要用tolua_usertype声明这是个用户类型:

​ tolua_usertype(tolua_S, “WebSocket”)

它里面的做事情很简单,就是分别为type和const type“两个类”建表(这个表也就是其实例userdata的元表),然后设置type继承const type,从数据的角度来看也就是:

//先用tolua_newmetatable分别创建创建两个元表,其内又调用tolua_classevents挂上各种属性

_R[“WebSocket”]={

__index = cfunc:class_index_event,

__newindex = cfunc:class_newindex_event,

__gc=_R.tolua_gc_event //上面之<一.6>

//其它各种add/sub/lt/eq等方法……

}

_R[“const WebSocket”]= ……同上

//mapsuper(L,type,ctype) 设置两者的继承关系

_R.tolua_super[_R[“WebSocket”]] ={ “const WebSocket” = 1,} //上面之<一.4>

//这个过程会递归执行,即把基类的所有基类都添到子类里

for k,v in pairs(_R.tolua_super[_R[“const WebSocket”]]) do

_R.tolua_super[_R[“WebSocket”]][k] = v

end

//在这一步里,mapsuper只是设置type->const type,但是在后续步骤里会添加大量type->base type,因此递归下来,每个type的_R.tolua_super[type]表还是有很多内容的。

2、然后用tolua_cclass来注册类。

​ tolua_cclass主要做两件事,一是把基类和父类(以及各const变种)之间的关系建立起来,二是注册类的析构函数(构造函数由普通的create静态函数替代了)。

关于继承关系,总共四对:

mapinheritance(L,name,base);

mapinheritance(L,cname,name);

mapsuper(L,cname,cbase);

mapsuper(L,name,base);

其中c指const。除了上面提到的mapsuper,又来了个mapinheritance, 再次对比说明下:

mapsuper是:在_R.tolua_super记录每个类(k)有哪些父类(v),所有父类以v[类名]=1的形式记录着。

mapinheritance是:把父类元表表设成子类元表表的元表,同时给基类表上挂一个用以记录该类objptr->userdata映射的弱表,大致是:

setmetatable( _R.type, _R.base_type ),

_R.type.tolua_ubox = _R.base_type.tolua_ubox or weak({})

其中第二句与前述之<一.3>有点相似,只是那是放在_R上的一个总表,而这里是为每个类单独建表,但子类与基类是共用的,也就是每次调用tolua_cclass注册一个类,就有“3个类”的表中的tolua_ubox字段指向了同一个表,这3个类从上到下是:base type -> type -> const type,至于const base type,那是在之前注册基类时处理的了。

不过这里还有个另外的问题!注意和第2条里的对比:

mapinheritance(L,cname,name) //tolua_cclass里

mapsuper(L,type,ctype) //tolua_usertype里

到底type和const type谁是“基类”呢?这主要看不同场合里“基类”这个概念是用于解决什么问题了:

对mapsuper而言:在c++里一个声明为const的参数,实际是对函数本身的约束而不是对实参的约束:它只是强调函数内部不会去修改这个参数,至于传进来的实参本身是不是const的根本不重要,因为反正函数已经承诺不会去修改它了。所以要把一个类型为type的obj传给某个带有const type参数的函数,是没有问题的,但反过来,一个const type对象要传给接受type参数的函数是不行的,因为不知道它到底会不会修改(没有承诺不修改就意味着会修改)。再加上tolua++在生成胶水代码时,对每个参数都要做类型匹配检测(也就是在生成代码中大量的lua_isusertype调用),一个usertype变量是否合格,就是检查它所在位置的参数类型,是否是它可以“扮演”的角色,这些角色一是它的所有父类,二就是它以及所有父类的const变种了,而这些可以扮演的类型,恰好就是mapsuper所建立的_R.tolua_super体系中记录的内容了。

对mapinheritance而言:它将基类表设成子类表的元表,这是为了在子类表里可以找到基类的函数(在class_index_event函数里,有一个while循环,通过这里建立的链条不断向上级查找)。就这个目的而言,type和const type谁做基类是一样的。但是,type还有真正的基类base,按照base->type->const type的继承顺序是恰好满足的:

classA -> const classA

​ -> classB -> const classB

​ -> classC -> const classC

​ -> classD -> const classD

也就是说const类型在当前层次的链上是一个叶,下一层次不是从它继承,而是与它并级。

而如果按照base->const type->type->const subtype->sub type,那么问题就麻烦了,因为在注册函数时,所有函数都是挂在不带const的类表上的,如CCNode的函数都在_R[“CCNode”]里,这也符合脚本里创建类实例时的习惯:直接以“纯粹的”(不带const的)类名来操作,比如CCNode:create,而不会写作const_CCNode:create()。那么在后者的继承链上,每一个const type实际成了断点,没有得到这个type自身的函数!

3.tolua_beginmodule() tolua_endmodule() tolua_function()

tolua_beginmodule(m_pState, “CTest”);是只注册一个模块,比如,我们管CTest叫做”CTest”,保持和C++的名称一样。这样在Lua的对象库中就会多了一个CTest的对象描述,等同于string,number等等基本类型,同tolua_beginmodule()和tolua_endmodule()对象必须成对出现,如果出现不成对的,你注册的C++类型将会失败。

tolua_function(m_pState, “SetData”, tolua_SetData_CTest);指的是将Lua里面CTest对象的”SetData”绑定到你的tolua_SetData_CTest()函数中去。