关于cocos2d-x下Lua调用C++的文档看了很多,但没有一篇真正把这事给讲明白了,我本身也是个初学者,摸索了半天,总结以下:php
cocos2d-x下Lua调用C++这事之因此看起来这么复杂、网上全部的文档都没讲清楚,是由于存在5个层面的知识点:html
一、在纯C环境下,把C函数注册进Lua环境,理解Lua和C之间能够互相调用的本质
二、在cocos2d-x项目里,把纯C函数注册进Lua环境,理解cocos2d-x是怎样建立Lua环境的、以及怎样获得这个环境并继续自定义它
三、了解为何要使用toLua++来注册C++类
四、在纯C++环境下,使用toLua++来把一个C++类注册进Lua环境,理解toLua++的用法
五、在cocos2d-x项目里,使用cocos2d-x注册自身的方式把自定义的C++类注册进Lua环境,理解cocos2d-x是怎样经过bindings-generator脚原本封装toLua++的用法来节省工做量的python
只有理解了前4层,在最后使用bindings-generator脚本的时候内心才会清清楚楚。而网上的文档,要么是只解释了第1层,要么是只填鸭式地告诉你第5层怎么用bindings-generator脚本,不只中间重要的知识点一律不提,示例代码每每也写的不够简洁,这让我这种看见C++就眼晕的人理解起来大为头疼(不是我不会C++,而是我很是不接受C++的设计哲学,能避就避)。因此接下来的讲解我会对每一层知识点逐一讲解,示例代码也不求完整严谨,而是尽可能用最简洁的方式把程序的关键点说明白。android
直接看代码比啰哩啰嗦讲一大堆概念要清晰明了的多。创建一个a.lua和一个a.c文件,内容以下,一看就明白是怎么回事了:ios
a.luac++
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来编译并执行吧:segmentfault
gcc a.c -llua && ./a.out
注意-llua
选项是必要的,由于要链接lua的库。xcode
看完上面那段代码,再解释起来就容易多了:
一、要想注册进Lua环境,函数须要定义为这个样:int xxx(lua_State *L)
二、使用lua_tonumber
、lua_tostring
等函数,来取得传入的参数,好比lua_tonumber(L, 1)
就是获得传入的第一个参数,且类型为数字
三、使用lua_pushnumber
、lua_pushstring
等函数,来将返回值压入Lua的环境中,由于Lua支持函数返回多个值,因此能够push多个返回值进Lua环境
四、最终函数返回的数字表示有多少个返回值被压入了Lua环境
五、使用lua_register
宏定义来将这个函数注册进Lua环境,Lua脚本里就能够用它了,大功告成!就这么简单!
也简单:
一、在frameworks/runtime-src/Classes/
目录下,找到AppDelegate.cpp
文件。若是frameworks目录不存在,则须要参考这篇Blog:用Cocos Code IDE写Lua,如何与项目中的C++代码和谐相处
AppDelegate.cpp文件中的关键代码以下:
```c++
auto engine = LuaEngine::getInstance();
ScriptEngineManager::getInstance()->setScriptEngine(engine);
LuaStack* stack = engine->getLuaStack(); stack->setXXTEAKeyAndSign("2dxLua", strlen("2dxLua"), "XXTEA", strlen("XXTEA")); //register custom function //LuaStack* stack = engine->getLuaStack(); //register_custom_function(stack->getLuaState());
能够看到cocos2d-x已经为咱们留出了注册自定义C函数的位置,在注释代码后面这么写就能够了: ```cpp lua_State *L = stack->getLuaState(); lua_register(L, "test_lua_bind", test_lua_bind);
也能够经过ScriptEngineManager
类从头取得当前的LuaEngine
对象,而后再getLuaStack()
方法获得封装的LuaStack
对象,再调用getLuaState()
获得原始的lua_State
结构指针。只要知道了入口位置,其余一切就不成问题了,仍是挺简单的。
感兴趣的话能够去看一下ScriptEngineManager
类的详细定义,在frameworks/cocos2d-x/cocos/base/CCScriptSupport.h
文件中。
BTW:这里还有一个小知识点,插入在AppDelegate.cpp中的自定义代码尽可能写在COCOS2D_DEBUG
宏定义的判断前面,由于在调试环境下和真机环境下后续执行的代码是不同的:
#if (COCOS2D_DEBUG>0) if (startRuntime()) return true; #endif // 调试环境下代码就不会走到这里了 engine->executeScriptFile(ConfigParser::getInstance()->getEntryFile().c_str()); return true;
二、接下来,找个地方把test_lua_bind
函数定义写进去就算大功告成了。若是追求文件组织的优雅,按理说应该新建一个.c文件,但这样的话搞很差会把本身陷入到编译阶段的泥潭里,因此先不追求优雅,而就在AppDelegate.cpp文件末尾写上函数的定义就能够了,简单清楚明了:
int test_lua_bind(lua_State *L) { int number = lua_tonumber(L, 1); number = number + 1; lua_pushnumber(L, number); return 1; }
三、大功告成,如今就能够在main.lua文件里使用test_lua_bind()
函数了:
local i = test_lua_bind(99) print("lua bind: " .. tostring(i))
四、若是是新建一个.c文件呢?把AppDelegate.cpp
文件里test_lua_bind
函数定义的代码删掉,在头部#include
后面加入:
#include "test_lua_bind.h"
在frameworks/runtime-src/Classes
目录下建立test_lua_bind.h
文件,内容以下:
extern "C" { #include "lua.h" #include "lualib.h" } int test_lua_bind(lua_State *L);
再建立test_lua_bind.c
文件,内容不变:
#include "test_lua_bind.h" int test_lua_bind(lua_State *L) { int number = lua_tonumber(L, 1); number = number + 1; lua_pushnumber(L, number); return 1; }
此时用cocos compile -p mac
命令编译,会发现test_lua_bind.c
文件并无被编译。这是固然的,普通的C/C++项目都是用Makefile来指定编译哪些.c/cpp文件的,当前的cocos2d-x项目虽然没有Makefile文件,但也是遵循这个原则的,也即确定是有一个地方来指定全部要编译的文件的,须要在这个地方把test_lua_bind.c
加进去,使得整个项目编译时把它也做为项目的一部分。
答案是,cocos2d-x项目没有使用Makefile,而是很是聪明地使用了与具体环境相关的工程文件来做为命令行编译的环境,好比在编译iOS或Mac时就使用Xcode工程文件,在编译Android时就使用Android.mk
文件。
因此,添加好了test_lua_bind.h
和test_lua_bind.c
文件后,用Xcode打开项目,将这俩文件添加进工程中就好了。
注意,千万不要勾选“Copy items into destination group's folder(if needed)”,由于cocos2d-x的Xcode工程目录组织不是常规的结构,一旦勾选这个,会致使这两个文件被拷贝至frameworks/runtime-src/proj.ios_mac
目录下,原来frameworks/runtime-src/Classes
目录下的文件就废掉了,这样的组织方式会乱,并且会影响Android那边对这俩文件的引用。
把test_lua_bind.h
和test_lua_bind.cpp
这俩文件添加进Xcode工程后,再去命令行执行cocos compile -p mac
,编译就能成功了。
网上有其余文章说还要修改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环境的方式。
由于Lua的本质是C,不是C++,Lua提供给C用的API也都是基于面向过程的C函数来用的,要把C++类注册进Lua造成一个一个的table环境是不太容易一会儿办到的事,由于这须要绕着弯地把C++类变成各类其余类型注册进Lua,至关于用面向过程的思惟来维护一个面向对象的环境。这其中的细节就不去深究了,总之正是由于如此,因此单纯地手写lua_register()
等代码来注册C++类是行不通的、代价高昂的,因此须要借助toLua++这个工具。
这一层的知识点看似简单,但实际上是很是重要的,只有理解了手工用lua_register()
去注册C++类的难度,才能理解使用toLua++这类工具的必要性。只有理解了使用toLua++工具的必要性,才会潜下心来冷静地接受toLua++自己的优势和缺点。只有看到了toLua++自己的缺点和使用上的麻烦,才会真心理解cocos2d-x使用bindings-generator
脚本带来的好处。只有理解了bindings-generator
脚本带来的好处,才能谅解这个脚本自己在使用上的一些不便之处。
虽然终极方法是用bindings-generator
脚原本注册C++类进cocos2d-x的Lua环境,但理解toLua++自己的用法仍是狠有必要的,只有知道了toLua++本来的用法,才能更好地理解cocos2d-x是怎么把本身的C++类都注册进Lua环境的,这不只能让编程时的思路更加清晰,也能为往后在源码中寻找各类接口文档的过程当中不至于看不懂那一大堆tolua_beginmodule
、tolua_function
是什么意思。影响程序员学习提升的一大障碍就是忽略那些只知其一;不知其二的代码,不去刨根究底地搞明白。
使用toLua++的标准作法是:
一、准备好本身的C++类,该怎么写就怎么写
二、仿造这个类的.h文件,改一个.pkg文件出来,具体格式要按照toLua++的规定,好比移除全部的private成员等
三、建一个专门用来桥接C++和Lua之间的C++类,使用特殊的函数签名来写它的.h文件,.cpp文件不写,等着toLua++来生成
四、给这个桥接的C++类写一个.pkg文件,按照toLua++的特殊格式来写,目的是把真正作事的C++类给定义进去
五、在命令行下用toLua++生成桥接类的.cpp文件
六、程序入口引用这个桥接类,执行生成的桥接函数,Lua环境中就可使用真正作事的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文件后,就能看到它里面的那一大堆桥接代码了,好比tolua_beginmodule
、tolua_function
等。之后看到这些东西就不陌生了,就明白这些函数只是toLua++用来作桥接的必备代码了,简单看一下代码,就理解toLua++是怎样把MyClass这个C++类注册进Lua中的了:
接下来,用g++来编译:
g++ MyClass.cpp MyLuaModule.cpp main.cpp -llua -ltolua++
默认就生成了a.out
文件,执行,就能看到main.lua的执行结果了:
至此,对toLua++的运做原理内心就透亮了,无非就是:
一、把本身该写的类写好
二、写个.pkg文件,告诉toLua++这个类暴露出哪些接口给Lua环境
三、再写个桥接的.h和.pkg文件,让toLua++去生成桥接代码
四、在程序里使用这个桥接代码,类就注册进Lua环境里了
cocos2d-x在2.x版本里就是用toLua++和.pkg文件这么把本身注册进Lua环境里的。不过这种方法明显笨拙,既要写真正作事的.pkg文件,也要写桥接的.pkg文件和.h文件,工做量又大又枯燥。因此从cocos2d-x 3.x开始,用bindings-generator脚本代替了toLua++。
bindings-generator脚本的工做机制是:
一、不用挨个类地写桥接.pkg和.h文件了,直接定义一个ini文件,告诉脚本哪些类的哪些方法要暴露出来,注册到Lua环境里的模块名是什么,就好了,等于将原来的每一个类乘以3个文件的工做量变成了全部类只须要1个.ini文件
二、摸清了toLua++工具的生成方法,改由Python脚本动态分析C++类,自动生成桥接的.h和.cpp代码,不调用tolua++命令了
三、虽然再也不调用tolua++命令了,可是底层仍然使用toLua++的库函数,好比tolua_function
,bindings-generator脚本生成的代码就跟使用toLua++工具生成的几乎同样
bindings-generator脚本掌握了生成toLua++桥接代码的主动权,不只能够省下大量的.pkg和.h文件,并且能够更好地插入自定义代码,达到cocos2d-x环境下的一些特殊目的,好比内存回收之类的。因此cocos2d-x从3.x开始放弃了toLua++和.pkg而改用了本身写的bindings-generator脚本是很是值得赞扬的聪明作法。
接下来讲怎么用bindings-generator脚本:
一、写本身的C++类,按照cocos2d-x的规矩,继承cocos2d::Ref类,以便使用cocos2d-x的内存回收机制。固然不这么干也行,可是不推荐,否则在Lua环境下对象的释放狠麻烦。
二、编写一个.ini文件,让bindings-generator能够根据这个配置文件知道C++类该怎么暴露出来
三、修改bindings-generator脚本,让它去读取这个.ini文件
四、执行bindings-generator脚本,生成桥接C++类方法
五、用Xcode将自定义的C++类和生成的桥接文件加入工程,否则编译不到
六、修改AppDelegate.cpp,执行桥接方法,自定义的C++类就注册进Lua环境里了
看着步骤挺多,其实都狠简单。下面一步一步来。
首先是自定义的C++类。我习惯将文件保存在frameworks/runtime-src/Classes/
目录下:
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/
目录下能看到genbindings.py
脚本和一大堆.ini文件,这些就是bindings-generator的实际执行环境了。随便找一个内容比较少的.ini文件,复制一份,从新命名为MyClass.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配置文件中存在macro_judgement = ...
宏定义,要特别当心,我第一次是从cocos2dx_controller.ini
文件复制来的,结果没注意macro_judgement
,致使生成的桥接类文件加入了不应加入的宏,只在iOS和Android平台上才起做用,对Mac平台无效,这个要特别注意。
而后修改genbindings.py
文件129行附近,将MyClass.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包狠简单,先sudo easy_install pip
,把pip装好,而后用pip各类pip search
、sudo pip install
就能够了)
成功执行genbindings.py脚本后,会在frameworks/cocos2d-x/cocos/scripting/lua-bindings/auto/
目录下看到新生成的文件:
每次执行genbindings.py脚本时间都挺长的,由于它要从新处理一遍全部的.ini文件,建议大胆修改脚本文件,灵活处理,让它每次只处理须要的.ini文件就能够了,好比像这个样子:
在frameworks/cocos2d-x/cocos/scripting/lua-bindings/auto/
目录下观察一下生成的C++桥接文件lua_MyClass_auto.cpp
,里面的注册函数名字为register_all_MyClass()
,这就是将MyClass类注册进Lua环境的关键函数:
编辑frameworks/runtime-src/Classes/AppDelegate.cpp
文件,首先在文件头加入对lua_MyClass_auto.hpp
文件的引用:
而后在正确的代码位置加入对register_all_MyClass
函数的调用:
最后在执行编译前,将新加入的这几个C++文件都加入到Xcode工程中,使得编译环境知道它们的存在:
这其中还有一个小坑,因为lua_MyClass_auto.cpp
文件要引用MyClass.h
文件,而这俩文件分属于不一样的子项目,互相不认识头文件的搜寻路径,所以须要手工修改一下cocos2d_lua_bindings.xcodeproj
子项目的User Header Search Paths
配置。特别注意一共有几个../
:
最后,就能够用cocos compile -p mac
命令从新编译整个项目了,不出意外的话编译必定是成功的。
修改main.lua文件中,尝试调用一下MyClass类:
local test = my.MyClass:create() print("lua bind: " .. test:foo(99))
而后执行程序(用cocos rum -p mac
或在Cocos Code IDE中都可),见证奇迹的时刻~~~~咦我擦?!程序崩溃!为毛?
这是我做为cocos2d-x初学者遇到的最大的坑,坑了我整整一天半,具体的研究细节就不详细说了,总之罪魁祸首是cocos2d-x框架中的CCLuaEngine.cpp
文件的这段代码:
缘由是executeScriptFile
函数执行时,对当前Lua环境中的栈进行了清理,当register_all_MyClass
函数被调用时,Lua栈是全空的状态,函数内部执行到tolua_module
函数调用时就崩溃了:
解决办法是修改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,则须要修改frameworks/cocos2d-x/tools/bindings-generator/targets/lua/conversions.yaml
文件,定义namespace与Lua之间的映射关系,不然会报conversion wasn't set
错误:
后记补充2:上面的配置完成后iOS的部分是能够正常运行的,可是这个时候编译android是不经过的,由于AppDelegate.cpp
里面调用的register_all_MyClass(L)
方法在android不存在,android的项目里并无配置去编译对应的MyClass.cpp
文件和后续生成的lua_MyClass_auto.cpp
,因此须要在android端配置Android.mk
文件,让项目编译时去编译这两个C++文件才行。
一、首先配置JNI下面的Android.mk
文件,让JNI部分编译时去编译MyClass.cpp
:
编辑frameworks/runtime-src/proj.android/jni/Android.mk
,在LOCAL_SRC_FILES
参数的后面添加:
../../Classes/MyClass.cpp
这里须要注意的是LOCAL_SRC_FILES
里面有不少个配置,若是是最后一个不须要带\
,以前的都须要在后面带\
有一种状况是MyClass.cpp
去#include "MyClass.h"
文件时找不到MyClass.h
文件所在位置,那就须要修改LOCAL_C_INCLUDES
配置,将MyClass.h
所在的目录加入到头文件搜索路径中。若是MyClass.h
文件自己就放在frameworks/runtime-src/Classes
目录下的话就不用再单独设置了。
二、而后配置lua-bindings下面的Android.mk
文件,让lua-bindings部分编译时去编译lua_MyClass_auto.cpp
:
编辑frameworks/cocos2d-x/cocos/scripting/lua-bindings/Android.mk
,在LOCAL_SRC_FILES
配置里添加lua_MyClass_auto.cpp
:
由于lua_MyClass_auto.cpp
里引用到了MyClass.h
,因此还须要配置LOCAL_C_INCLUDES
,使得lua_MyClass_auto.cpp
能正常的找到MyClass.h
:
这样在Android端的编译就完整了,执行:
cocos compile -p android
不出意外的话就能正常编译了,能够用Android真机测试了。
--
参考资料:
Calling C++ Functions From Lua
子龙山人:Lua教程(4)Lua调用C/C++函数
wtyqm:tolua++实现分析
wtyqm:cocos2dx的lua绑定
cocos2d-x-lua如何导出自定义类到lua脚本环境
How to bind a custom class to lua runtime
如何使用 bindings-generator 自动生成 lua绑定
cocos2d-x 3.0 + lua tolua_module崩溃问题与解决吐槽
cocos2d-x 3.0rc0 - bindings-generator 问题与解决