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
、.set
和 index|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_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. 函数存储 函数键值由 id
、class
和 tag
组成:
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_getcallback
和 olua_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)
obj
:c++
对象。
fidx
:回调函数在 lua
栈上的位置。
tag
:用标识函数的标签。
tagmode
:存储时,标签的匹配模式。
func
:存储在 uservalue
中的函数的键值。
argc
:回调函数的参数个数。
3.4. 使用示例 olua 自动导出工具生成代码: static int _cclua_timer_delay(lua_State *L){ float arg1 = 0 ; std::function<void ()> arg2; 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); } }; cclua::timer::delay (arg1, arg2); return 0 ; } #define makeTimerDelayTag(tag) ("delayTag." + tag) static int _cclua_timer_killDelay(lua_State *L){ std::string arg1; 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); 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
指定存储方式,如果 obj
是 table
,还需添加 | OLUA_FLAG_TABLE
4.4. 使用示例 自动导出工具生成代码: static int _cocos2d_Node_addChild(lua_State *L){ cocos2d::Node *self = nullptr ; cocos2d::Node *arg1 = nullptr ; olua_to_object (L, 1 , &self, "cc.Node" ); olua_check_object (L, 2 , &arg1, "cc.Node" ); self->addChild (arg1); 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 ; olua_to_object (L, 1 , &self, "cc.Node" ); olua_check_object (L, 2 , &arg1, "cc.Node" ); self->removeChild (arg1, arg2); olua_delref (L, 1 , "children" , 2 , OLUA_FLAG_MULTIPLE); return 0 ; }
5. 临时对象池 在游戏引擎中,每一帧都可能会产生用户输入事件 Touch
,这些对象仅仅使用一次就可能不需要了,如果每一次都创建和销毁,显得有些得不偿失。而且有些实现中,这些 Touch
可能还是栈变量。为了优化和解决这些问题,olua
使用了可选对象池的设计。下面代码中,需要新创建的 Touch
和 Event
对象都将使用对象池。
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_newobjstub
和 olua_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_toobj
和 olua_checkobj
区别在于,check
会检查对象是指定的类。这两种函数都会返回非空对象,如果对象是 NULL
,则抛出 lua error
。olua_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_$$_vector
和 olua_$$_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
对象那样,可以自动生成代码,比如 int
、float
等,这时候,我们可以使用 olua::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::pointer<bool > olua_bool;typedef olua::pointer<int8_t > olua_int8_t ;typedef olua::pointer<uint8_t > olua_uint8_t ;typedef olua::pointer<int16_t > olua_int16_t ;typedef olua::pointer<uint16_t > olua_uint16_t ;typedef olua::pointer<int32_t > olua_int32_t ;typedef olua::pointer<uint32_t > olua_uint32_t ;typedef olua::pointer<int64_t > olua_int64_t ;typedef olua::pointer<uint64_t > olua_uint64_t ;typedef olua::pointer<olua_char_t > olua_char;typedef olua::pointer<olua_short_t > olua_short;typedef olua::pointer<olua_int_t > olua_int;typedef olua::pointer<olua_long_t > olua_long;typedef olua::pointer<olua_llong_t > olua_llong;typedef olua::pointer<olua_uchar_t > olua_uchar;typedef olua::pointer<olua_ushort_t > olua_ushort;typedef olua::pointer<olua_uint_t > olua_uint;typedef olua::pointer<olua_ulong_t > olua_ulong;typedef olua::pointer<olua_ullong_t > olua_ullong;typedef olua::pointer<olua_float_t > olua_float;typedef olua::pointer<olua_double_t > olua_double;typedef olua::pointer<olua_ldouble_t > olua_ldouble;typedef olua::pointer<size_t > olua_size_t ;typedef olua::pointer<ssize_t > olua_ssize_t ;typedef olua::pointer<std::string> olua_string;
同时我们在 lua-types.lua
中,关联了类型的其它表现形式:
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) ;
默认情况之下,olua
把 c++
与 lua
之间的类关联信息存储在 lua registry
表中。你可以自定义这些信息的存储位置,以加快信息的获取。
9.3. 参考实现 luauser.h olua-2dx.h olua-2dx.cpp