这是关于 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
类和接口
原生 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, Argv&&... argv)
{
return FunctionCaller<N - 1>::exec(f, l, argoffset,
FuncArg(l, N + argoffset),
std::forward<Argv>(argv)...);
}
......
};
template<>
class FunctionCaller<0> {
public:
template<typename FuncRetType, typename... FuncArgType, typename... Argv>
static int exec(FuncRetType (*f)(FuncArgType...), lua_State* l, int,
Argv&&... argv)
{
pushres(l, f(std::forward<Argv>(argv)...));
return 1;
}
template<typename... FuncArgType, typename... Argv>
static int exec(void (*f)(FuncArgType...), lua_State* l, int,
Argv&&... argv)
{
f(std::forward<Argv>(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 的支持,这个还没用到,先放一边。