Cocos2d-x下Lua调用自定义C++类和函数的最佳实践
关于cocos2d-x下Lua调用C++的文档看了不少,但没有一篇真正把这事给讲明白了,我自己也是个初学者,摸索了半天,总结如下: cocos2d-x下Lua调用C++这事之所以看起来这么复杂、网上所有的文档都没讲清楚,是因为存在5个层面的知识点: 1、在纯C环境下,把C函数注册进Lua环境,理解Lua和C之间可以互相调用的本质 只有理解了前4层,在最后使用bindings-generator脚本的时候心里才会清清楚楚。而网上的文档,要么是只解释了第1层,要么是只填鸭式地告诉你第5层怎么用bindings-generator脚本,不仅中间重要的知识点一概不提,示例代码往往也写的不够简洁,这让我这种看见C++就眼晕的人理解起来大为头疼(不是我不会C++,而是我非常不接受C++的设计哲学,能避就避)。所以接下来的讲解我会对每一层知识点逐一讲解,示例代码也不求完整严谨,而是尽量用最简洁的方式把程序的关键点说明白。 第一层:纯C环境下,把C函数注册进Lua环境直接看代码比啰哩啰嗦讲一大堆概念要清晰明了的多。建立一个a.lua和一个a.c文件,内容如下,一看就明白是怎么回事了: a.lua print(foo(99)) a.c #include <lua.h> #include <lualib.h> #include <lauxlib.h> int foo(lua_State *L) { int n = lua_tonumber(L,1); lua_pushnumber(L,n + 1); return 1; } int main() { lua_State *L = lua_open(); luaL_openlibs(L); lua_register(L,"foo",foo); luaL_dofile(L,"a.lua"); lua_close(L); return 0; } 怎么样,这代码简单吧?一看就明白,简单的不能再简单了。我特别烦示例代码里又是判断错误又是加代码注释的,本来看自己不会的代码就够吃力的了,还加那么多花花绿绿的干扰项,纯粹增加学习负担。 在命令行下用gcc来编译并执行吧: gcc a.c -llua && ./a.out 注意 看完上面那段代码,再解释起来就容易多了: 1、要想注册进Lua环境,函数需要定义为这个样: 第二层:在cocos2d-x环境下,把C函数注册进Lua环境也简单: 1、在 AppDelegate.cpp文件中的关键代码如下: LuaStack* stack = engine->getLuaStack(); stack->setXXTEAKeyAndSign("2dxLua",strlen("2dxLua"),"XXTEA",strlen("XXTEA")); //register custom function //LuaStack* stack = engine->getLuaStack(); //register_custom_function(stack->getLuaState());
也可以通过 感兴趣的话可以去看一下 BTW:这里还有一个小知识点,插入在AppDelegate.cpp中的自定义代码尽量写在 #if (COCOS2D_DEBUG>0) if (startRuntime()) return true; #endif // 调试环境下代码就不会走到这里了 engine->executeScriptFile(ConfigParser::getInstance()->getEntryFile().c_str()); return true; 2、接下来,找个地方把 int test_lua_bind(lua_State *L) { int number = lua_tonumber(L,1); number = number + 1; lua_pushnumber(L,number); return 1; } 3、大功告成,现在就可以在main.lua文件里使用 local i = test_lua_bind(99) print("lua bind: " .. tostring(i)) 4、如果是新建一个.c文件呢?把 #include "test_lua_bind.h" 在 extern "C" { #include "lua.h" #include "lualib.h" } int test_lua_bind(lua_State *L); 再创建 #include "test_lua_bind.h" int test_lua_bind(lua_State *L) { int number = lua_tonumber(L,number); return 1; } 此时用 答案是,cocos2d-x项目没有使用Makefile,而是非常聪明地使用了与具体环境相关的工程文件来作为命令行编译的环境,比如在编译iOS或Mac时就使用Xcode工程文件,在编译Android时就使用 所以,添加好了
注意,千万不要勾选“Copy items into destination group's folder(if needed)”,因为cocos2d-x的Xcode工程目录组织不是常规的结构,一旦勾选这个,会导致这两个文件被拷贝至 把 网上有其他文章说还要修改Xcode工程的“User Headers Path”,这个经过试验是不需要的,哪怕把这俩文件放进新建的文件夹里也不需要,只要加入了Xcode工程即可,因为Xcode内部根本就不是按照文件夹的形式来组织文件的,它自己有一套叫做“Group”的东西。搞了好几年iOS开发,对Xcode的这个特性还是熟悉的。
说到这就不禁要插一句对网上所有cocos2d-x文档的吐槽了,学习cocos2d-x的人水平实在是良莠不齐,大部分人似乎都是对游戏热衷的编程初学者,他们大多底子薄基础差,甚至一大部分人之前都没做过移动APP的开发,他们学习cocos2d-x只想知其然而不想知其所以然,给他们讲他们也看不明白(因为编程基础差),所以网上不少cocos2d-x文章都是只讲123步骤,而不告诉你为什么这么做,包括cocos2d-x官方的大量文档也是基于这个思路写的,中文和英文都一样。我看这些文章就特别痛苦,一边看一边心里就总是在想,“凭什么要这么做啊”、“这一步是为了什么啊”、“怎么这么麻烦啊”、“这个步骤明显不是最佳实践啊”、“解决这事为啥要这么麻烦”、“有更好的方法吗”,所以我这种初学者来看cocos2d-x文档就变成了不是单纯的学习,而是学习、质疑、求证、反思、优化的过程,对别人来说cocos2d-x的入门比较容易,到我这里反倒成了入门比较难、入门之后比较容易了,因为文档中的垃圾信息和无效信息实在是太多了,别人可以照单全收、以后懂了之后再慢慢剔除,我是必须从一开始就自己甄别垃圾、只保留最佳实践,这也是这篇Blog写的比较长的原因。 扯远了。反正经过以上步骤,就完成了在cocos2d-x项目中把C函数注册进Lua环境这件事。至此,算是彻底搞懂了Lua和C函数之间的互相调用关系,也能在cocos2d-x的Lua环境中使用自定义的C函数了。但这还不够,因为一个正规的项目是需要狠好的组织结构的,全局C函数满天飞肯定是不行的,好一点的情况是把所有的C函数都在Lua中组织为模块注册进去,更好一点的情况是把C++类注册进Lua、并且C++类也是以Lua模块为组织方式注册进Lua环境的。这其实就是cocos2d-x自己把自己注册进Lua环境的方式。 第三层:了解为什么要使用toLua++来注册C++类因为Lua的本质是C,不是C++,Lua提供给C用的API也都是基于面向过程的C函数来用的,要把C++类注册进Lua形成一个一个的table环境是不太容易一下子办到的事,因为这需要绕着弯地把C++类变成各种其他类型注册进Lua,相当于用面向过程的思维来维护一个面向对象的环境。这其中的细节就不去深究了,总之正是因为如此,所以单纯地手写 这一层的知识点看似简单,但其实是非常重要的,只有理解了手工用 第四层:在纯C++环境下,使用toLua++来把一个C++类注册进Lua环境虽然终极方法是用 使用toLua++的标准做法是: 1、准备好自己的C++类,该怎么写就怎么写 toLua++这种自己手写.pkg文件的方式古老又难受,所以我没有仔细地去学习,这套流程放在10年前的那个年代是没有太大问题的,作者怎么规定就怎么用好了,但是放在2014年的今天,任何程序的架构设计都讲究学习成本低、轻量化、符合以往的习惯,因此toLua++用起来我觉得其实是难受的。 下面我以尽量最少的代码来走一遍toLua++的流程,注意这是在纯C++环境下,跟任何框架都没关系,也不考虑内存释放等细节: MyClass.h class MyClass { public: MyClass() {}; int foo(int i); }; MyClass.cpp #include "MyClass.h" int MyClass::foo(int i) { return i + 100; } MyClass.pkg class MyClass { MyClass(); int foo(int i); }; MyLuaModule.h extern "C" { #include "tolua++.h" } #include "MyClass.h" TOLUA_API int tolua_MyLuaModule_open(lua_State* tolua_S); MyLuaModule.pkg $#include "MyLuaModule.h" $pfile "MyClass.pkg" main.cpp extern "C" { #include <lua.h> #include <lualib.h> #include <lauxlib.h> } #include "MyLuaModule.h" int main() { lua_State *L = lua_open(); luaL_openlibs(L); tolua_MyLuaModule_open(L); luaL_dofile(L,"main.lua"); lua_close(L); return 0; } main.lua local test = MyClass:new() print(test:foo(99)) 先在命令行下执行: tolua++ -o MyLuaModule.cpp MyLuaModule.pkg 此命令用来生成桥接文件MyLuaModule.cpp。注意命令行中-o参数的顺序不能随意摆放,从这个小事也能看出tolua++的古老和难用 生成好MyLuaModule.cpp文件后,就能看到它里面的那一大堆桥接代码了,比如
接下来,用g++来编译: g++ MyClass.cpp MyLuaModule.cpp main.cpp -llua -ltolua++ 默认就生成了
至此,对toLua++的运作原理心里就透亮了,无非就是: 1、把自己该写的类写好 第五层:使用cocos2d-x的方式来将C++类注册进Lua环境cocos2d-x在2.x版本里就是用toLua++和.pkg文件这么把自己注册进Lua环境里的。不过这种方法明显笨拙,既要写真正做事的.pkg文件,也要写桥接的.pkg文件和.h文件,工作量又大又枯燥。所以从cocos2d-x 3.x开始,用bindings-generator脚本代替了toLua++。 bindings-generator脚本的工作机制是: 1、不用挨个类地写桥接.pkg和.h文件了,直接定义一个ini文件,告诉脚本哪些类的哪些方法要暴露出来,注册到Lua环境里的模块名是什么,就行了,等于将原来的每个类乘以3个文件的工作量变成了所有类只需要1个.ini文件 bindings-generator脚本掌握了生成toLua++桥接代码的主动权,不仅可以省下大量的.pkg和.h文件,而且可以更好地插入自定义代码,达到cocos2d-x环境下的一些特殊目的,比如内存回收之类的。所以cocos2d-x从3.x开始放弃了toLua++和.pkg而改用了自己写的bindings-generator脚本是非常值得赞赏的聪明做法。 接下来说怎么用bindings-generator脚本: 1、写自己的C++类,按照cocos2d-x的规矩,继承cocos2d::Ref类,以便使用cocos2d-x的内存回收机制。当然不这么干也行,但是不推荐,不然在Lua环境下对象的释放狠麻烦。 看着步骤挺多,其实都狠简单。下面一步一步来。 首先是自定义的C++类。我习惯将文件保存在 frameworks/runtime-src/Classes/MyClass.h #include "cocos2d.h" using namespace cocos2d; class MyClass : public Ref { public: MyClass() {}; ~MyClass() {}; bool init() { return true; }; CREATE_FUNC(MyClass); int foo(int i); }; frameworks/runtime-src/Classes/MyClass.cpp #include "MyClass.h" int MyClass::foo(int i) { return i + 100; } 然后编写.ini文件。在 frameworks/cocos2d-x/tools/tolua/MyClass.ini [MyClass] prefix = MyClass target_namespace = my headers = %(cocosdir)s/../runtime-src/Classes/MyClass.h classes = MyClass 也即在MyClass.ini中指定MyClass.h文件的位置,指定要暴露出来的类,指定注册进Lua环境的模块名。 注意,这个地方我踩了个坑。如果.ini配置文件中存在 然后修改 frameworks/cocos2d-x/tools/tolua/genbindings.py cmd_args = {'cocos2dx.ini' : ('cocos2d-x','lua_cocos2dx_auto'), 'MyClass.ini' : ('MyClass','lua_MyClass_auto'), ... (其实这一步本来是可以省略的,只要让genbindings.py脚本自动搜寻当前目录下的所有ini文件就行了,不知道将来cocos2d-x团队会不会这样优化) 至此,生成桥接文件的准备工作就做好了,执行genbindings.py脚本: python genbindings.py (在Mac系统上可能会遇到缺少yaml、Cheetah包的问题,安装这些Python包狠简单,先 成功执行genbindings.py脚本后,会在
每次执行genbindings.py脚本时间都挺长的,因为它要重新处理一遍所有的.ini文件,建议大胆修改脚本文件,灵活处理,让它每次只处理需要的.ini文件就可以了,比如像这个样子:
在
编辑
然后在正确的代码位置加入对
最后在执行编译前,将新加入的这几个C++文件都加入到Xcode工程中,使得编译环境知道它们的存在:
这其中还有一个小坑,由于
最后,就可以用 修改main.lua文件中,尝试调用一下MyClass类: local test = my.MyClass:create() print("lua bind: " .. test:foo(99)) 然后执行程序(用
这是我作为cocos2d-x初学者遇到的最大的坑,坑了我整整一天半,具体的研究细节就不详细说了,总之罪魁祸首是cocos2d-x框架中的
原因是
解决办法是修改AppDelegate.cpp为这个样子:
文本形式的代码如下: AppDelegate.cpp lua_State *L = stack->getLuaState(); lua_getglobal(L,"_G"); register_all_MyClass(L); lua_settop(L,0); 重新编译并执行,程序就正确执行了:
至此,就彻底搞清楚应该怎样在cocos2d-x项目里绑定一个C函数或者C++类到Lua环境中了,感兴趣的话可以再进一步深入研究Lua内部metatable的运作原理、类对象的生成与释放、以及垃圾回收。我自己也是刚接触cocos2d-x不到一个星期,理解不深,以上难免会有用词不当或理解错误的地方,如有错误请多包涵。 后记补充:如果C++类定义了namespace,则需要修改
后记补充2:上面的配置完成后iOS的部分是可以正常运行的,但是这个时候编译android是不通过的,因为 1、首先配置JNI下面的 编辑 ../../Classes/MyClass.cpp 这里需要注意的是
有一种情况是
2、然后配置lua-bindings下面的 编辑
因为
这样在Android端的编译就完整了,执行: cocos compile -p android 不出意外的话就能正常编译了,可以用Android真机测试了。 -- Calling C++ Functions From Lua (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |