tolua实现分析

4.3k 词

tolua++如何将c++对象导入到lua里

tolua++为每一个传入lua的对象建立一个userdata,userdata的值,是c++对象的地址。userdata的metatable,是一个tolua++建立的,记录了userdata对应c++类型信息的表格,包括导出的成员变量、成员函数等信息。

对于成员变量的读取赋值,tolua++是在metatable里新建了.get和.set两个表。两个表里分别存储了以变量名为键,以读取设置c函数为值的表项。在metatable的index和newindex里,以变量名为键,从.get和.set表里取得读取设置函数并调用。

对于成员函数的调用,只需要以函数名为键,函数为值,存储在metatable里就好了。

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

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

父类的metatable,是子类metatable的metatable。这样调用父类方法时,就会去父类的metatable里查找了。tolua_ubox 用来存储以C++对象指针为键, 值为lua建立的fulluserdata的键值对. 看tolua++源码中有两套ubox, 一套在全局注册表, 一套在C++类型的metatable中, 实际在看代码的过程中, 只用其中一套就OK, 先判断在C++类型的metatable中是否有ubox, 没有再建立全局注册表的ubox, 而在注册C++类型的metatable中就会在该metatable中建立ubox. 优先使用C++类型的metatable的ubox. ubox为值为弱引用的弱表.

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表里。tolua_gc 用来标识是否由lua来进行垃圾回收的表. 以C++对象指针为键, 值为传入C++类型的metatable, 传入C++类型只能为C++对象的同类或者父类, 如果传入的是父类类型, gc的时候只会调用父类的析构

tolua++自定义属性

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

tolua++在LUA_REGISTRY里维护了一张tolua_peers表用来存储C++对象在lua中的扩展, 键为弱引用的弱表。这张表以表示c++对象的userdata为键,以一张表格t为值。lua5.1没用到这张表, 直接通过对C++对象指针进行lua_setfenv环境变量设置和获取该C++对象的tolua_peers表

tolua++注册类

  • 在项目文件中tolua++自动生成的绑定文件中的tolua_xxx_open(L)函数注册类绑定(其中xxx为pkg文件名)
  • tolua_xxx_open(L)函数调用了tolua_open(L), tolua_reg_types, tolua_cclass, tolua_beginmodule, tolua_function, tolua_variable, tolua_endmodule等函数进行类的绑定, 其中tolua_reg_types是自动绑定文件生成的函数, 其他都是tolua++库函数
  • tolua_open(L)建立相应的全局注册表, 包括tolua_opened, tolua_peers, tolua_ubox, tolua_super, tolua_gc
  • tolua_reg_types主要调用tolua_usertype注册该pkg文件下的所有C++类型的metatabl
  • tolua_usertype调用tolua_newmetatable 注册C++类型metatable和const C++类型metatable
  • tolua_usertype调用tolua_classevents注册C++类型metatable中的add, call, div, eq, gc, index, le, lt, mul, newindex, __sub方法
  • tolua_cclass设置父子关系, 以及对象回收函数(为tolua++自动生成的一个调用delete的C函数)
  • tolua_cclass调用mapinheritance设置父子关系metatable, 指定父类为子类的metatable, 通过指定metatable的方式进行继承, 其中const_type—继承—>type—继承—>base_type
  • tolua_cclass调用set_ubox设置父子关系共享同一tolua_ubox, tolua_ubox暂时为空表, 且值为弱引用的弱表
  • tolua_cclass调用mapsuper设置全局注册表中tolua_super表的父子关系
  • 绑定类成员函数: tolua_function
  • 绑定类成员变量: tolua_variable, 成员变量的绑定通过建立.set 和.get两张表, 通过index绑定tolua++生成的get方法, newindex绑定tolua++绑定的set方法
  • 如果有namespace的话, 通过tolua_beginmoduletolua_endmodule进行定义
  • 绑定new, 通过绑定函数创建的对象, lua不会自动调用析构, 得手动调用delete进行析构
  • 绑定new_local, 通过绑定的函数创建对象, 该函数中比new的绑定函数多了一个lua_register_gc调用, 在全局注册表中的tolua_gc表中, 以C++指针为键, 类型的metatable为值, 通过class_gc_event进行自动释放
  • 绑定delete, 如果有析构函数的一定要注册析构函数, 否则释放时会调用默认的释放函数, tolua_default_collect, 调用free进行释放, 会发生很多未定义的行为

cocos2dx对tolua++绑定的修正

c对lua回调函数的引用

在使用cocos2dx编写游戏时,我们经常会设置一些回调函数(时钟、菜单选择等)。如果采用脚本方式编写游戏的话,这些回调函数也是需要写在脚本里的。实现这个功能,就需要lua将自己的函数传递给c++,c++保持对这个函数的引用,不要让这个lua函数被垃圾回收,并在适当的时候回调这个lua函数。 这种需求的一般抽象是在C环境下保存lua状态,在PIL(Programming In Lua)里有比较详尽的描述。可以使用luaL_ref函数,将一个luaValue(function、table等没有直接对应c类型的数据)存储到LUA_REGISTRY里(luaL_ref返回一个唯一整数,c++可以用这个整数来索引对应的luaValue),不过cocos2dx因为某种原因,并没有使用这个功能,而是自己实现了一套类似的引用机制。 cocos2dx注册回调函数的接口,除了一个参数为c函数指针的版本外,都会提供一个参数为int的对应版本。阅读一下自动生成的cocos2dx lua绑定代码,会发现注册回调函数的接口,都会调用toluafix_ref_function函数,将lua函数转换为一个LUA_FUNCTION(int),并调用响应的注册回调函数的cocos2dx api。 这个toluafix_ref_function,定义在tolua_fix.c里,干的事情就很类似luaL_ref了。区别是对lua函数的引用,没有直接保存在LUA_REGISTRY里,而是放在一个自己创建的表格里。

B.野指针预防

使用已经释放的指针,通常是一个令人头疼的bug来源。如果能提早发现对野指针的使用,对于bug的定位有很大好处。tolua_fix.c里也提供了这样一套检查机制。
阅读自动生成的cocos2dx lua绑定代码,会发现每当把一个继承自CCObject类型的对象传给lua时,会调用toluafix_pushusertype_ccobject函数。
如果这个对象是第一次传递给lua,toluafix_pushusertype_ccobject会为这个对象生成一个索引id,并将这个对象的指针、类型字符串和这个索引相关联。cocos2dx再将这个索引存储在CCObject数据结构里。
在c++里析构这个对象时,CCObject的析构函数会调用toluafix_remove_ccobject_by_refid。这个函数先利用整数索引,找到指针、类型字符串,再通过tolua的tolua_ubox表格(见tolua++实现分析),取到对象的userdata(值为对象的地址),将它置空。这样,以后lua环境再使用这个对象,调用这个对象的c接口时,只能取到空地址,错误也能提早发现了。

参考

tolua++实现分析
tolua++实现分析