基于反射在 C++ 中嵌入 Lua
示例代码
本项目有一些示例代码,在 LinHeLurking/lua.cc (github.com)
C API
Lua 本身是有比较详细的 C API 的,利用这套 API 可以完成 Lua 与 C 语言函数的双向调用,具体的内容见 Lua 5.1 Reference Manual - contents (因为带 JIT 编译能力的分支 LuaJIT 停留在 Lua 5.1,所以基本看看 5.1 的特性就够了)。下面介绍一下最基本的概念。
- 数据类型:Lua 除基本数据类型之外,最核心的数据类型是
table
,存储 KV 对。此外,Lua 还提供了metatable
机制,可对特定的某个table
重写根据 K 查找 V 的函数,从而给很多复杂的修改提供了基础。 - Lua 默认的变量(无 local 修饰)都是全局变量,使用 local 修饰的函数只在当前作用域可见。
- Lua 是一个基于栈的语言。函数参数和返回值都是通过栈传递的。Lua 的栈可以用整数指标去访问栈内的任何一个元素。正整数指标表示入栈顺序,例如 1 表示第一个入栈(位于栈底),2 表示第二个入栈(栈底更高一个)等等;负整数指标表示刚刚(即将)出栈的顺序,-1 表示下一次弹栈会弹出的元素(位于栈顶),-2 表示再下一次弹栈会弹出的元素(栈顶更低一个)。在使用 Lua 本身编程时不用关心这一点,但是在使用 C API 的时候需要手动维护。
- C/C++ 调用 Lua 函数
f(a,b)
大概是这个流程:- 根据函数名称
f
查变量表,将查找结果(实际上就是函数指针)压栈 - 将
a
压栈 - 将
b
压栈 - 使用 API 触发函数执行,函数内部依照约定维护调用栈,参数都会被清空,然后返回值被压栈
- 弹栈,获取返回值
- 根据函数名称
- Lua 调用 C++ 函数大概是这个流程:
- 在 C/C++ 定义一个函数,签名必须是
int f(lua_State*)
,该函数需自行满足上述的栈上调用约定,C/C++ 的返回值表示在 Lua 中拿到的返回值个数 - 把该函数及其名字绑定到某个
table
中或者全局作用域
- 在 C/C++ 定义一个函数,签名必须是
C++ API Wrapper
栈的维护
Lua 的 C API 对不同的数据类型是使用了不同的函数,例如将某个变量压栈,空指针、数字、字符串分别需要使用 lua_pushnil
、lua_pushnumer
、lua_pushstring
来执行。如果我们使用 C++,其实可以对这些东西再包装一层,封装出一个统一的 push
、pop
函数方便使用。这既可以用函数重载来做到,也可以用模板。我这里使用模板,方便后续的操作。
自定义 class 绑定
如果是要把 C++ 的自定义类压如 Lua 的栈,有这样两种思路:
- 把整个数据结构复制一次,写成一个大的 Lua
table
,然后把整个table
压栈 - 只传递一个指针,然后通过重写
metatable
把 Lua 的查表函数委托到对应的 C++ Getter/Setter Wrapper 上
上述方法 1 传参开销比较大,但是因为整个数据结构都复制了,不会有潜在的生命周期问题。方法 2 需要保证在 Lua 使用该指针的过程中,该指针始终是有效的,没有并发线程把它 delete。在我的使用场景确保不会有生命周期问题,所以我选择了方法 2。
Advanced Bindings Based on boost::describe
boost::describe
是 Boost 库的一部分,它提供了一系列工具用于编译期反射。下面是一个例子:
#include <boost/describe.hpp>
#include <boost/describe/members.hpp>
#include <boost/describe/modifiers.hpp>
#include <boost/mp11/algorithm.hpp>
#include <iostream>
#include <type_traits>
#include "../common/logging.h"
#include "../util/util.h"
class Person {
public:
int age_;
std::string name_;
std::string career_;
Person(int age, const std::string& name, const std::string& career)
: age_(age), name_(name), career_(career) {}
void greet() const noexcept {
logf("Hello I'm %s and I'm %d years old! I'm a %s!", name_.c_str(), age_,
career_.c_str());
}
void aging(int year) noexcept { age_ += year; }
// Boost reflection declaration
BOOST_DESCRIBE_CLASS(Person, (), (age_, name_, career_, greet, aging), (),
());
};
int main(int argc, char** argv) {
logf("Information about class `Person`:");
logf("---------------------------------");
logf("Public member variables:");
using namespace boost::describe;
using namespace boost::mp11;
using namespace lua_detail;
using M_VAR = describe_members<Person, mod_public>;
mp_for_each<M_VAR>(
[](auto&& D) { logf("%s\t[%s]", D.name, name_from_ptr(D.pointer)); });
logf("---------------------------------");
logf("Public member functions:");
using M_FUNC = describe_members<Person, mod_public | mod_function>;
mp_for_each<M_FUNC>(
[](auto&& D) { logf("%s\t[%s]", D.name, name_from_ptr(D.pointer)); });
return 0;
}
借助该工具库,我们能自动地给类的所有成员变量定义 lambda 函数作为其 getter/setter,并将函数指针存入 map 用于后续 Lua 绑定。当然,只有 lambda without capture 才能转成一个 C 风格的函数指针,因此需要些编程上的取巧之处绕过它,不过基本思想就是这样。我们对常见的几个 STL 模板容器,boost::describe
注册过反射的类都递归地定义注册函数,自定义类的成员变量访问就搞定了。
对于成员函数,情况会复杂一些。因为 C++ 的函数可以传值,可以传指针,可以传左值引用,可以传 const 左值引用,还可以传右值引用。但是按照我们的设计,我们需要向 Lua C API 传递指针。如果要追求模板+反射的自动注册,会有很多边界情况,代码就需要利用模板元编程的一些奇怪技巧。我的例子中 extract_methods
提供了一个原型实现,但也就是勉强能用的水平。
Bidirectional Calling
注册时,先提取 getter、setter和成员函数的代理,然后放入一个 metadata map 中。为了方便,我给每个类型提供了一个平行的 prototype,利用 metadata map 构造并存储了对 metatable
的重写。在实际注册时,就只需要把 metatable
的关键函数指向相应的 prototype 即可。注册流程可以这样写:
template <class T>
inline void register_type(lua_State* lua) {
if (ClazzMeta<T>::REGISTERED) return;
extract_methods<T>();
extract_getter_setter<T>();
int flag = register_prototype<T>(lua);
if (flag != 0) {
logf("Prototype register error: %s", lua_tostring(lua, -1));
return;
}
register_metatable<T>(lua);
if (flag != 0) {
logf("Metatable register error: %s", lua_tostring(lua, -1));
return;
}
ClazzMeta<T>::REGISTERED = true;
}
只要是注册过的类型,针对它的调用就很简单了:
Call C++ Functions from Lua
-- Here `mp` is actually a C++ `std::map`.
function map_query(mp, key)
print(getmetatable(mp))
for k, v in pairs(getmetatable(mp)) do
print(k, v)
end
print("[Lua] key: " .. key)
print('value of key " ' .. key .. ' " in map: ' .. mp:at(key))
end
Call Lua Functions from C++
template <class Ret, class... Arg>
int call(const char* lua_func_name, Ret& ret, Arg&&... arg) {
lua_getglobal(lua_, lua_func_name);
assert(lua_isfunction(lua_, -1));
// Push all arguments
(push(arg), ...);
// Count the number of arguments
constexpr int nargs = int(sizeof...(Arg));
// Count the number of return values.
constexpr int nresults = int(ret_helper<Ret>::count);
int flag = protected_call(nargs, nresults, 0);
ret_helper<Ret>::extract_res(this, ret);
return flag;
}