天天酷跑基于自研引擎NPEngine,在游戏运营过程中暴露出修改bug过程冗长(尤其是iOS平台),项目编译耗时长(万恶的C++)等问题。经过短暂的技术调研,项目组决定集成Lua脚本引擎。
然而作为一个纯C++实现的引擎,NP在设计之初就没有考虑到脚本化的需求。大量的原生接口需要导出到脚本环境,这个工作如果采用人力,过程将十分耗时且容易出错。在考察了Cocos2dx的脚本绑定技术之后,我们决定采用自动生成绑定代码的方式来完成这个工作。
为了能在cocos2dx中使用Javascript和Lua开发的接口绑定代码生成工具。基于Python2.7,py-yaml(Python yaml模块),cheetah(类似Jinja2,模板引擎)和libclang(LLVM前端Clang的Python绑定)。
tolua++扩展了tolua,用于将C/C++代码导入到lua脚本执行环境。tolua++加入了对C++模板的支持。
按照tolua的文档,可以直接用于接口的导出,只是我们还需要写一些(da liang)配置文件,这就是为什么我们需要binding-generator的理由。
使用Python脚本用Clang将代码中我们需要的接口提取出来自动生成tolua的接口文件不就行了?This is a good idea,然而binding-generator走的更远。事实上它只用了tolua++提供了的基础函数,而绑定代码都由自己生成(这里要再由衷的赞叹一下Lua的capi的简洁设计!)。
秉持光荣的拿来主义传统,我们直接把cocos2dx源码tools文件夹下的bindings-generator
和tolua
两个文件夹拷贝到我们自己的引擎下面。然后,
tolua/genbindings.py
的内容,把cocosdir参数换成我们自己的头文件路径(这里定义的配置都可以在步骤3中的ini文件中直接引用)。把main
中的cmd_args
字典的内容清空(这些都是cocos2dx的导出配置)tolua
下所有xxx.ini
文件,仿照这些文件的格式写自己引擎的配置文件,其格式大致如下
关键的一步,修改bindings-generator/targets/lua/conversions.yaml
,这里主要需要配置lua和c++参数之间的互相转换函数。to_native定义了lua到c++,而from_native则定义了c++到lua的转换,这里定义的字符串会被用来替换templates下的模板中相应的转换调用
举个from_native的例子,我们在C++中有一个结构PlayerSubTaskInfo
,现在需要在lua中用到这个结构,首先在实现文件中定义转换函数
void luaval_by_native(lua_State* L, const PlayerSubTaskInfo& info)
{
if (NULL == L) return;
lua_newtable(L);
lua_pushinteger(L, info.dwSubTaskId);
lua_setfield(L, -2, "subTaskId");
}
然后需要做的就是在from_native里面加一行转换规则"PlayerSubTaskInfo": "luaval_by_native(L, ${in_value})"
。这样之后我们在lua中就可以以一个table的形式来访问c++传过来的PlayerSubTaskInfo对象,这个表的subTaskId域即是原对象的dwSubTaskId。
与ini文件中定义要导出的类对象不同,from_native中定义的对象在导出后都是以table的形式在lua中使用,我们只是从中取数据(以field的形式)。如果需要修改数据or调用方法,那么必须在ini文件中定义。数据类型的转换函数是luaval_by_native,而对象类型则是luaval_by_object,区分这一点很重要,使用过程中发现这是常见的错误之一。
在使用binding-generator的过程中,一旦导出错误都会在控制台输出错误原因,这方便了配置错误的定位。
至此,我们用binding-generator生成了绑定代码,然后我们将这些绑定文件添加到工程中去。编译,链接,OK,happy ending!我们的故事似乎可以结束了!等等,还有一个重要角色尚未登场!作为重要的组成部分,tolua++究竟做了什么?
事实上,在binding-generator导出的大量代码中,包括了对tolua++的调用。以NPEngine中的NPNode类为例,绑定该类的方法如下
int NPLuaRegisterCommon_NPNode(lua_State* L)
{
tolua_usertype(L, "NPNode");
tolua_cclass(L, "NPNode", "NPNode", "NPRenderObject", NULL);
tolua_beginmodule(L, "NPNode");
tolua_function(L, "AddChild", NPNode_AddChild);
...
tolua_endmodule(L);
return 1;
}
这段代码将NPNode以metatable的形式注册到Lua中,而这个元表中包含的正是我们导出的函数(如AddChild)。当我们在Lua中接受一个C++的NPNode对象的时候,运行的是luaval_by_object这个函数,
void tolua_pushusertype_internal (lua_State* L, void* value, const char* type, int addToRoot)
{
...
luaL_getmetatable(L, type);
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
return;
}
lua_pushstring(L, "tolua_ubox");
lua_rawget(L, -2);
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_pushstring(L, "tolua_ubox");
lua_rawget(L, LUA_REGISTERINDEX);
}
lua_pushlightuserdata(L, value);
lua_rawget(L, -2);
if (lua_isnil(L, -1)) {
lua_pop(L, 1); /*stack: mt ubox*/
lua_pushlightuserdata(L, value);
*(void**)lua_newuserdata(L, sizeof(void*)) = value; /*stack: mt ubox value newud*/
lua_pushvalue(L, -1); /*stack: mt ubox value newud newud*/
lua_insert(L, -4); /*stack: mt newud ubox value newud*/
lua_rawset(L, -3); /*stack: mt newud ubox*/
lua_pop(L, 1); /*stack: mt newud*/
lua_pushvalue(L, -2); /*stack: mt newud mt*/
lua_setmetatable(L, -2); /*stack: mt newud*/
#ifdef LUA_VERSION_NUM
lua_pushvalue(L, TOLUA_NOPEER);
lua_setfenv(L, -2);
#endif
} else {
...
}
...
}
这里value是NPNode对象的指针,因为light userdata是不能有元表的,所以这里绕了一个弯子,对于每个指针新建了一个userdata对象,其内存值就是指针的值。代码的最后把NPNode对应的元表设置为这个userdata的元表,且将这个userdata作为返回值返回给脚本。在这之后,如果我们在脚本中调用该对象的AddChild方法,则事实上实际上调用的是NPNode元表里面的AddChild,这个逻辑藏在tolua_usertype(L, "NPNode")
中,
TOLUA_API void tolua_classevents (lua_State* L)
{
lua_pushstring(L,"__index");
lua_pushcfunction(L,class_index_event);
lua_rawset(L,-3);
...
}
NPNode元表的__index
域被设置为函数class_index_event
。通过查看代码可以看出倘若在Lua中去调用NPNode对象的AddChild方法,最终得到调用的是NPNode元表中对应的NPNode_AddChild。
至此,整个绑定的主体部分豁然开朗!