Lua C++ Binding之Lunar, Luna

时间:2021-01-02 15:35:03

服务端引擎的脚本, 我们项目在老端游项目上发展, 采取的是Lua脚本. 当前服务端的发展趋势是瘦引擎, 胖脚本模式, 基本上引擎负责的功能非常少, 主要是网络, 定帧, 定时器, 引擎通过导出相应的接口给脚本层调用, 数据基本上都是放在脚本层里, 引擎层只记录一些相关功能必须的数据, 如我们的引擎层里面记录了断线重连相关的验证信息, 玩家所在battle信息用于转发battle和client之间的通讯数据等. 瘦引擎胖脚本的好处是基础设施架设好之后, 可以很快的铺玩法, 而且由于脚本虚拟机的存在, 服务端代码的稳定性和健壮性有一定的保证, 缺点可能就是老大难的脚本执行效率问题.

我们的引擎入口是在C++, 也可以认为C++这一层次就是引擎层, 而Lua这一层是脚本层. 回到主题, 引擎层和脚本层需要能够*切换, 可以方便的从引擎层调用进入脚本层, 脚本层也需要方便的使用引擎层提供的数据对象和接口, 我们服务端采取的是一个很轻量级的binding方案, 就是lunar.h(http://lua-users.org/wiki/CppBindingWithLunar), 可以看到, 这是一个比较古老的方案, 支持Lua5.0, 5.1版本, 5.2, 5.3版本可能需要修改一些接口调用才能编译通过.

对于lunar.h, 它提供了以下功能:

1. 提供了定义机制, 可以在Lua层声明C++对象

2. 提供了push接口, 可以在C++层传入C++对象(唯一的userdata, 就是对同一对象多次调用push, 传入的是同一个userdata对象), 在Lua层可以调用该对象注册给Lua层调用的接口

3. 提供了call接口, 可以在C++层调用Lua层定义的扩展函数, 或者是调用对象的Lua函数接口(http://lua-users.org/wiki/CallingLuaFromCpp), 调用对象的Lua函数接口是指从C++层调用该对象注册给Lua层的接口, 从而达到不用重复实现函数的目的, 但是理论上来说效率会下降, 距离来说, 对象sugar有一个taste接口, 一个c_taste接口(注册给Lua层使用), 两者功能重复, call可以让你只实现c_taste接口, 当C++层需要调用这个接口的时候, 调用c_taste接口, 而不需要重复编写一个taste接口.

4. 对于对象的生命周期, Lunar的默认处理是对象由C++层来销毁, 但是对于在C++层创建的对象, 可以通过在push时设置gc参数为true, 将销毁的行为交由Lua层处理. 而对于在Lua层创建的C++对象, 一定是由C++层来进行销毁的, 这部分要特别特别注意, 我们的做法是这类对象要么是全局对象, 声明周期从创建到服务器关闭, 例如db对象, 要么是一个函数调用中使用, 不会被缓存下来放到别处使用, 而函数调用末进入引擎层接口后, 引擎层会回收这个对象, 如buffer对象, 底层发送完这个网络buffer之后就会delete, 而脚本层不会缓存住这个buffer对象, 之后在某个时刻再次使用(无法判断底层是否已经回收了这个对象), 算是潜规则.

对于Lunar, 其代码注释写的已经不错, 接口不多容易理解, 其实现代码翻译为Lua代码如下(以实现一个Sugar的C++类为例)

 methods = {
     new = new_T,
     func1 = thunk, --with upvalue func1 RegType
     func2 = thunk, --with upvalue func2 RegType
     -- ...
 }
 userdata_mt = {
     __metatable = methods,
     __index = methods,
     __tostring = tostring_T,
     __gc = gc_T,
     userdata = { __mode = "v", lightuserdata = fulluserdata, ... }
     ["do not trash"] = { __mode = "k", fulluserdata = true, ... }
 } -- 存在注册表当中
 mt = { __call = new_T }
 setmetatable( methods, mt )
 _G["Sugar"] = methods

Luna.h也是Lua C++ binding的一种方式, 最新的Luna版本是LunaFive(http://lua-users.org/wiki/LunaFive), LunaFive个人认为还未能达到Lunar的使用级别, 它具有一些Lunar没有的优点, 包括

1. 直接支持Lua5.3

2. 支持命名空间, Register接口的第二个参数, 默认为全局表

3. 提供了获取属性的接口函数, 而Lunar需要直接写set/get函数

4. 支持将C函数作为对象保存, 比如local func = obj.func, 之后func()是合法的, 而Lunar不能

相对于Lunar的缺点有:

1. gc行为是默认delete, 也就是说对象生命周期由Lua gc控制, 回收时机不确定

2. 没有考虑同一对象的多次push, 会产生多个userdata, Lunar只会产生一个

LunaFive的代码实现翻译为Lua代码如下, 利用了__index是function时会传入原始的table/userdata来获取C++对象指针, 通过index来对应记录成员变量和成员函数, 最多支持128个变量和128个函数.

 userdata_mt = {
     __gc = gc_obj,
     __tostring = to_string,
     __eq = equals,
     __index = property_getter,
     __newindex = property_setter,
     [,
     [,
     ...
     [ | (  <<  ),
     [ | (  <<  ),
     ...
 } -- 存在注册表当中
 _G["Sugar"] = constructor

我们项目使用的是Lunar, 虽然古老, 但是起码经受了端游项目和手游项目的考验, 虽然没有很花哨的功能, 需要遵守一些潜规则, 但是如果不是追求极致, Lunar这个解决方案应该也是够用了.

现在我在看一个新的解决方案, 是github上的一个开源项目lua-intf(https://github.com/SteveKChiu/lua-intf), 利用了一些C++11的新特性, 看起来蛮优雅的, 搜了一下似乎资料不多, 等细心研读之后有机会再分享.