olua 设计与实现

Cocos2dx 原生的 lua 绑定,c++ 对象与 lua 对象的生命周期不一致,c++ 对象基于引用计数,而 lua 对象基于 lua GC,生命周期不一致,容易产生各种不可遇知的行为。所以,我想重新设计一个绑定,使用得 c++ 对象与 lua 对象生命周期一致。起初考虑过在 tolua 的基础上实现,在仔细阅读源码之后,发现如果要实现这一目标,需要修改大量的源码,于是自己重新设计了一个绑定 olua

olua 设计之初,就定下了以 c++ 和 lua 对象生命周期一致为核心原则,所以在诸多接口的设计上都以此为目标。本文将阐述 olua 的核心设计要素与部分实现参考。

如果想了解 olua 导出工具的使用说明,请参见:

1. 类模型

lua class model
class A = {
class = class A
classname = A
classtype = native
super = class B
.classobj = userdata -- store static callback
.isa = {
copy(B['.isa'])
A = true
}
.func = {
__index = B['.func'] -- cache after access
dosomething = func
...
}
.get = {
__index = B['.get'] -- cache after access
name = get_name(obj, name)
...
}
.set = {
__index = B['.set'] -- cache after access
name = set_name(obj, name, value)
...
}
...__gc = cls_metamethod -- .func[..._gc]
}

1.1. 变量 class

class 指向的是 class,是此类所有对象的元表,为 c/c++ 对象提供 lua 接口。

  • 访问类静态变量,比如 Object.classname
    local Object = require "Object"
    print(Object.classname)
    print(Object.class)
  • 在 lua 层扩充方法或属性:
    local Object = require "Object"
    // print 方法将存在 .func 变量中
    function Object:print(value)
    print(Object.classname, value)
    end

1.2. 变量 super

super 指向父类。

1.3. 变量 .classobj

.classobj 是一个特殊 userdata 对象。因为在 olua 设计中,所有 lua 回调函数都存储在 userdata 中,由于静态函数回调没有明确的所属权,所以此类回调都存储在 .classobj 中。

1.4. 变量 .isa

.isa 以类名为键,存储此类及所有父类的类名,方便快速判断指定对象是否为指定的类,用于 olua_isa 函数中。

1.5. 变量 .get

.getter 存储 getter 函数和 const 变量获取函数 cls_const

1.6. 变量 .set

.setter 存储 setter 函数和禁止赋值函数 cls_readonly

1.7. 变量 .func

.setter 存储普通函数和元方法代理函数 cls_metamethod

1.8. 元方法代理

olua 的元方法代理 cls_metamethod 是用来访问自定义元函数,元函数可以通过 oluacls_func 设置。olua 默认提供了 __eq__tostring 实现,除 __gc 外,若使用未提供的元函数,将会抛出 lua 错误。

1.9. __index 与 __newindex

__index__newindex 是用于实现访问 c/c++ 对象接口,所以在实现上与上述元方法代理有所不同,__index__newindex 也支持自定义函数,只不过只有在未找到任何属性、函数和常量时才会触发,相当于最后的选择。
__index__newindex 是闭包函数,各拥有五个闭包值:.isa.func.get.setindex|newindex

__index 函数查找顺序:

  • 如果存在 getter = .getter[k],执行 getter()
  • 如果存在 value = .func[k],则返回 value
  • 如果存在 func = .func[__index],则执行 func(t, k)
  • 如果对象是 userdata,则返回 userdata[k]
  • 其它情况返回 nil

__newindex 函数执行顺序:

  • 如果存在 setter = .setter[k],执行 setter(v)
  • 如果对象是 table,则 .func[k] = v
  • 如果存在 func = .func[__newindex],则执行 func(t, k, v)
  • 如果对象是 userdata,则 userdata[k] = v
  • 其它情况不处理。

2. 类定义与注册

2.1. 定义类

int luaopen_cclua_Object(lua_State *L)
{
oluacls_class<cclua::Object>(L, "cclua.Object");
//oluacls_class<cclua::Object, SuperCls>(L, "cclua.Object");
oluacls_func(L, "__gc", _gc);
oluacls_func(L, "new", _new);
oluacls_func(L, "print", _print);
oluacls_prop(L, "name", _name_get, _name_set);
oluacls_prop(L, "id", _id_get, NULL);
oluacls_const(L, "BOOL", false);
oluacls_const(L, "NUM", 1.2f);
oluacls_const(L, "INT", 3);
oluacls_const(L, "STRING", "click");

return 1;
}

通常情况下,我们会按照上面的方式导出 c++ 接口。

2.2. 注册类

olua_require(L, "cclua.Object", luaopen_cclua_Object);

olua_require 仅仅是完成类的创建,并不向全局暴露类的信息,所以不能通过 cclua.Object 方式使用。

2.3. lua 层使用

local Object = require "cclua.Object"
local obj = Object.new()
obj:print()
obj.name = 'hello olua'
print(obj.id)
print(Object.BOOL)

3. 回调函数

olua 设计中,所有的 lua 回调函数都存储在 userdata.uservalue 中,在 lua5.1 中,使用 env 代替 uservalue。之所有采用这种方式,是因为可以让函数生命周期与对象的生命周期一致,相当于让对象管理函数的生命周期。

3.1. 函数存储

函数键值由 idclasstag 组成:

  • id 用于保证使用相同 tag 的不同函数可以并存。
  • class 用于标识函数属于哪个类对象,主要用途是调试。
  • tag 任意字符串,标识函数行为。
    obj.uservalue {
    |---id---|--class--|--tag--|
    .olua.cb#1$classname@onClick = lua_func
    .olua.cb#2$classname@onClick = lua_func
    .olua.cb#3$classname@update = lua_func
    .olua.cb#4$classname@onRemoved = lua_func
    ...
    }

3.2. 标签匹配模式

在调用回调函数接口时,可以指定标签匹配模式,以准确实现意图,每种匹配模式有指定的函数使用范围。

适用于 olua_setcallback

  • OLUA_TAG_NEW 直接创建新键存储新的函数。
  • OLUA_TAG_REPLACE 匹配 tag,如果存在,可以替换,否则创建新的。

适用于 olua_getcallbackolua_removecallback

  • OLUA_TAG_WHOLE 匹配整个键。
  • OLUA_TAG_EQUAL 匹配 tag
  • OLUA_TAG_STARTWITH 匹配开头部分 tag

3.3. 回调接口

olua.h
const char *olua_setcallback(lua_State *L, void *obj, int fidx, const char *tag, int tagmode)
int olua_getcallback(lua_State *L, void *obj, const char *tag, int tagmode)
void olua_removecallback(lua_State *L, void *obj, const char *tag, int tagmode)
int olua_callback(lua_State *L, void *obj, const char *func, int argc)
  • objc++ 对象。
  • fidx:回调函数在 lua 栈上的位置。
  • tag:用标识函数的标签。
  • tagmode:存储时,标签的匹配模式。
  • func:存储在 uservalue 中的函数的键值。
  • argc:回调函数的参数个数。

3.4. 使用示例

olua 自动导出工具生成代码:
static int _cclua_timer_delay(lua_State *L)
{
float arg1 = 0; /** time */
std::function<void()> arg2; /** callback */
olua_check_number(L, 1, &arg1);
olua_check_callback(L, 2, &arg2);

void *cb_store = (void *)olua_pushclassobj(L, "cclua.timer");
std::string cb_tag = "delay";
std::string cb_name = olua_setcallback(L, cb_store, 2, cb_tag.c_str(), OLUA_TAG_NEW);
olua_Context cb_ctx = olua_context(L);
arg2 = [cb_store, cb_name, cb_ctx]() {
lua_State *L = olua_mainthread(NULL);
if (olua_contextequal(L, cb_ctx)) {
int top = lua_gettop(L);
olua_callback(L, cb_store, cb_name.c_str(), 0);
olua_removecallback(L, cb_store, cb_name.c_str(), OLUA_TAG_WHOLE);
lua_settop(L, top);
}
};
// static void delay(float time, @local const std::function<void ()> callback)
cclua::timer::delay(arg1, arg2);
return 0;
}

#define makeTimerDelayTag(tag) ("delayTag." + tag)

static int _cclua_timer_killDelay(lua_State *L)
{
std::string arg1; /** tag */
olua_check_tring(L, 1, &arg1);
std::string cb_tag = makeTimerDelayTag(arg1);
void *cb_store = (void *)olua_pushclassobj(L, "cclua.timer");
olua_removecallback(L, cb_store, cb_tag.c_str(), OLUA_TAG_EQUAL);
// static void killDelay(const std::string &tag)
cclua::timer::killDelay(arg1);
return 0;
}

4. 引用链

olua 引入引用链机制是为解决回调函数可能失效的问题,此举会增加导出者负担,但减少使用者负担,所幸大部分代码都可以使用 olua 导出工具自动生成。

4.1. 引入原由

如下示例,因为回调函数属于 act 管理,如果 act 没有被任何对象持有,在 GC 阶段会被回收,导致回调函数也被回收,最终行为与预期不一致。引用链的核心就是让 obj 持有 act,就像 act 持有回调函数一样。

local Object = require "cclua.Object"
local Action = require "cclua.Action"
local obj = Object.new()
local act = Action.new(function ()
print('hello action')
end)
obj:run(act)

4.2. 引用存储

obj.uservalue {
| prefix |- name -|
.olua.ref.component = obj_component -- OLUA_FLAG_SINGLE
.olua.ref.children = { -- OLUA_FLAG_MULTIPLE
obj_child1 = true
obj_child2 = true
...
}
}

与回调函数一样,都存储在 uservalue 中,引用存储有两种模式,一是独立存在,二是共存,所以调用接口时,要指定存储的方式。

4.3. 引用接口

typedef bool (*olua_RefVisitor)(lua_State *L, int idx);
int olua_loadref(lua_State *L, int idx, const char *name);
void olua_addref(lua_State *L, int idx, const char *name, int obj, int flags)
void olua_delref(lua_State *L, int idx, const char *name, int obj, int flags)
void olua_delallrefs(lua_State *L, int idx, const char *name)
void olua_visitrefs(lua_State *L, int idx, const char *name, olua_RefVisitor walk)
  • idx 引用持有对象的位置。
  • name 引用名称。
  • obj 需要被持有的对象的位置,可以为 userdata 或者 table
  • flags 指定存储方式,如果 objtable,还需添加 | OLUA_FLAG_TABLE

4.4. 使用示例

自动导出工具生成代码:
static int _cocos2d_Node_addChild(lua_State *L)
{
cocos2d::Node *self = nullptr;
cocos2d::Node *arg1 = nullptr; /** child */
olua_to_object(L, 1, &self, "cc.Node");
olua_check_object(L, 2, &arg1, "cc.Node");
// void addChild(@addref(children |) cocos2d::Node *child)
self->addChild(arg1);
// insert code after call
olua_addref(L, 1, "children", 2, OLUA_FLAG_MULTIPLE);
return 0;
}

static int _cocos2d_Node_removeChild(lua_State *L)
{
cocos2d::Node *self = nullptr;
cocos2d::Node *arg1 = nullptr; /** child */
olua_to_object(L, 1, &self, "cc.Node");
olua_check_object(L, 2, &arg1, "cc.Node");
// void removeChild(@delref(children |) cocos2d::Node *child)
self->removeChild(arg1, arg2);
// insert code after call
olua_delref(L, 1, "children", 2, OLUA_FLAG_MULTIPLE);
return 0;
}

5. 临时对象池

在游戏引擎中,每一帧都可能会产生用户输入事件 Touch,这些对象仅仅使用一次就可能不需要了,如果每一次都创建和销毁,显得有些得不偿失。而且有些实现中,这些 Touch 可能还是栈变量。为了优化和解决这些问题,olua 使用了可选对象池的设计。下面代码中,需要新创建的 TouchEvent 对象都将使用对象池。

size_t last = olua_push_objpool(L);
olua_enable_objpool(L);
olua_push_object(L, arg1, "cc.Touch");
olua_push_object(L, arg2, "cc.Event");
olua_disable_objpool(L);
olua_callback(L, cb_store, cb_name.c_str(), 2);
olua_pop_objpool(L, last);
  • olua_push_objpool 获取对象池当前可用开始位置。
  • olua_enable_objpool 开启对象池功能。
  • olua_disable_objpool 关闭对象池功能。
  • olua_pop_objpool 清理此次用到的对象并还原可用开始位置。

6. 对象操作

6.1. 创建对象

创建一个 lua 对象:

olua_pushobj(L, cppobj, "cclua.Object");
olua_pushobj<cclua::Object>(L, cppobj);
olua_push_object(L, cppobj, "cclua.Object");

olua_postpush<>olua_push_object 会在内部调用 c 函数 olua_postpush 之后,把状态作为参数,调用 olua_postpush 函数,使得我们有机会处理其它事情。

#ifdef OLUA_HAVE_POSTPUSH
template <typename T>
void olua_postpush(lua_State *L, T* obj, int status)
{
if (std::is_base_of<cocos2d::Ref, T>::value &&
(status == OLUA_OBJ_NEW || status == OLUA_OBJ_UPDATE)) {
((cocos2d::Ref *)obj)->retain();
#ifdef COCOS2D_DEBUG
if (!olua_isa<cocos2d::Ref>(L, -1)) {
luaL_error(L, "class '%s' not inherit from 'cc.Ref'", olua_getluatype(L, obj, ""));
}
#endif
}
}
#endif

olua_pushobj<> 主要于手写绑定代码,olua_push_object 是自动导出工具的标准转换接口。

6.2. 创建对象桩

如果以 std::function 作为参数才能创建对象,以目前的方式会遇到问题。因为回调函数要存储在 userdata 中,userdata 需要 c++ 对象才能创建,这就形成一个循环依赖的问题。为此,olua 引入另外两个接口来处理此问题:olua_newobjstubolua_pushobjstub

整个过程分为三步走:

  • 首先,使用 olua_newobjstub 创建一个 userdata 对象。
  • 其次,创建回调函数,并把 lua 回调存入此 userdata 对象中。
  • 最后,创建 c++ 对象,并使用 olua_pushobjstub 对象与此前的 userdata 对象关联。
    自动导出工具生成代码:
    void *cb_store = (void *)olua_newobjstub(L, "cc.Object");
    std::string cb_tag = "tween";
    std::string cb_name = olua_setcallback(L, cb_store, 1, cb_tag.c_str(), OLUA_TAG_NEW);
    olua_Context cb_ctx = olua_context(L);
    arg1 = [cb_store, cb_name, cb_ctx]() {
    ...
    };
    cclua::Object *obj = cclua::Object::create(arg1);
    olua_pushobjstub(L, obj, cb_store, cls);

6.3. 获取对象

获取 c++ 对象:

void *obj = olua_checkobj(L, 1, "cclua.Object");
void *obj = olua_toobj(L, 1, "cclua.Object");
auto obj = olua_checkobj<cclua::Object>(L, 1);
auto obj = olua_toobj<cclua::Object>(L, 1);
olua_to_object(L, 1, &obj, "cclua.Object");
olua_check_object(L, 1, &obj, "cclua.Object");

olua_toobjolua_checkobj 区别在于,check 会检查对象是指定的类。这两种函数都会返回非空对象,如果对象是 NULL,则抛出 lua errorolua_toobj 一般用于获取当前对象,而 olua_checkobj 用于获取参数。

olua_checkobj<> 主要于手写绑定代码,olua_check_object 是自动导出工具的标准转换接口。

6.4. 对象判定

判定对象是不是指定的类:

olua_isa(L, 2, "cclua.Object");
olua_isa<cclua::Object>(L, 2);
olua_is_object(L, 2, "cclua.Object");

olua_isa<> 主要于手写绑定代码,olua_is_object 是自动导出工具的标准转换接口。

7. 对象转换接口规范

7.1. 接口规范

为了方便自动导出工具生成正确代码,转换接口必须满足于这种形式:olua_$$_type

$$ 可取值:

  • is 用于判断是不是指定的类型。
  • to 不检查类型的转换。
  • check 检查类型的转换。
  • pack 把指定位置开始的参数,打包成一个对象:
    // lua代码
    obj:setPosition(x, y)

    // c++代码
    Point p;
    olua_pack_Point(L, 2, &p);
    obj->setPosition(p);
  • unpack 把对象展开,并返回展开的个数:
    // c++代码:
    Point p = obj->getPosition();
    int num = olua_unpack_Point(L, &p);
    return num;

    // lua代码
    local x, y = obj:getPosition()
  • canpack 判断从指定位置开始的参数,是否满足打包为一个对象。

以上的 5 种接口不必部分提供,可以使用编译不报错就不提供的原则。

7.2. 内置转换接口

olua 提供了常用的转换接口:

  • olua_$$_bool
  • olua_$$_string
  • olua_$$_integer
  • olua_$$_number
  • olua_$$_enum
  • olua_$$_object
  • olua_$$_vector
  • olua_$$_map
  • olua_$$_callback

7.3. 模版类型转换接口

对于模版类容器的转换接口,核心在于添加迭代,所以必须提供以下函数的重载版本:

template <class K, class V, template<class ...> class Map, class ...Ts>
void olua_insert_map(Map<K, V, Ts...> &map, const K &key, const V &value)
{
map.insert(std::make_pair(key, value));
}

template <class K, class V, template<class ...> class Map, class ...Ts>
void olua_foreach_map(const Map<K, V, Ts...> &map, const std::function<void(K &, V &)> &callback)
{
for (auto itor : map) {
callback(const_cast<K &>(itor.first), itor.second);
}
}

template <class T>
void olua_insert_vector(std::vector<T> &array, const T &value)
{
array.push_back(value);
}

template <class T>
void olua_insert_vector(std::set<T> &array, const T &value)
{
array.insert(value);
}

template <class T, template<class ...> class Array, class ...Ts>
void olua_foreach_vector(const Array<T, Ts...> &array, const std::function<void(T &)> &callback)
{
for (auto &itor : array) {
callback(const_cast<T &>(itor));
}
}

有了以上函数,我们就能够很方便使用 olua 已经提供的 olua_$$_vectorolua_$$_map 函数:

模版类型容器使用:

const std::unordered_map<std::string, cclua::Object *> &children = obj->getChildren();
olua_push_map<std::string, cclua::Object *>(L, children, [L](std::string &name, cclua::Object *child) {
olua_push_string(L, name);
olua_push_object(L, child, "cclua.Object");
});

8. 基础类型指针

一些时候,我们需要在 API 中使用指针,但是这个类型并不像 class 对象那样,可以自动生成代码,比如 intfloat 等,这时候,我们可以使用 olua::arrayolua::pointer 来定义这些类型的指针版本。
olua 内置定义了基础类型的指针版本:

typedef char olua_char_t;
typedef short olua_short_t;
typedef int olua_int_t;
typedef long olua_long_t;
typedef long long olua_llong_t;
typedef unsigned char olua_uchar_t;
typedef unsigned short olua_ushort_t;
typedef unsigned int olua_uint_t;
typedef unsigned long olua_ulong_t;
typedef unsigned long long olua_ullong_t;
typedef float olua_float_t;
typedef double olua_double_t;
typedef long double olua_ldouble_t;

typedef olua::array<bool> olua_bool;
typedef olua::array<int8_t> olua_int8_t;
typedef olua::array<uint8_t> olua_uint8_t;
typedef olua::array<int16_t> olua_int16_t;
typedef olua::array<uint16_t> olua_uint16_t;
typedef olua::array<int32_t> olua_int32_t;
typedef olua::array<uint32_t> olua_uint32_t;
typedef olua::array<int64_t> olua_int64_t;
typedef olua::array<uint64_t> olua_uint64_t;
typedef olua::array<olua_char_t> olua_char;
typedef olua::array<olua_short_t> olua_short;
typedef olua::array<olua_int_t> olua_int;
typedef olua::array<olua_long_t> olua_long;
typedef olua::array<olua_llong_t> olua_llong;
typedef olua::array<olua_uchar_t> olua_uchar;
typedef olua::array<olua_ushort_t> olua_ushort;
typedef olua::array<olua_uint_t> olua_uint;
typedef olua::array<olua_ulong_t> olua_ulong;
typedef olua::array<olua_ullong_t> olua_ullong;
typedef olua::array<olua_float_t> olua_float;
typedef olua::array<olua_double_t> olua_double;
typedef olua::array<olua_ldouble_t> olua_ldouble;
typedef olua::array<size_t> olua_size_t;
typedef olua::array<ssize_t> olua_ssize_t;
typedef olua::pointer<std::string> olua_string;

同时我们在 lua-types.lua 中,关联了类型的其它表现形式:

-- olua_int_t 会自动生成 typedef 信息
typedef 'signed *;int *'
.luacls 'olua.int'
.conv 'olua_$$_array'

有了以上的功能的支持,void read(int *t) 就可以正常生成代码了:

static int _Object_read(lua_State *L)
{
cclua::Object *self = nullptr;
int *arg1 = nullptr;
olua_to_object(L, 1, &self, "cclua.Object");
olua_check_array(L, 2, &arg1, "olua.int");
self->read(arg1);
return 0;
}
local Object = require "cclua.Object"
local int = require "olua.int"
local obj = Object.new()
local n = int.new()
obj:read(n)
print(n.value)

9. 自定义

olua.h 文件,会检测 OLUA_USER_H,如果有定义则 #include OLUA_USER_H,所以我们可以定制一个头文件 luauser.h,并且添加编译参数 OLUA_USER_H=\"luauser.h\"

9.1. 声明模版函数

在这个文件中,可以事先申明一些模版函数:

luauser.h
template <class T>
void olua_insert_vector(example::vector<T> &array, const T &value);

9.2. 自定义函数

还可以定义一些宏,表明自己会提供哪些函数:

9.2.1. 获取 lua 主线程
#define OLUA_HAVE_MAINTHREAD
lua_State *olua_mainthread(lua_State *L)

在执行回调之时,需要获取 lua vm 以执行 lua 回调函数。

9.2.2. 检测 lua 主线程
#define OLUA_HAVE_CHECKHOSTTHREAD
void olua_checkhostthread();

检查当前线程是否是 lua vm 所在线程,避免在执行回调之后产生不可预知的行为。

9.2.3. 比较移除引用
#define OLUA_HAVE_CMPREF
void olua_startcmpref(lua_State *L, int idx, const char *refname);
void olua_endcmpref(lua_State *L, int idx, const char *refname);

有些函数如 Object::removeChildren() 并未提供足够的信息来移除此前添加的引用,所以可以在调用 removeChildren,之前调用 olua_startcmpref 记录一些信息,在调用之后调用 olua_endcmpref 移除引用。

9.2.4. 追踪调用栈
#define OLUA_HAVE_TRACEINVOKING
void olua_startinvoke(lua_State *L);
void olua_endinvoke(lua_State *L);

olua 自动导出工具会在导出的函数开头插入 olua_startinvoke(L),在每一个返回位置插入 olua_endinvoke(L)。目的是为了在发生 lua error 之时,可以准确知道是哪个 lua thread 发生了错误。

9.2.5. 处理 push 状态
#define OLUA_HAVE_POSTPUSH
template <typename T> void olua_postpush(lua_State *L, T* obj, int status)

Push 对象之后,会调用此函数,你可以根据状态做额外的事情。

9.2.6. 处理 new 状态
#define OLUA_HAVE_POSTNEW
template <typename T> void olua_postnew(lua_State *L, T *obj)

在自动导出代码的 _Object_new 函数中,使用 new Object() 创建对象之后会调用此函数。

9.2.7. 对象销毁
#define OLUA_HAVE_POSTGC
template <typename T> void olua_postgc(lua_State *L, int idx);

提供对象销毁的自定义行为。

9.2.8. 类型注册与获取
#define OLUA_HAVE_LUATYPE
void olua_registerluatype(lua_State *L, const char *type, const char *cls);
const char *olua_getluatype(lua_State *L, const char *type);

默认情况之下,oluac++lua 之间的类关联信息存储在 lua registry 表中。你可以自定义这些信息的存储位置,以加快信息的获取。

9.3. 参考实现

luauser.h
olua-2dx.h
olua-2dx.cpp