Lua 挺好用的,但实际上,我一接触它,首先面临的问题就是复杂的 C++ 与 Lua 间的交互问题,不解决这个,单纯用个 Lua 并没有什么意义。
这个绑定的问题是复杂的,从众多的开源库[1]中就能瞥见端倪。问题的复杂性在于不同语言间交互面临的类型与存储的管理。
C++ 与 Lua 间的绑定,目测有两个流派:手动派和自动派
手动派就是手写栈操作,自动派也至少分两种,其一是靠编译器插件,另一是靠 wrapper。
我选择的方案是最简单的 wrapper 方案,这相关的开源库就挺多。
选择了 wrapper 后,剩下要做的事情就是用这个 wrapper 把 C++ 类注册上,只要结合上
"全球最强" の 99 行 C++ 静态反射库,这并不困难。
结合两相之力,C++ 与 Lua 间的绑定在用户端就变成了 1 行 的事
代码
Ubpa/ULuaPPgithub.com1. Lua wrapper
awesome-cpp 上相关的库有 LuaBridge,Luacxx,sol2,SWIG(编译器)
我选择了其中“最潮”的 sol2 (v3),它文档十分丰富,开源工作搞得非常好。
但其实代码已经很脏了,作者面临各种 bug 已经感觉修不过来了。没办法,再怎么说这也是当前最好之一的库了。主要是来源于问题的复杂性吧。我看库里边真是无所不用其极,连 &/&& 成员函数都有,要处理 unique_ptr, shared_ptr, move, reference 等等麻烦问题。库的代码也来到了 25k 行,就解决这个 wrapper 的问题。问题实在是很复杂。
我还给他修了
不管怎样,用了再说
对于一个 C++ 的类
利用 sol2 注册到 Lua 中需要写如下代码
2. auto bind
在第 1 节中提到了在使用 sol2 时需要写一定的注册代码,我们看到,这个代码,就跟"全球最强" の 99 行 C++ 静态反射库 里的声明极其类似。对于 Point
类,静态反射的声明代码为
假设用户已经声明了静态反射,那么在使用 sol2 时,就可以利用这个静态信息自动生成 sol2 注册代码,此时用户只需写 1 行代码。
这行代码里干了什么呢?
- 获取构造函数(名字为
"constructor"
的 field)组装成sol::initializers
- 将所有函数按名字分组,然后各组分别注册(多函数组注册成
sol::overload
,但函数就简单注册)。如果函数名是operator+
,operator-
等特殊函数,则注册成sol::meta_function::*
- 将所有变量注册
另外,如 "全球最强" の 99 行 C++ 静态反射库 所述,域还有属性列表,我这里还会将属性注册上,对于重载函数,由于域同名,在注册时会自动加上必要的索引_0
,_1
以区分不同的域的属性列表
这其中涉及非常繁重的元编程(复杂度
constexpr
要做好多的工作,最后只需用下边的函数就能获取到一个类的各重载函数组(这里省略了其调用函数的细节)
结果确实是 constexpr
的
USRefl
库确实用起来挺顺手的,就是 C++ 语言的能力还不够,导致部分内容写法上需要绕。现在体会到了 C++20 的好处了,特别是类常量模板参数和 template lambda,可惜我目前用的 MSVC 只有鸡肋的/std:c++latst
。再等等吧,终究是错付了。
3. 示例
下边举一个复杂的例子,包含了上边所讲的所有特性
类
涉及:构造函数(重载),变量,成员函数重载,特殊成员函数(
operator-
),类属性,变量属性,重载函数属性
静态反射声明
接着用 sol2 注册到 Lua 中
然后在 Lua 里跑下边的代码
结果如下
完美!
4. 结语
至此,C++ 绑定到 Lua 后,使得我们后续的开发可以直接在 lua 上进行。
这项工作将用于我的引擎之中,供给 UECS 使用。
脚本真香!
参考
- ^http://lua-users.org/wiki/BindingCodeToLua