Quick-cocos2d-x luabinding 教程
--基于quick-cocos2d-x 3.3rc1版本
目录
Quick-cocos2d-x luabinding 教程... 1
1. lua绑定原理... 2
1.1 什么是lua. 2
1.2 Lua绑定原理... 2
2. tolua++绑定原理... 6
2.1 什么是tolua++. 6
2.2 为什么要使用tolua++来注册C类... 6
2.3 如何使用tolua++. 6
3. quick 中导出C/C++ API给LUA使用... 8
3.1 基本步骤... 8
3.2 实战演示... 9
3.3 tolua 文件内容的修改规则... 13
3.3.1 删除所有无需在 Lua 中使用的内容... 13
3.3.2 处理 CCObject 继承类... 13
3.3.3 展开宏... 13
3.3.4 处理名字空间... 14
3.3.5 添加必要的 #include 指令... 14
3.3.6 修改函数参数和返回值类型,去除 const 修饰符... 14
3.3.7 从 C/C++ 函数返回多个值... 14
3.3.8 将 Lua 函数传入 C/C++. 15
3.3.9 在 Lua 和 C/C++ 间交换二进制数据... 15
3.3.10 更多用法... 16
前言
本文主要目的是帮助使用quick-cocos2d-x(简称quick)的开发者快速的掌握在在quick如何绑定C-C++到lua,quick或者cocos2d-x对于lua绑定其实是基于tolua++进行封装的,为了读者更好的了解绑定原理,文章的开始会简单介绍lua绑定和tolua++(第三方软件软件包)绑定原理。文中关于quick的luabinding说明完全基于quick开发团队官网关于luabinding的说明,为了方便理解做了一下简单调整和说明,如果你已经了解并且只想知道quick中luabinding的使用可以直接从第三章开始阅读。
1. lua绑定原理
在直接讲解lua绑定原理前,我们先进行一下扫盲,照顾一下一些对lua不是很了解的读者。并且下面的分析对lua脚本整合于游戏引擎的模式进行分析。
1.1 什么是lua
lua是一种免费的、轻量的、独立的、可扩展的嵌入式程序语言,具有变量无类型、动态定义类型、面向对象、编译产生中间代码和内存自动回收的特点。正因为其这新特点常被作为一种脚本嵌入到其他系统中。
Lua是由标准的ANSIC语言实现的一个静态库,因此有对应各种操作系统的版本,简单的说只要支持编制C的操作系统都支持。Lua语法简单但是功能强大,可移植性高,被广泛的作为脚本语言嵌入于主程序中。
1.2 Lua绑定原理
Lua是一种嵌入式语言,依靠虚拟机运行。Lua系统是在state的机制上运行,state中包含运行环境的借口信息,而且每个state只能容纳一个脚本文件装载至内存中,要同时执行多个脚本必须初始化多个state。所以lua与其他系统共享数据是通过栈来实现的。将函数和参数等信息压栈来相互引用。下图展示了lua引用引擎的函数,我们用图文结合的方式进行说明。
从图中可以看出要在引擎中调用lua脚本,必须先起一个state,在lua中要调用引擎中的脚本,需要引擎中先注册,引擎中注册后在乱中就可以调用了。引擎中注册的函数,对于lua而言相当于一个系统函数。在引擎中引用lua函数式同样的原理,依然通过state。见下图
在引擎中调用lua函数,首先需要将函数名压站,然后将参数压栈,然后调用lua_call执行,最后是从栈中取返回值图中没有画出。
看到这里相信大家对于lua与主程是如何相互引用的原理有了一定认识,下面我们直接通过代码演示,加深一下印象。
例1:纯C环境下,注册C函数进LUA环境
Main.c
#include
#include
#include
int foo(lua_State *L)
{
int n = lua_tonumber(L, 1);foo函数参数
lua_pushnumber(L, n + 1);foo 返回值
return 1; foo函数返回值个数
}
int main()
{
lua_State *L = lua_open(); 初始化一个state
luaL_openlibs(L); 加载lua基本库
lua_register(L, "foo", foo);注册C函数foo
luaL_dofile(L, "a.lua");装载,检查,并立即执行a.lua
lua_close(L);与lua_open对应,关闭state
return 0;
}
文件a.lua
print(foo(99))
文件a.lua中就一条语句,加入lua虚拟机编译运行后,在屏幕就看到输出100例2:C环境下,调用lua函数
#include
#include
#include
int main()
{
lua_State *L = lua_open(); 初始化一个state
luaL_openlibs(L); 加载lua基本库
luaL_dofile(L, "b.lua");
lua_getglobal(L, "add");将函数名压栈
lua_pushnumber(L, 51);参数压栈
lua_pushnumber(L, 49);
lua_call(L,2,1);调用最近一次压栈的函数
int result = (int) lua_tonumber(L,1); 从栈获取返回值
lua_pop(L,1);从栈中清除返回值
lua_close(L);
return 0;
}
b.lua
function add(nubmer1, number2)
print(number1)
print(number2)
return number1 + number2
end
例2输出的结果是51 49 100
通过这两个例子相信大家已经看到lua和主程序相互调用非常的简单,这里用的是c语言的一个示例,使用起来很简单,但是我们一般需要帮定给lua的函数有很多,比如真个cocos2d-x的库函数,这么多函数要绑定注册lua,那么需要我们重复写多少上面一样的代码,而且每个库绑定都需要重新写写,这些繁琐的工作也是一个非常痛苦的事情,更疼苦的是lua的本质是C,提供的函数都是C函数,要把一个C++的类注册进lua形成一个table是不是要做更多工作来转换,但是这些仍然是重复性。既然是重复性的工作,那么有没有什么工具或者库来帮我们完成这样的工作呢。答案当然是肯定的,这就是我们接下来要讲的tolua++,一个三方软件包。
2. tolua++绑定原理
2.1 什么是tolua++
tolua++是一种第三方的软件包,可以为Lua提供面向对象的特性,更直接的说tolua++ 是一个将 C/C++ 的函数和对象导出给 Lua 脚本使用的工具,即帮我们完成我们上面的重复性的工作。
2.2 为什么要使用tolua++来注册C类
因为Lua的本质是C,不是C++,Lua提供给C用的API也都是基于面向过程的C函数来用的,要把C++类注册进Lua形成一个一个的table环境是不太容易一下子办到的事,因为这需要绕着弯地把C++类变成各种其他类型注册进Lua,相当于用面向过程的思维来维护一个面向对象的环境。这其中的细节就不去深究了,总之正是因为如此,所以单纯地手写lua_register()
等代码来注册C++类是行不通的、代价高昂的,所以需要借助toLua++这个工具。
2.3 如何使用tolua++
使用这个工具的基本步骤:
将要导出的 C/C++ 函数和对象定义写入 .pkg 文件;
运行 tolua++ 工具,将 .pkg 文件编译为目标 .cpp 文件;
将目标 .cpp 文件加入项目,在启用 Lua 虚拟机后调用目标文件中的 open() 函数注册导出的内容。
看到这里相信大家最想知道的什么是pkg文件,pkg文件内容怎么写, 简单说pkg文件就是一个遵循一定规则的文本文件,规则很简单和我们的头文件差不多,只是表明我们那些类,那些函数需要暴露给lua使用,具体的规则和使用可以去官网上学习,或者参考tolua++源码中的例子,其他的不多说了,直接给大家来个例子就明白了。
例1:tolua++的使用
// file: MyClass.h
#include
class my_class
{
public:
void greet()
{
std::cout << “Hello World!” << std::endl;
}
};
// file: mylib.pkg 该pkg文件声明我们要绑定到lua中的类和类函数
$pfile “UsingIt.h”
class my_class
{
my_class();
~my_class();
void greet();
};
利用工具tolua++.exe 生成cpp文件
>>tolua++ –n mylib –o mylib.cpp mylib.pkg
然后将生成mylib.cpp 文件加入你要编译的工程中编译,并且链接tolua++库
// file: Main.cpp
#include
#include
#include “MyClass.h”
int main()
{
int tolua_mylib_open (lua_State*) ;声明
lua_State* L = lua_open();
luaL_openlibs();
tolua_mylib_open(L); // 打开mylib,该函数在cpp中有实现。
luaL_dofile(L, “mytest.lua”); // 执行脚本文件
lua_close(L);
return 0;
}
测试lua文件
//mytest.lua
local my = my_class()
my:greet()
看到这里大家对tolua++的用法应该已经明白了吧,把上面生成cpp的步骤写个脚本是不是更方便,还有int tolua_mylib_open (lua_State*) ;要放在我们自己的文件声明是不是很怪,其实解决这个问题很简单只要tolua++在生成一个头文件就可以了。我们接下来的quick就为我们提供了这样的支持,在quick上述得操作就有事一个脚本和一个.tolua的文件,然后会生成一个cpp和.h 文件,只要我们把生成的cpp和.h加入工程就可以了,是不是很方便了,并且quick和cocos2dx本身就提供了对lua的支持,所以我们使用quick需要导入我们自己的类就可以了。就不在这里多说,下一章我们将详细讲解quick中的luabinding工作原理,也是我们本篇文章的重点。
3. quick 中导出C/C++ API给LUA使用
本章主要是重点是讲解在quick-cocos2d-x 中将自定义C-C++ API导出给lua使用。大家会很奇怪,这片文章主要是讲quick中luabinding的使用,为什么要将前两章的内容呢,其实前两章非常重要,只有了解lua注册的具体细节,才能理解使用tolua++这个工具的必要性,同时才会思考tolua++带来的优缺点,然后才能理解quick中compile-luabinding.bat帮助我们到处C-C++ API脚本的好处,才能真正了解怎么使用。到目前为止quick和cocos2d-x + lua 都是基于tolua++的。未来可能会摆脱对tolua++的依赖,反正quick团队是这样说的。如果你阅读本文只是为了学习lua和tolua的绑定原理,那么接下来的内容就没有必要看了,接下来内容只对使用quick-cocos2d-x进行开发的人有用。我们进入正题。
3.1 基本步骤
cocos2d-x 和 quick-cocos2d-x 的底层代码都是使用 C++ 语言开发的。为了使用 Lua 脚本语言进行开发,我们利用 tolua++ 工具,将大量的 C/C++ API 导出到了 Lua 中。只是各自进行实现了一下脚本帮助cocos2d-x和quick的使用者。下面是quick中使用tolua++的基本步骤:
从 C/C++ 源代码复制头文件的内容到 .tolua(tolua++ 文档中称为 .pkg)文件中。
修改 .tolua 文件内容,去掉 tolua++ 无法识别的内容,以及不需要导出到 Lua 的定义。
运行 tolua++ 工具,根据 .tolua 文件生成 luabinding 接口文件(由 .cpp 文件和 .h 文件自称)。
在 AppDelegate.cpp 中加载 luabinding 文件。
在 AppDelegate 初始化 Lua 虚拟机后,调用 luabinding 接口文件中的 luaopen 函数,注册 C/C++ API。
3.2 实战演示
我们通过实际的例子先来演示quick中tolua++的使用,然后再去解释其细节。
创建一个quick-cocos2d-x 3.3rc1的工程,打其目录,看看我们需要在哪里进行操作。如创建一个名为game3.3rc1的工程。其目录结构如下。
我们需要要关注的目是:
在这个目录中我们可以看到两个脚本,还有一些.tolua的文件,以及一些只是扩展名不同的.h和.cpp文件。对的,你猜对了。这些tolua文件就是pkg文件,对应的.h和.cpp就是对应的使用tolua++生成的。这个脚本看其名字就应该知道是干什么的吧。当然是看不出来,我们先来看一下这个脚本build.bat的内容:
这个脚本的功能就是讲刚刚的.tolua文件转换成对应的.和.cpp文件,还指定了一些参数。对于脚本compile_luabinding.bat 实际上就是封装了tolua++的使用,其实其下层脚本是用php封装的,有兴趣的读者可以去研究一些封装的细节。对应使用者我们只需要搞清楚这几个参数的意义然后依葫芦画瓢就可以。
compile-luabinding.bat 脚本使用参数的意义
从上面build.bat 中我们可以知道将.tolua文件转换成.h和cpp的实际上compile_luabinding这个脚本的,下面对其参数进行说明:
-prx cc ,将这些API或者类导入到lua后使用前缀
-d ,指定生成cpp和h文件存放的路径
-E,指定某个类需要按照某种方式处理,否则可能会出现内存泄露等问题。
CCObject 及其继承类都具备“引用计数”和“自动释放”机制。如果你的 C++ 对象是从 CCObject 继承的,那么必须告诉 tolua++ 做相应处理,否则可能出现内存泄漏等问题。
对于 quick,只需要在 build 脚本中通过 -E CCOBJECTS 参数指定这些 class 的名字即可。
如果有多个类,那么每个类名之间用“,”分隔即可
ompile-luabinding.bat -E CCOBJECTS=MyClass,MyClass2,MyClass3 -d "$OUTPUT_DIR" MyClass_luabinding.tolua
从C/C++源文件编写一个.tolua++文件
假设我们的 MyClass.h 头文件内容如下:
#ifndef __MY_CLASS_H_
#define __MY_CLASS_H_
class MyClass :public CCObject
{
public:
static void addTwoNumber(float number1, float number2);
private:
MyClass(void) {}
};
#endif // __MY_CLASS_H_
为了便于维护,应该将 .h 文件对应的 tolua 命名为 XXX_luabinding.tolua。这样生成的 luabinding 接口文件名就是 XXX_luabinding.cpp 和 XXX_luabinding.h,不会和已有的 C/C++ 源文件冲突。
创建 MyClass_luabinding.tolua 文件,详细的内容修改规则,会在本文后续部分说明。并修改内容为:
class MyClass : public CCObject
{
public:
static void addTwoNumber(float number1, float number2);
};
生成 luabinding 接口文件
在刚刚的build.bat脚本中添加下一行,或者自己新建一个脚本:
call %MAKE_LUABINDING% -E MyClass –pfx my -d %OUTPUT_DIR% MyClass_luabinding.tolua
如果一切顺利:
creating file: MyClass_luabinding.cpp
creating file: MyClass_luabinding.h
// add to AppDelegate.cpp
#include "MyClass_luabinding.h"
// add to AppDelegate::applicationDidFinishLaunching()
CCLuaStack* stack = CCScriptEngineManager::sharedManager()
->getScriptEngine()
->getLuaStack();
lua_State* L = stack->getLuaState();
luaopen_MyClass_luabinding(L);
载入 luabinding 接口文件
打开我们的项目,将 MyClass_luabinding.cpp 和 MyClass_luabinding.h 文件加入工程。然后修改 AppDelegate.cpp 文件:
在 AppDelegate.cpp 头部区域添加:
#include "MyClass_luabinding.h"
在 AppDelegate::applicationDidFinishLaunching() 函数内添加:
luaopen_MyClass_luabinding(L);
注意这一行代码应该添加在其他 luaopen 函数后面,例如:
// register lua engine
CCLuaEngine *pEngine = CCLuaEngine::defaultEngine();
CCScriptEngineManager::sharedManager()->setScriptEngine(pEngine);
CCLuaStack *pStack = pEngine->getLuaStack();
lua_State* L = pStack->getLuaState();
// load lua extensions
luaopen_lua_extensions(L);
// load cocos2dx_extra luabinding
luaopen_cocos2dx_extra_luabinding(L);
// thrid_party
luaopen_third_party_luabinding(L);
// CCBReader
tolua_extensions_ccb_open(L);
// MyClass
luaopen_MyClass_luabinding(L);
经过上述修改后,重新编译运行项目应该就可以在 Lua 脚本中使用我们导出的 MyClass 对象极其方法了。
在lua中使用导出的类
// test.lua
my.MyClass.addTwoNumber(1,2);
3.3 tolua 文件内容的修改规则
前面的 MyClass 是一个非常简单的例子,但我们实际游戏中的 C/C++ API 可能比较复杂。在修改 .tolua 文件内容时,应该仔细阅读以下内容。
3.3.1 删除所有无需在 Lua 中使用的内容
导出的 API 越多,在 Lua 虚拟机中占用的符号表空间就越多。因此我们第一步要做的就是删除所有无需在 Lua 中使用的内容。
对于 enum、宏定义,如果需要导出,原文保留即可。但宏定义只能导出数值定义,例如:
define kCCHTTPRequestMethodGET 0
#define kCCHTTPRequestMethodPOST 1
而非数值的宏定义无法导出,以下内容会导出失败:
#define kMyConstantString "HELLO"
删除所有无法识别的宏,例如 CC_DLL。
删除 C++ class 中所有非 public 的定义。
删除 C++ class 中的类成员变量
删除 inline 关键词,以及 inline function 的实现,只保留声明。
3.3.2 处理 CCObject 继承类
CCObject 及其继承类都具备“引用计数”和“自动释放”机制。如果你的 C++ 对象是从 CCObject 继承的,那么必须告诉 tolua++ 做相应处理,否则可能出现内存泄漏等问题。
对于 quick,只需要在 build 脚本中通过 -E CCOBJECTS 参数指定这些 class 的名字即可。
例如前面 MyClass 的示例中,用 -E CCOBJECTS=MyClass 告诉 tolua++ 应该将 MyClass 当作 CCObject 的继承类进行处理。
如果有多个类,那么每个类名之间用“,”分隔即可,例如:
ompile-luabinding.bat -E CCOBJECTS=MyClass,MyClass2,MyClass3 -d "$OUTPUT_DIR" MyClass_luabinding.tolua
3.3.3 展开宏
有些宏是不能直接删除的,例如 CC_PROPERTY。对于这类宏,需要根据宏定义,将宏展开为声明。
CC_PROPERTY(float, m_fDuration, Duration)
展开为:
float getDuration();
void setDuration(float v);
需要如此处理的宏包括:CC_PROPERTY_READONLY, CC_PROPERTY, CC_PROPERTY_PASS_BY_REF,CC_SYNTHESIZE_READONLY,CC_SYNTHESIZE_READONLY_PASS_BY_REF,CC_SYNTHESIZE,CC_SYNTHESIZE_PASS_BY_REF,CC_SYNTHESIZE_RETAIN。
幸运的是这些宏大多只用在 cocos2d-x 基础代码里,我们自己的 C++ class 还是不要用这些宏了。
3.3.4 处理名字空间
如果使用了名字空间,那么在 .tolua 的头部应该加入:
$using namespace myname;
这里用到的“$”符号,后续内容会原样放入 luabinding 文件。
3.3.5 添加必要的 #include 指令
如果生成的 luabinding 接口文件无法编译,需要检查是否是需要 include 相应的头文件,并添加如下代码:
$using namespace myname; $#include "MyClass.h"
3.3.6 修改函数参数和返回值类型,去除 const 修饰符
一些函数的参数或返回值,使用了 const 修饰符。由于 tolua++ 的限制,并不能很好的处理这类定义,所以我们要从 .tolua 文件中移除 const 修饰符。唯一例外的就是 const char* 不需要修改为 char*。
例如:
CCPoint convertToNodeSpace(const CCPoint& worldPoint);
应该修改为:
CCPoint convertToNodeSpace(const CCPoint& worldPoint);
这样修改的原因是 tolua++ 把 const CCPoint 和 CCPoint 当做两个不同的类型来处理。如果不做修改,那么调用函数时会报告参数类型不符。
3.3.7 从 C/C++ 函数返回多个值
如果一个函数的所有参数都是引用或指针类型,并且不是 const char*,那么在 luabinding 接口文件中,该函数会返回多个值。
例如:
void getPosition(float* x = 0, float* y = 0);
在 Lua 中调用这个函数,会得到两个返回值:
local x, y = node:getPosition()
3.3.8 将 Lua 函数传入 C/C++
quick 里,允许将 Lua 函数传入 C/C++,只要求 C/C++ 函数中使用 int 做参数类型。但在 .tolua 文件里,则必须使用 LUA_FUNCTION 做参数类型。
例如:
static CCHTTPRequest* createWithUrlLua(int listener,
const char* url,
int method = kCCHTTPRequestMethodGET);
listener 参数用于保存传入的 Lua 函数,所以 .tolua 文件里要改写为:
static CCHTTPRequest* createWithUrlLua(LUA_FUNCTION listener,
const char* url,
int method = kCCHTTPRequestMethodGET);
具体用法请参考 lib/cocos2dx_extra/extra/network/CCHTTPRequest 中的 createWithUrlLua() 方法。
3.3.9 在 Lua 和 C/C++ 间交换二进制数据
要从 C/C++ 返回二进制数据给 Lua,函数返回值类型必须是 int,而 .tolua 文件中修改返回值为 LUA_STRING。函数中,需要用 CCLuaStack::pushString() 将二进制数据放入 Lua stack。然后返回“需要传递给 Lua 的值”的数量。
具体用法请参考 lib/cocos2dx_extra/extra/network/CCHTTPRequest 中的 getResponseDataLua() 方法。
从 Lua 传递二进制数据给 C/C++ 很简单,使用 const char* 参数类型和 int 类型参数分别指定二进制数据的指针和数据长度。
具体用法请参考 lib/cocos2dx_extra/extra/crypto/CCCrypto 中的 decryptXXTEALua() 方法。
3.3.10 更多用法
关于利用 tolua++ 的更多用法,建议参考 lib/cocos2dx_extra 中的 CCCrypto、CCNative、CCHTTPRquest 等 class。这些 class 对 Lua 提供了良好的支持,具体用法上也覆盖了绝大多数 C/C++ 和 Lua 交互的需求。