tolua源码分析(五)lua使用C#的enum
上一节我们讨论了C#类是如何注册到lua的过程,以及lua调用C#函数时底层所做的事情。在此基础之上,本节我们来看看C#的enum是如何注册到lua的,它和一般类的注册有哪些区别。
老规矩,还是看官方提供的例子,这次是examples 10,代码如下:
string script =
@"
space = nil
function TestEnum(e)
print('Enum is:'..tostring(e))
if space:ToInt() == 0 then
print('enum ToInt() is ok')
end
if not space:Equals(0) then
print('enum compare int is ok')
end
if space == e then
print('enum compare enum is ok')
end
local s = UnityEngine.Space.IntToEnum(0)
if space == s then
print('IntToEnum change type is ok')
end
end
";
LuaState state = null;
new LuaResLoader();
state = new LuaState();
state.Start();
LuaBinder.Bind(state);
state.DoString(script);
state["space"] = Space.World;
LuaFunction func = state.GetFunction("TestEnum");
func.BeginPCall();
func.Push(Space.World);
func.PCall();
func.EndPCall();
func.Dispose();
func = null;
例子很简单,就是把C#的UnityEngine.Space
类型的enum传给了lua,并在lua层测试了tostring,ToInt,Equals等接口,验证了在lua层可以对enum判等,以及将一个int转换为enum,或者将enum转换为int等操作,输出结果如下:
首先我们来看下lua层是如何表示C#的enum。例子的第36行和第40行都是将enum push到lua层,相关代码如下:
public object GetEnumObj(Enum e)
{
object o = null;
if (!enumMap.TryGetValue(e, out o))
{
o = e;
enumMap.Add(e, o);
}
return o;
}
public void Push(Enum e)
{
if (e == null)
{
LuaPushNil();
}
else
{
object o = GetEnumObj(e);
PushUserData(o, EnumMetatable);
}
}
由于enum是值类型,C#层使用了enumMap
来缓存装箱后的object与enum的映射关系。其他的操作与普通类是相似的,EnumMetatable
就是注册到lua层的Enum类reference。
接下来我们来看下C#的enum注册到lua的方法,可以在System_EnumWrap.Register
方法中一览:
public static void Register(LuaState L)
{
L.BeginClass(typeof(System.Enum), null);
L.RegFunction("GetTypeCode", GetTypeCode);
L.RegFunction("GetValues", GetValues);
L.RegFunction("GetNames", GetNames);
L.RegFunction("GetName", GetName);
L.RegFunction("IsDefined", IsDefined);
L.RegFunction("GetUnderlyingType", GetUnderlyingType);
L.RegFunction("CompareTo", CompareTo);
L.RegFunction("ToString", ToString);
L.RegFunction("Equals", Equals);
L.RegFunction("GetHashCode", GetHashCode);
L.RegFunction("Format", Format);
L.RegFunction("Parse", Parse);
L.RegFunction("ToObject", ToObject);
L.RegFunction("ToInt", ToInt);
L.RegFunction("__tostring", ToLua.op_ToString);
L.EndClass();
}
我们依次看例子中使用到的C#方法,首先是__tostring
:
public static int op_ToString(IntPtr L)
{
object obj = ToLua.ToObject(L, 1);
if (obj != null)
{
LuaDLL.lua_pushstring(L, obj.ToString());
}
else
{
LuaDLL.lua_pushnil(L);
}
return 1;
}
这里ToLua.ToObject
会将lua栈上的userdata转换为object,也是通过userdata的index,在C#的object缓存中查找,然后返回,因此这里是不会产生gc的。
下面看一下ToInt
方法:
static int ToInt(IntPtr L)
{
try
{
object arg0 = ToLua.CheckObject<System.Enum>(L, 1);
int ret = Convert.ToInt32(arg0);
LuaDLL.lua_pushinteger(L, ret);
return 1;
}
catch (Exception e)
{
return LuaDLL.toluaL_exception(L, e);
}
}
同样地,这里的CheckObject
也是在C#的object缓存中查找,只不过会进行类型检查,因此也不会有gc。在将取出的object转换为int时,由于使用的是Convert.ToInt32
方法,也不会触发拆箱。该方法并不涉及类型转换,而是调用了一个接口返回转换结果:
public static int ToInt32(object value) {
return value == null? 0: ((IConvertible)value).ToInt32(null);
}
接下来是和int类型判等的Equals
方法:
static int Equals(IntPtr L)
{
try
{
ToLua.CheckArgsCount(L, 2);
System.Enum obj = (System.Enum)ToLua.CheckObject<System.Enum>(L, 1);
object arg0 = ToLua.ToVarObject(L, 2);
bool o = obj != null ? obj.Equals(arg0) : arg0 == null;
LuaDLL.lua_pushboolean(L, o);
return 1;
}
catch (Exception e)
{
return LuaDLL.toluaL_exception(L, e);
}
}
我们看第7行,此时lua栈上的元素是一个number,返回到C#是double,它是一个值类型,然而ToLua.ToVarObject
会将其转换为object,因此这里会触发装箱,也会产生gc。所以,在实际使用中,应当尽量避免在lua层对C#的enum与number进行比较。
再看例子中的第16行,直接使用了==
操作符来比较space
和e
是否相等。这两者都是C# push到lua层的userdata,userdata只和它自身相等。由于enum的唯一性,它们来自于同一个C#缓存,对应的userdata index也相同,那么在lua层它们就是同一份userdata。
最后看一下例子中的第20行,它是在lua层将一个number转换为enum。IntToEnum
定义在C#的UnityEngine_SpaceWrap
类中,在了解它的具体实现之前,我们先来研究一下Space
这个enum的注册过程:
public static void Register(LuaState L)
{
L.BeginEnum(typeof(UnityEngine.Space));
L.RegVar("World", get_World, null);
L.RegVar("Self", get_Self, null);
L.RegFunction("IntToEnum", IntToEnum);
L.EndEnum();
TypeTraits<UnityEngine.Space>.Check = CheckType;
StackTraits<UnityEngine.Space>.Push = Push;
}
BeginEnum
会调用到C层的tolua_beginenum
函数:
LUALIB_API int tolua_beginenum(lua_State *L, const char *name)
{
lua_pushstring(L, name);
lua_newtable(L);
_addtoloaded(L);
lua_newtable(L);
lua_pushvalue(L, -1);
int reference = luaL_ref(L, LUA_REGISTRYINDEX);
lua_pushlightuserdata(L, &tag);
lua_pushnumber(L, 1);
lua_rawset(L, -3);
lua_pushstring(L, ".name");
_pushfullname(L, -4);
lua_rawset(L, -3);
lua_pushstring(L, "__index");
lua_pushcfunction(L, enum_index_event);
lua_rawset(L, -3);
lua_pushstring(L, "__newindex");
lua_pushcfunction(L, enum_newindex_event);
lua_rawset(L, -3);
return reference;
}
函数的实现与tolua_beginclass
类似,也是使用两个table,一个table表示enum本身,另一个table表示enum的metatable,保存了enum类的全称,以及访问enum类的属性或者方法的__index
和__newindex
元方法。由于enum是不可被继承的,所以这里不需要考虑基类相关的内容。这里以__index
元方法为例,来看看enum_index_event
的实现:
static int enum_index_event (lua_State *L)
{
lua_getmetatable(L, 1); //stack: t, k, mt
if (lua_istable(L, -1))
{
lua_pushvalue(L, 2); //stack: t, k, mt, k
lua_rawget(L, -2); //stack: t, k, mt, v
if (!lua_isnil(L, -1))
{
return 1;
}
lua_pop(L, 1); //stack: t, k, mt
lua_pushlightuserdata(L, &gettag);
lua_rawget(L, -2); //stack: t, k, mt, tget
if (lua_istable(L,-1))
{
lua_pushvalue(L,2); //stack: t k mt tget k
lua_rawget(L,-2); //stack: t k mt tget v
if (lua_isfunction(L,-1))
{
lua_call(L, 0, 1);
lua_pushvalue(L,2);
lua_pushvalue(L,-2);
lua_rawset(L, 3);
return 1;
}
lua_pop(L, 1);
}
}
lua_settop(L, 2);
lua_pushnil(L);
return 1;
}
这里的实现与class_index_event
相比,要简化许多。函数首先获取当前enum对象或者enum类的metatable,如果metatable存在,说明是已经注册过的enum,则先去metatable中直接查找要访问的key,如果查找成功直接返回即可;如果查找失败,则继续在metatable的get table中查找,如果查找到对应的get属性,则将其缓存后返回。由于enum不存在基类的说法,所以也不需要像class一样,进行循环查找。
现在我们回过头再看IntToEnum
的具体实现:
static int IntToEnum(IntPtr L)
{
int arg0 = (int)LuaDLL.lua_tonumber(L, 1);
UnityEngine.Space o = (UnityEngine.Space)arg0;
ToLua.Push(L, o);
return 1;
}
这里直接将lua栈上返回的double转换为int,然后再转换为UnityEngine.Space
类型,由于都只是值类型之间的转换,因此不存在拆装箱,也不会有gc。前面我们已经提到过C#层push enum到lua层时,是会从C#的缓存中取到enum对应的object,然后再将object push到lua层,所以这里也不会产生gc。
总结一下,在lua使用C#的enum时,C#传递enum到lua层不会产生gc,在lua层number与enum类型互相转换不会产生gc,两个enum直接比较也不会产生gc。但是,enum与number类型进行比较时,会有一次number类型转换到object类型的操作,触发装箱并产生gc。这一点,其实我们可以自己进行针对性优化。
下一节我们将研究平时开发中相当常用的,C#的委托/事件注册lua函数的实现。
如果你觉得我的文章有帮助,欢迎关注我的微信公众号 我是真的想做游戏啊
Reference
[1] Does Convert.ToInt32(object) skip unboxing?