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.luaSelectPlay.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作为第一个参数传入函数,而.则不会传入selfp: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++框架。

参考资料: