tolua++中的类型系统

一、类型继承

  为了在Lua中访问C++对象,tolua++用了些方法将C++接口映射为Lua代码。Lua本身就可以存储调用一些C的对象,包括基本数据类型、C函数、指针等等。但是一个C++对象所拥有的东西要比这些复杂得多。其中最重要的就是C++的类信息。
  C++的类包括类的成员和方法,还有继承关系。Lua中为了进行OO编程,已经提供了metatable的机制,一个metatable中可以包含一些成员和方法,给一个Lua对象设置一个metatable以后,这个Lua对象就可以调用metatable中的成员和方法。这个metatable就好像是一个类型的定义,而调用该metatable的Lua对象就成为了该类型的一个实例。metatable本身也可以给它设置上另一个metatable,这样原来的metatable又可以调用新metatable中的成员和方法。形成了类的继承。
  tolua++中定义一个C++对象进入Lua系统后,便被转换为一个包含原C++对象指针的Lua Userdata,然后给这个Userdata设置上一个包含所有C++对象可以调用方法的metatable。使得该Userdata可以调用原C++对象的成员和方法。从而在Lua中提供操作一个C++对象的方法。
  此外tolua++中除了设置好和C++类型继承一致的metatable链,还提供了每个metatable对应的叫做tolua_super的表,里面会记录自己的父类链上的所有父类,用于查询一个Lua中的类型是否是另一个类型的子类。为什么需要这个东西呢?因为一个C++对象可能会被多次在Lua通过不同的接口获取,同一个对象从不同的接口中获取时,可能会显示为不同的类型,如获取Cocos2d-x中一个CCNode的子节点,用getChildByTag获取的是类型为CCNode的子节点,但是从getChildren接口获取的子节点类型为CCObject,而这个子节点在最初创建使用的时候可能其实是一个CCSprite的类型。就是说同一个对象在不同的地方可能会被识别为不同的类型。但是我们为了不重复创建对象而让同一个C++对象只会对应一个Lua Userdata,那么这个Userdata的metatable应该设置为上述的哪一个类型呢?实际上只能是继承链靠后的类型,在CCObject <= CCNode <= CCSprite的继承链中应为CCSprite,因为只有CCSprite类型能满足所有调用该C++对象的场景。所以每次C++对象被Lua获取的时候需要检查类型的继承关系,将Lua中的C++对象升级成更靠后的子类,所以设计了这个tolua_super表来完成这项功能。

userdata|metatable|tolua_super

userdata, metatable, tolua_super

二、tolua.cast

  在tolua++中有一个方法叫做tolua.cast,这个方法官方的实现,其效果相当于C++中的static_cast,强制将一个Lua Userdata的metatable设置为转换目标的类型。没有任何的安全检查,需要人力弄清要转换的目标是否是对象的实际类型或是父类的类型。我自己使用了一下发现在弱类型的Lua层面使用这个方法,很容易误判对象类型而转换出错,一旦出错程序就会崩溃得莫名奇妙并且还不容易发现问题。
  不过,在我认为在Lua中这个方法实际上还是很有用处的。如果它的功能能像C#中的as关键字,可以将任意类型转换为任意类型,如果目标不是实际类型的父类,我们就会得到null对象,有这样的类型检查就会好用得多。于是我便打算将tolua++中的这个static_cast升级为as关键字。要这样做最大的难点是如何知道一个任意的C++对象的实际类型,尤其是当C++对象进入Lua后我们只能通过对应的Userdata拿到一个具有一切可能的void*指针,以及进入Lua时这个C++对象被识别为的类型metatable。比如,实际是一个CCSprite的对象,当它还没有被当成CCSprite在Lua中使用时,我们可能只能在Lua中拿到一个void*和对应的名字为“CCObject”类型的metatable。急需解决的是怎么知道它其实是一个CCSprite。
  C++标准中本来就有识别类型信息的方法,名字叫typeid的关键字,然后我们可以通过它获取每个C++类唯一的hash_code。typeid对void*的指针无能为力,但是对CCObject*就可以起作用。如果object是CCObject*的话,我们就可以用typeid(*object).hash_code()获得object实际的类型的哈希值,用typeid(CCSprite).hash_code()获得CCSprite类的哈希值然后做一下比对就可以知道object实际类型是不是CCSprite了。好在Cocos2d-x中的重要的对象几乎都是继承自CCObject,不继承CCObject的主要为单例类和值对象的类,我们可以很容易对它们做特殊处理,不让单例类和值对象类在Lua中需要进行类型转换,并且在做转换时将其剔除。
  具体实现下来又发现了两个问题,一个是实际在Lua中并不一定需要知道真正的C++类型。原因是我们在C++中很容易为了扩展功能,通过类继承而不是组合的方式增加大量的功能类,如Cocos2d-x中的一系列动作类(CCAction),瞬时动作继承于CCFiniteTimeAction,有时间周期的动作继承于CCActionInterval,其它功能的类直接继承于CCAction。这些动作最主要只做两件事,一件是创建自己的动作对象,一件是交由CCNode来执行和控制。这些动作对象的控制管理接口都统一由CCNode来提供,所以区别它们是CCScale类还是CCMoveBy类,在Lua层面上意义并不大,所以我不再一一为它们都导出实际的C++类型,而只把它们导出为三种动作基类的类型,这样几十种动作类最终只用导出三种类便可以缩减大量的导出代码。但是这样做会又导致了用typeid获得的对象真实类型与对象在Lua中的实际类型有出入。

Lua对象在C++和Lua中类型不同

Lua对象在C++和Lua中类型不同/p>

  另一个问题是typeid().hash_code()的效率问题,在VS2013中我原以为这个hash_code是一个编译器计算的值,事实上是用typeid().name()获得的字符串(在VS2013中是包含命名空间,模板类型,类名称组合的一个字符串,长度会达到20~40个左右),然后实时对这个字符串做FNV hash(遍历每个字符做异或和乘法运算加总)得到hash_code()运算值,如果遍历一个CCNode节点树做类型转换的话,就会很容易产生大量不必要的运算。此外typeid()得到的其实是一个叫type_info的对象,name()是type_info的一个成员方法,经过尝试以后我发现在VS2013中跨动态链接库访问创建对象时,比如在另一个项目调用cocos2d.dll创建CCNode对象,&typeid(*node)与&typeid(CNode)得到的就是不同的对象地址,typeid(*node).name()与typeid(CCNode).name()得到的字符串地址也是不同。所以想优化一下直接利用type_info类对象的地址也是不行,果然是undefined behavior。
  于是我想了另外一个方法,干脆自己定制一个供Lua使用的高效的typeid系统。利用虚函数的话很容易实现。然后再利用模板函数来生成每个类型的类型信息。做法如下:

extern CC_DLL int g_luaType;

template <class T>
int CCLuaType()
{
	static int type = ++g_luaType;
	return type;
}

#define CC_LUA_TYPE(type) \
public: virtual int getLuaType() const \
{ \
	return CCLuaType<type>(); \
}

class CCObject
{
	...
	CC_LUA_TYPE(CCObject)
};

class CCNode: public CCObject
{
	...
	CC_LUA_TYPE(CCNode)// Lua get it`s type as CCNode
};

class CCPlace: public CCAction
{
	...
	CC_LUA_TYPE(CCAction)// Make Lua recognize it as CCAction
};

CCObject* obj = CCNode::create()
assert(obj->getLuaType() == CCLuaType<CCNode>());

obj = CCPlace::create()
assert (obj->getLuaType() == CCLuaType<CCAction>());

  因为Cocos2d的Win32项目生成目标是动态链接库,我们还有一点麻烦的工作就是还得显式声明每个Lua的导出类的模板特例化的函数,在cocos2d.h中增加以下代码:

...
template int CC_DLL CCLuaType<CCNode>();
template int CC_DLL CCLuaType<CCAction>();
...

  这样的话,获取每一个Lua导出类的实际类型就可以又准确又高效了。

标题目录