Lua C++ bindings (2)

归档: 软件开发 | 标签:

这是关于 lua-cpp 的第二篇博客,上一篇在 这里。这里主要是根据 git 的提交日志回忆一下实现过程,有点意识流,想到哪说那了。btw,代码在 这里

设置和获取变量

首先是一般的变量的获取。返回数值比较好办,字符串也可以返回 std::string,但是像 table 和函数这样的类型就不好直接返回了。一个直接的想法就是在外面封装一层,记下 lua_State 和对应的位置。刚开始就是直接记的栈内的 index,但是当某个对象析构之后在这个对象之后创建的对象的 index 都要改变,因此这个方法行不通。后来翻 API 的时候发现可以用 luaL_ref() 和 luaL_unref(),问题就解决了。

一个不算太重要但是也比较基础的问题是这些类型的生存期问题,其实也就是 lua_State 的作用域问题。Lua 本来就要求使用者自己保证,但是这个保证有函数调用来强制(Lua 的函数都需要提供 lua_State),如果封装成 C++ 后每个函数都要求传入 LuaState 就太不专业了,于是想到在每个类里加一个 std::shared_ptr,然后判断引用计数来决定是否析构;后来在查手册的时候发现 shared_ptr 居然还支持自定义析构函数,而 lua_close() 正好符合要求的析构函数形式,这样 lua_State 就完全由 shared_ptr 管理了。

类和接口

原生 Lua 对于获取不同类型的做法都是通过名称或索引来获取,然后判断类型,再转换成对应的类型。在 lua-cpp 中也使用了同样的流程,即 LuaState::get() 只返回一个 LuaObject 的类型,然后通过 LuaObject::type() 判断类型,再调用 LuaObject::to* 系列函数来转换。这样比起直接 get() 的做法要啰嗦,不过感觉前者和原生的流程一致,而后者可能要判断转换是否成功或者在转换出错时抛异常,无形中增加了出错处理的成本,其实也差不多,最后还是用了现在的方法。

在类继承层次关系上,刚开始的时候只有一个 LuaObject(很自然地,原生的 Lua get() 得到的就是这样一个 object),然后所有 to*() 系列函数都放在这个类里;后来随着 LuaTable 出现,发现这些类不能再转换成别的类型,直接继承 LuaObject 在逻辑上是不对的。其实这里忽略了动词:“转换”,虽然也可以说 LuaTable “是一种” LuaObject。后来想到 LuaTable 只是使用了 luaL_ref() 的功能,转换后已经是一种新的类型了,因此把 LuaRefObject 抽象出来。另外如果 type() 和 typestr() 放在 LuaObject 中,而 LuaTable 也应该有获取类型的功能,但是后者又不能直接继承 LuaObject(因为 table 不能再转换成字符串和整数了)。所以现在的类继承关系是这样的:

                                +--------------+
                                | LuaRefObject |
                                +--------------+
                                       ^
                                       |
      +--------------+-----------------+-----------------+-----------------+
      |              |                 |                 |                 |
+-----------+   +----------+    +-------------+     +----------+     +-------------+
| LuaObject |   | LuaTable |    | LuaFunction |     | LuaClass |     | LuaUserdata |
+-----------+   +----------+    +-------------+     +----------+     +-------------+

LuaTable,LuaFunction,LuaUserdata 都是简单的封装;LuaClass 的实现包含两个 table:一个是类本身,包含构造函数和静态成员函数;另一个作为类实例的 metatable,包含一般的成员函数和静态成员函数。实例化的时候生成 userdata 然后关联对应的 metatable 就行了。

LuaTable::foreach() 的参数刚开始是一个“裸”函数指针,即“bool (*func)(…)”,后来在用 lambda 函数的时候报错,才转成 std::function。

提两句接口的设计。使用 set() 系列重载函数而不是直接用模板的原因有两个,一个是没法对所有类型统一处理,例如字符串和数值的处理就不一样;另一个原因也是限制可以设置的类型。至于重载 set() 而不是叫 setnumber(),setstring() 这样的名字,是为了方便使用者用模板来处理不同的类型。

导出自定义函数

刚开始的时候函数导出功能只支持 Lua 标准的导出函数的形式,即 typedef int (lua_CFunction)(lua_State),但是如果暴露了 lua_State 说明要使用 Lua 的 C API,也就是说穿透了封装层;而且对于已有的函数,还要重新封装一次才能导出,这也加大了工作量,跟没有封装差不多。总的来说,导出自定义函数最麻烦的部分就是怎样调用这个自定义函数,这个是阻碍当时继续写下去的主要原因。

首先是怎样把函数保存起来。刚开始想到的方法是用 std::bind,就是一层层地解析模板参数,最后返回的是一个不带任何参数的 std::function,到真正要调用的时候直接调用。仔细一想其实行不通,因为注册函数的时候根本没有参数,只有函数类型信息,没法绑定。后来想到的方法是用一个通用的 lua_CFunction,然后把需要执行的函数作为这个函数的参数,执行的时候再把函数取出来,然后从 lua_State 中取出参数传给需要执行的函数。

通用的函数大概长这样:

template<typename FuncRetType, typename... FuncArgType>
static int l_function(lua_State* l)
{
    typedef FuncRetType (*func_t)(FuncArgType...);

    int argoffset = lua_tonumber(l, lua_upvalueindex(1));
    auto func = (func_t)lua_touserdata(l, lua_upvalueindex(2));
    return FunctionCaller<sizeof...(FuncArgType)>::exec(func, l, argoffset);
}

第一个参数表示从 lua_State 的哪个位置开始取第一个参数,这是因为 Lua 使用面向对象形式调用函数的时候会把 userdata(或 table 本身)作为第一个参数传过来,为了统一调用过程就加了这么一个参数,这个在后面介绍 class 导出的时候再细说。

第二个参数就是实际要调用的函数,来看看怎样导出一个自定义函数:

template<typename FuncRetType, typename... FuncArgType>
LuaFunction LuaState::newfunction(FuncRetType (*func)(FuncArgType...), const char* name)
{
    lua_pushinteger(m_l.get(), 0); // argument offset
    lua_pushlightuserdata(m_l.get(), (void*)func);
    lua_pushcclosure(m_l.get(), l_function<FuncRetType, FuncArgType...>, 2);
    ......
}

可以看到自定义函数被转换成 void* 指针作为 l_function() 的第二个参数。

通过这两部分代码,怎样保存自定义函数和最后调用的流程已经很清楚了,接下来就是最麻烦的部分:怎样调用,这也是 FunctionCaller 这个类的工作。

从 Lua 调用函数的时候,结果存放在 lua_State 中,而要调用真正的函数的时候,要将这些结果取出来然后一次性传递给函数。看了变长参数模板后在 stackoverflow 搜到这个:How do I expand a tuple into variadic template function’s arguments?。后来想想跳过 std::tuple 直接解析 lua_State 中的参数不也可以么?于是就有了下面的模板(只贴了调用一般函数的部分):

template<uint32_t N>
class FunctionCaller {

    public:

        template<typename FuncType, typename... Argv>
        static int exec(FuncType f, lua_State* l, int argoffset,
                        const Argv&... argv)
        {
            return FunctionCaller<N - 1>::exec(f, l, argoffset,
                                               FuncArg(l, N + argoffset),
                                               argv...);
        }

        ......
};

template<>
class FunctionCaller<0> {

    public:

        template<typename FuncRetType, typename... FuncArgType, typename... Argv>
        static int exec(FuncRetType (*f)(FuncArgType...), lua_State* l, int,
                        const Argv&... argv)
        {
            pushres(l, f(argv...));
            return 1;
        }

        template<typename... FuncArgType, typename... Argv>
        static int exec(void (*f)(FuncArgType...), lua_State* l, int,
                        const Argv&... argv)
        {
            f(argv...);
            return 0;
        }

        ......
};

这里举个例子分析一下 FunctionCaller 的工作流程。假设要导出一个函数 add(),作用就是返回两个整数相加的和:

int add(int a, int b)
{
    return (a + b);
}

l.newfunction(add, "add");

Lua 脚本中调用这个函数:

a = add(3, 2)

当调用 add() 的时候,Lua 根据符号表找到通过 newfunction() 导出的函数 l_function<int, int, int>,接着 l_function() 取出真正的函数 add(),将

return FunctionCaller<sizeof...(FuncArgType)>::exec(func, l, argoffset);

变为

return FunctionCaller<2>::exec(add, l, 0); // 参数 func 就是 add

由于模板类的参数 2 不为 0,生成一个实例:

template<2>
class FunctionCaller {

    public:

        template<int (*)(int, int), typename... Argv>
        static int exec(FuncType f, lua_State* l, int argoffset)
        {
            return FunctionCaller<2 - 1>::exec(f, l, argoffset,
                                               FuncArg(l, 2 + argoffset));
        }

        ......
};

在 exec() 中,由于模板参数不为 0,因此又生成另一个调用(注意,这时 exec() 最后已经有一个参数了,这个参数是我们要调用的函数的最后一个参数):

template<1>
class FunctionCaller {

    public:

        template<int (*)(int, int), typename... Argv>
        static int exec(FuncType f, lua_State* l, int argoffset,
                        const FuncArg& argN) // 多了这个参数,即上一轮传入的 FuncArg(l, 2 + argoffset)
        {
            return FunctionCaller<1 - 1>::exec(f, l, argoffset,
                                               FuncArg(l, 1 + argoffset), // 这一轮调用新增的参数
                                               argN); // 被放到最后
        }

        ......
};

最后类模板参数为 0,匹配到的是偏特化的那个类:

template<>
class FunctionCaller<0> {

    public:

        // 对应于模板,第一个 int 是 FuncRetType, 第二和第三个 int 是 FuncArgType, 最后两个 FuncArg 是 Argv
        template<int, int, int, FuncArg, FuncArg, typename... Argv>
        static int exec(int (*f)(int, int), lua_State* l, int,
                        const FuncArg& arg1, // FuncArg(l, 1 + argoffset)
                        const FuncArg& arg2) // FuncArg(l, 2 + argoffset)
        {
            pushres(l, f(arg1, arg2));
            return 1;
        }

        ......
};

可以看到,在偏特化的类里实现了最终的调用,而之前的类都是对参数进行解包。

这里的 FuncArg 是一个简单的包装,主要是重载了几个类型转换的操作符,作用是将 lua_State 中的某个位置的变量转成实际函数需要的参数类型,简单贴一下代码:

class FuncArg {

    public:

        operator lua_Number () const { return lua_tonumber(m_l, m_index); }
        operator const char* () const { return lua_tostring(m_l, m_index); }

        ......
};

在传给实际函数的时候,这些重载的 operator 就会被调用,将 lua_State 中对应位置的变量转换成对应的参数。

至于区分有无返回值,就是通过特化 FunctionCaller<0>::exec() 的函数参数返回值为 void。

导出自定义类

对应于 C++,在 Lua 中也分类定义和实例化。

首先是类定义,用 table 实现就可以了,自身作为自己的 metatable,构造函数和静态成员函数放在 metatable 中。为了用起来更像真的类,这里将构造函数赋值给 metatable 的成员“__call”,这样就能像“c = ClassName(…)”那样生成一个实例。因为调用“__call”的时候会把自身作为第一个参数传过去,因此在实现 FunctionCaller::exec() 的时候多加了个 argoffset 参数,跳过了第一个参数。后面调用类成员函数的时候也是类似的做法。

静态成员函数和普通函数其实是一样的,要注意的是要把静态成员函数放到类 table 本身的 metatable 和类实例的 metatable 中,这样静态成员函数才既能在不生成实例的时候被直接使用,也可以被实例调用。

注册类成员函数有点麻烦,因为 C++ 不允许将类成员函数指针和 void* 互转,网上的说法是成员函数指针其实不算是一个真实的指针,很有可能是比较复杂的实现。后来想想也是,模板类的成员函数或者模板函数有若干实例,不可能都一样的地址。既然不能直接转,封装到一个类里然后将类实例转成 void* 也是一样的。然后发现类成员函数还会带 const 修饰。当然可以在保存函数指针的时候将 const 去掉,但是最终还是选择了用一个 union MemberFuncWrapper 保存了两种不同的函数指针。相应地,FunctionCaller::exec() 也重载了几个版本,主要是将类实例(也就是 userdata)作为第一个参数传入,这样在最后才能用“(obj->*f)(argv…)”这样的形式调用。

userdata

类实例化使用 userdata + metatable 实现。本来只打算提供类导出功能的(也就是在 Lua 可以生成导出的类实例,但是在 C++ 中无法获得这个实例的内容)。后来为了让类实例对于 C++ 和 Lua 都可见,就定义了一个 userdata 的对应类 LuaUserdata。

刚开始的设计是将 LuaUserdata 定义成一个模板类,初衷是只能由 LuaClass 生成 LuaUserdata,这样确保生成的 userdata 都是有效的:

template<typename T>
class LuaUserdata { ... };

后来觉得 LuaClass 从注册完了之后就不应该被修改,因此 LuaClass 不能从 LuaObject 转换得到(LuaClass 的实现是一个 table,如果允许调用 type() 的话得到一个 table 类型会让人迷惑,不让调用 type() 又无法判断是否能转成 LuaClass,即便类型是 table 也不一定能转成 LuaClass),于是如果想在 C++ 中生成导出的类实例就必须一直保存 LuaState::newclass() 返回的结果(如果这样,导出类不能被修改的限制就没啥用了,因为可以随时使用 LuaClass::set() 来修改),因此把定义中的模板去掉了,只在最后转换的步骤加入了模板限制,即必须将 userdata 转换成类实例的指针。其实这个限制也没啥用,毕竟你爱将指针转成啥谁都管不着,加上只是作为一个不要乱用的提醒而已。

其它

目前的功能还有些限制。

首先是要导出的 C++ 函数的参数和返回值都必须是数值或指针,如果是引用或值的话都不行。这个限制的原因在于不知道怎样将 Lua 中的内容转换成对应的类型。一个想法是将 LuaFunction 和 LuaClass 也实现为一个模板类,要求为其中的函数用到的参数和返回值都定义转换函数,这些转换函数作为重载的 operator 封装到一个类中,并将这个类作为模板的参数;或者传入一个抽象类指针也可以。不过目前并没有用到这么这些功能,因此也就没实现。

然后是 lua_CFunction 的支持。毕竟已经有了很多开发好的 Lua 模块,如果想导入的话目前还没有定好接口,只能通过 LuaState::ptr() 取得 lua_State 指针自己处理。

另外还有 coroutine 的支持,这个还没用到,先放一边。

发表于 2015年3月8日
本文目前尚无任何评论.

发表评论

XHTML: 您可以使用这些标签: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>