lua与c++的交互¶
在书写策略时,一定会遇到lua层需要获取c++的数据,或调用c++的函数等交互的需求。本文会举例说明在Rocos系统中多种不同的lua与c++的交互方式,并进行应用场景的比较。
总览¶
lua作为一种胶水语言,其与宿主语言c/c++的交互是必不可少的。在Rocos中,我们将交互分为三类
c++对lua的解释执行¶
由于lua的解释器是由c编写,从c/c++到lua的调用可以理解为:将lua语言作为整个字符串交给解释器执行。在Rocos中,体现为对于lua脚本的调用执行,对于的函数为LuaModule
中的RunScript(id)
。 该函数分别在DecisionModule
的初始化和每一帧执行中分别对StartZeus.lua
和SelectPlay.lua
进行了调用。
lua调用c/c++的函数¶
:name: method2 相较于c++对lua的解释执行的单一性来说,在具体执行lua时多次进行与c/c++的交互更为常见。总结来说,lua对于c的调用都是通过lua的虚拟栈来完成的。我们以rocos
中的CGetSettings
为样例,展示lua对c函数的调用。该函数的作用是获取配置中的相应参数所对应的值,例如通过获取IsYellow
来判断当前是否为黄方。
在LuaModule.cpp
中,有这样一段代码:
extern "C" int FUNC_GetSettings(lua_State* L){
QString key(LuaModule::Instance()->GetStringArgument(1, NULL));
QString type(LuaModule::Instance()->GetStringArgument(2, NULL));
if(type == "Bool"){
bool temp;
...
LuaModule::Instance()->PushBool(temp);
...
}
return 1;
}
开头的extern "C"
表示这是一段c语言的代码,这段代码定义了一个函数,类型为:
typedef int (*lua_CFunction) (lua_State *L);
任何用于被lua调用的函数通常包含取
和存
两个步骤,取
是为了获取从lua传入的参数,存
是为了将执行的返回值还给lua。在上述函数中,通过调用了LuaModule
封装过的GetStringArgument
来获取第一和第二,两个字符串类型的参数,通过PushBool
将结果存入栈中返回给lua使用。类似的调用接口还有:
// 获取参数
GetStringArgument();
GetNumberArgument();
GetBoolArgument();
// 存入返回值
PushString();
PushNumber();
PushBool();
这些定义都是在LuaModule
中对于lua的原有c-api的再次封装。
上述定义好的函数,被存放在一个键值对数组中(LuaModule.cpp:GUIGlue
),相关的定义如下:
luaDef GUIGlue[] = {
...
{"CGetSettings", FUNC_GetSettings}, // 定义函数在lua中的symbol
...
{NULL, NULL} // 定义键值对数组的末尾值
};
完成上述两步后,我们可以在lua中使用CGetSettings
对函数进行调用。
在rocos中,从lua的task中对于c++/Skill的调用也使用了类似的方法。不过在即将到来的v0.1
版本中,我们将使用tolua++
的交互方式代替现有的方式,以此来规避掉创建一个新的c++的skill时的繁琐的书写方式。
使用tolua++框架进行交互¶
在上述的交互方式中,我们只描述了如何使用c/c++的基础类型与lua进行交互,忽略了利用struct或者array进行交互的过程。但在实际应用中,使用到class或struct才能更好的结合c++特性发挥lua的动态特性。在lua的官方文档中,提供了userdata
/metatable
等多种方式来实现对于c++的类的封装。但这些方式都需要我们手动的书写大量的代码,而且在不同的类之间的交互方式也不尽相同。为了解决这个问题,tolua++
框架应运而生。
我们以一个最简单的Point
为例,展示如何使用tolua++
来进行交互。首先,我们需要定义一个Point
类,如下:
class Point{
public:
int x;
int y;
Point(int x, int y):x(x),y(y){}
int GetX(){return x;}
int GetY(){return y;}
};
在没有使用tolua++
的情况下,我们需要手动的书写Point
类的封装,例如想要实现一个构造函数,代码如下:
// 手写一个用于在lua使用的Point的创建函数
extern "C" int Point_new(lua_State* L){
int x = luaL_checkinteger(L, 1);
int y = luaL_checkinteger(L, 2);
Point** p = (Point**)lua_newuserdata(L, sizeof(Point*));
*p = new Point(x, y);
luaL_getmetatable(L, "Point");
lua_setmetatable(L, -2);
return 1;
}
这样的过程机械而繁琐,而且在不同的类之间的交互方式也不尽相同。于是tolua++
框架应运而生,它可以自动的将c++的类封装成lua的类,而且还可以自动的处理类之间的继承关系。在tolua++
中,我们只需要定义一个pkg
文件,然后通过tolua++
工具自动生成相应的c++代码。pkg
文件的定义可以直接照抄:
// Point.pkg
class Point
{
int x;
int y;
Point(int x, int y);
int GetX();
int GetY();
};
那么可以如下的lua代码可以使用Point
类:
local p = Point:new_local(1, 2)
print(p:GetX(), p:GetY())
值得注意的一点是,这里在调用GetX()
时使用的是:
而不是.
,在lua中,:
表示的是将self
作为第一个参数传入函数,而.
则不会传入self
。 p:GetX()
的意思可以认为是Point.GetX(p)
。这样的语法糖也曾经出现在最初的c++的编译器中用于将c++的oop用法转换成c的函数调用。
在Rocos的实现中,tolua++
被用于几乎所有与class相关的交互中,例如获取vision
信息,绘制debug
信息等。在编译过程中,pkg
文件中的定义会被tolua++
工具自动的转换成c++代码,编译进Core中。因为修改pkg
文件后需要重新执行cmake指令来重新生成c++代码。
总结¶
从上述内容可以得出,想要从lua调用c/c++的函数,可以选择直接调用或利用tolua++框架进行交互。在实际应用中,可以根据自己的需求进行选择。如果只需要进行函数级别的交互,则选取直接调用更为简单,代码也更为直观。如果需要进行类级别的交互,尤其是涉及到Rocos大量出现的单例模式时,则可以使用tolua++框架。