cocos2dx之tolua++全面分析(一):tolua++工具本身
在cocos2dx/tools/tolua++下面,有大量pkg文件,这些是按tolua++要求格式写好的、需要导出到lua中的c++类描述文件。
每当在c++类里增加了新函数需要导出时,应同步修改相应的pkg文件,然后运行此目录下的build.sh,就会重新生成cocos2dx/script/lua/cocos2dx_support/LuaCocos2d.cpp,里面就包含了对新增函数的封装代码。
build.sh的内容如下:
${TOLUA} -L basic.lua -o ../../scripting/lua/cocos2dx_support/LuaCocos2d.cpp Cocos2d.pkg
在运行标准的tolua++之前,还加载了额外的脚本basic.lua,这是由于cocos2dx在lua绑定方面并未完全遵照tolua++的默认做法,因此需要对其进行定制,basic.lua主要做的事情是:
1、将所有导出的CCXXXX类的push函数(也就是将cpp obj传进lua时调用的函数)修改为自己的toluafix_pushusertype_ccobject
2、将function和table两种类型的to和is函数(分别是指将lua obj传进cpp时调的函数、判断一个变量是否为本类型时调的函数)修改为toluafix_is/to_funtion/table
以上两条是tolua++本身提供的用户类型定制方法,也就是通过定义tolua++认别的3个特殊变量_to/push/is_function[classname] = XXX来实现。
至于为什么要做这些定制到后面分析tolua++的绑定实现时再详细说明。
下面还有一些通过强制替换输出的cpp胶水代码来实现的针对某一个类型的特殊定制:
3、将
ccColor3B color = *((ccColor3B*) tolua_tousertype(tolua_S,4,(void*)&(const ccColor3B)ccBLACK));
改为
const ccColor3B clr = ccBLACK;
ccColor3B color = *((ccColor3B*) tolua_tousertype(tolua_S,4,(void*)&clr));
这里的原因是某些函数声明里,给ccColor3B类的参数带了默认值ccBLACK,因此tolua++会转出前者代码,而ccBLACK实际是个const定义,无法取地址,因此强制替换成后者。这个问题其实很典型,在我以前自己做lua绑定库时也遇到过,即c++的默认参数只是个语法糖,在实际生成汇编指令时,所有参数都是要齐备的,而调用方未提供的实参,自然是由编译器帮助补上了(编译期),因此在导出到lua里被调用时,早已无默认参数的概念(运行期),要么lua代码必须提供所有参数,要么就是胶水层代码提供。我当时的做法是胶水层没有管这个事情,也就是写lua代码时根本不要想默认参数这回事。而cocos2dx这里的做法则是在胶水层搞定,给lua代码提供了便利。
4、将
tolua_usertype(tolua_S,"LUA_FUNCTION");
删除,将作为参数的
*((LUA_FUNCTION*)
也删除,也就是普通的lua function不做为usertype使用(注册、取参)。照理说lua自己的基本类型本就不该做为usertype,为什么tolua++会生成这样的本不该有的代码呢?出现这种情况的原因是,cocos2dx里有一些接受某个lua回调函数作为参数的函数,如
void registerScriptObserver(CCObject *target,int handler,const char* name);
这种函数在c++代码里,使用int作为回调函数参数的类型是很自然的,因为cocos2dx除了lua还要支持js等其它脚本语言,不可能直接使用某一个语言特有的函数类型来表示此参数,因此将其抽象为一个int型的handler。(虽然实际上lua本身也恰好是用int来表示函数引用的)
但是在pkg文件里,以上声明被改写为:
void registerScriptObserver(CCObject *target,LUA_FUNCTION funcID,const char* name);
这里int换成了LUA_FUNCTION。因为如果不换,那么tolua++生成的胶水代码就不知道这里要提取一个function,而是直接生成提取一个int变量的错误代码了。为了让tolua++生成正确的代码,需要hook它生成此处代码的逻辑,所以这里实际上包含两步,一是首先通过将int修改为LUA_FUNCTION使tolua++意识到这里有一个特殊类型(即不是基础类型)的参数,二是通过上述第2条所做的事让tolua++使用cocos2dx针对此类型提供的专有存取函数。如果不做第一步,直接给int提供push/to/is函数,当然也可以达到hook插入自有代码的目的,但是所有使用int做参数的地方(而非仅是用int表示脚本回调函数处)就全受影响了,因此LUA_FUNCTION在这里就是起到一个标识回调函数——hook只限于此——用途的作用。
可以在basic.lua里将此处注释掉(包括第2条)来检查生成代码的差异:
@@ -13018,7 +13018,7 @@ static int tolua_Cocos2d_CCNotificationCenter_registerScript
{
CCNotificationCenter* self = (CCNotificationCenter*) tolua_tousertype(tolua_S,1,
CCObject* target = ((CCObject*) tolua_tousertype(tolua_S,2,0));
- LUA_FUNCTION funcID = ( toluafix_ref_function(tolua_S,3,0));
+ LUA_FUNCTION funcID = *((LUA_FUNCTION*) tolua_tousertype(tolua_S,3,0));
const char* name = ((const char*) tolua_tostring(tolua_S,4,0));
-对应的是正确的代码,调用专门提供的toluafix_ref_function来取得一个lua function的ref,其返回值类型定义正是pkg中形参类型,也恰好是lua本身的一个typedef,语法完全正确。
+对应的是注释之后生成的错误的代码,对于lua function型参数,用普通的tolua_tousertype去提取,完全对不上型号。
5、将
toluafix_pushusertype_ccobject(tolua_S,(void*)tolua_ret
替换为
int nID = (tolua_ret) ? (int)tolua_ret->m_uID : -1;
int* pLuaID = (tolua_ret) ? &tolua_ret->m_nLuaID : NULL;
toluafix_pushusertype_ccobject(tolua_S, nID, pLuaID, (void*)tolua_ret
这也是无奈之举,第1条虽然用_push_function修改了usertype的push函数名,但传参列表却没有改,为了结合cocos2dx自己的CCObject::nID/LuaID机制,这里只好强行替换函数调用语句,加上提取出这两个参数。具体这两个ID的用处也待后面说明。
除了LuaCocos2d.cpp,同目录下还有几个胶水文件:
LuaCocoStudio.cp: 与上述一样,用大量pkg和一个定制lua脚本通过tolua++自动生成
下面这些看起来似乎都是手写的了,各有各的特殊逻辑要处理(都不是tolua++常规途径能解决的),就先不一一细说了。
lua_cocos2dx_manual.cpp:
lua_cocos2dx_cocostudio_manual.cpp:
lua_web_socket.cpp:
lua_extensions_CCB.cpp:
CCBProxy.cpp:
关于tolua++自身的编译:有个特别一点的地方,即它先编出一个tolua++_boostrap,用途是将src/bin/lua下的一堆lua文件转成二进制字节数组的c文件(toluabind.c),然后再加进这个c文件生成最后的tolua++,好处是发布最终程序时,就是一个裸的可执行程序,不需要再携带一堆lua脚本了(通常容易带来各种定位麻烦)。至于那一堆lua文件,除了package.lua是用来做lua文件转字节数组外,大部份是用来做pkg文件解析的,也就相当于一个微型类c++头文件解析器了,这一点使我觉得tolua++很蛋疼,因为c++语法解析(即使只是弱化版的头文件)本来就很复杂,非要自己做,还写那么大一堆冗长晦涩的lua代码,不是原作者根本没法看懂,想对pkg格式做点修改扩展什么的几乎下不了手,只能把它当一个将就能用的东西用了,所以也才出现了上面cocos2dx对它各种修改替换的结果。