最近在用lua写游戏,用的是quick-cocos2dx,研究了一下它的机制,或者说是tolua++的机制吧,怕以后忘了,写在这儿做下记录。
首先,lua最强大的类型就是table,它可以保存任何变量,number、boolean、string,甚至可以保存table。table是一个hash表,也就是key-value存储方式。在c中lua所有的变量表示均为一个结构体。lua是c实现的,所以对于入栈出栈来说,任意类型的size必须相等,不然怎么实现呢?也的确如此,简而言之,lua的类型其实是一个固定大小的内存加上固定大小的类型描述。就当32位吧,就理解成void* pData + unsigned int 吧。无符号整形保存的是lua变量的类型,void*就是类型的值啦。
对于普通的非gc类型,那么void*的值就是变量的值。比如number类型(实现为c中的float),light userdata类型。它们不被gc所控制,在lua中作为参数或者返回值的传递也就是普通的值复制模式。
对于gc类型(string, userdata, table, closure),那么void*就是对应类型结构的指针。这里最主要就是研究了下userdata和table的不同,因为用cocos2dx通过tolua++导出的类对象时候,因为obj.xxx = xxx的写法很多,所以就一直在问,到底userdata是不是也算是一个table?为何它具备了table所有的key-value存储功能?
其实userdata是一个内存块,也是一个预定义的结构体。因为涉及到gc操作,它和table、string、closure一样,都有一个gc所具有的结构头。然后就是基本信息了,比如分配的size等等。当你分配size大小的userdata时候,会分配一个sizeof(userdata)+size大小的内存块,前面是userdata自身的结构信息,后续就是你可以操作的size大小的内存了,你可以保存你要保存的数据进里面。在tolua++导出c++对象的时候,也就是把c++对象的指针保存在了里面。
所以对于lua任意类型来说,入栈出栈都是push or pop一个size固定的结构,这样理解后很多东西也不用钻牛角尖了。
前面提到的userdata是实现lua面向对象机制的非常重要的类型。当然牵扯到实现面向对象,tolua++的实现中还有一个非常重要的东西就是元表。元表(metatable)是一个普通的表,它可以预设一些基本操作,当然孤立的元表没有任何意义,它只有成为了别的表的元表时候,才会发挥作用。
对于元表的作用,下面是一个简单的例子。
-- table base
local base = {4, 6, 1, 7, 3}
-- print contents
print("base")
for i, v in ipairs(base) do
print("index["..i.."] key:"..v)
end
-- set metatable
local metaFunc = {
invoke = function (obj, idx, value)
obj[idx] = value
end,
}
metaFunc.__index = metaFunc
setmetatable(base, metaFunc)
base:invoke(1, 545)
在上述例子中,有一个base表。然后直接看最后一句,调用了base表中的invoke函数。仔细看看base表的描述,没有任何invoke函数!怎么可以实现调用呢?关键就在于这个metatable了。上述代码定义了一个metaFunc表,里面有一个invoke函数,并将base表的元表设置为了metaFunc,让base表能调用到metaFunc表中的函数。
在调用的过程中,先查询base表,没有任意invoke键,所以会去查找元表,若有元表(上述为metaFunc),就执行里面的index,这个index是一个元表预设的方法,也叫元方法,当元表附属的表中没有查找到对应键的时候,就会调用元表中的index。index有两种设法,一个是table,一个是函数。当设置为table的时候,就返回对应table的对应键值,当设为function的时候,则会调用该function,该调用会传入两个参数,一个为附属的table,一个为键。不同的原方法有不同的调用规则,上述就是index原方法的调用规则。回到上述例子,index为表,则直接返回元表中的key=invoke的key,它是一个function,于是回到base:invoke调用中,调用了该方法,把base中key=1的value设为545。
在上述例子中我们已经看到了元表的神奇作用。而元表则是大部分实现lua面向对象机制中最重要的一个元素。在tolua++中,userdata和元表是实现导出机制的基石。在tolua++中导出一个类,首先会为该类生成一个元表,导出的函数会添加到这个元表中。当在c++中push一个对象到lua中,亦或是在lua中调用c++函数生成一个c++对象导入到lua中,都离不开这个metatable。c++中push一个c++对象到lua中,就会生成一个userdata,并将对应的metatable设置为自己的元表。没错,userdata也支持元表,由于userdata没有table的各种特性,所以在lua中对userdata的各种操作,比如查找键值都会调用到元表中的原方法。
要导出的c++类的元表中,各项原方法已被tolua++绑定。在lua中的键值添加,就会触发原方法newindex,继而调用到tolua++中对应的函数。当触发了newindex原方法的时候,tolua++会为对应的userdata生成一个table来记录要添加的key与value,并将这个table作为value,userdata作为key保存到另一个专门用于保存
大概就这样了吧,以后想到再继续。