Contents

基于反射在 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 的特性就够了)。下面介绍一下最基本的概念。

  1. 数据类型:Lua 除基本数据类型之外,最核心的数据类型是 table,存储 KV 对。此外,Lua 还提供了 metatable机制,可对特定的某个 table 重写根据 K 查找 V 的函数,从而给很多复杂的修改提供了基础。
  2. Lua 默认的变量(无 local 修饰)都是全局变量,使用 local 修饰的函数只在当前作用域可见。
  3. Lua 是一个基于栈的语言。函数参数和返回值都是通过栈传递的。Lua 的栈可以用整数指标去访问栈内的任何一个元素。正整数指标表示入栈顺序,例如 1 表示第一个入栈(位于栈底),2 表示第二个入栈(栈底更高一个)等等;负整数指标表示刚刚(即将)出栈的顺序,-1 表示下一次弹栈会弹出的元素(位于栈顶),-2 表示再下一次弹栈会弹出的元素(栈顶更低一个)。在使用 Lua 本身编程时不用关心这一点,但是在使用 C API 的时候需要手动维护。
  4. C/C++ 调用 Lua 函数 f(a,b) 大概是这个流程:
    1. 根据函数名称 f 查变量表,将查找结果(实际上就是函数指针)压栈
    2. a 压栈
    3. b 压栈
    4. 使用 API 触发函数执行,函数内部依照约定维护调用栈,参数都会被清空,然后返回值被压栈
    5. 弹栈,获取返回值
  5. Lua 调用 C++ 函数大概是这个流程:
    1. 在 C/C++ 定义一个函数,签名必须是 int f(lua_State*),该函数需自行满足上述的栈上调用约定,C/C++ 的返回值表示在 Lua 中拿到的返回值个数
    2. 把该函数及其名字绑定到某个 table 中或者全局作用域

C++ API Wrapper

栈的维护

Lua 的 C API 对不同的数据类型是使用了不同的函数,例如将某个变量压栈,空指针、数字、字符串分别需要使用 lua_pushnillua_pushnumerlua_pushstring 来执行。如果我们使用 C++,其实可以对这些东西再包装一层,封装出一个统一的 pushpop 函数方便使用。这既可以用函数重载来做到,也可以用模板。我这里使用模板,方便后续的操作。

自定义 class 绑定

如果是要把 C++ 的自定义类压如 Lua 的栈,有这样两种思路:

  1. 把整个数据结构复制一次,写成一个大的 Lua table,然后把整个 table 压栈
  2. 只传递一个指针,然后通过重写 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;
}