在使用lua代码开发的过程中,一个非常重要的部分是对C#代码的调用,接下来就结合源码分析理解它的实现:
在lua中,使用诸如以下代码便可以调用C#的代码:
CS.UnityEngine.Debug.Log('hello world')
CS是一个全局的Table,所以CS.UnityEngine可以当做是在一个名为CS的Table中查询名为UnityEngine的值。在调动虚拟机也就是LuaEnv的创建时,会调用以下代码对CS表进行初始化:
DoString(init_xlua, "Init");
初始化代码部分截取如下:
local metatable = {}
local rawget = rawget
local setmetatable = setmetatable
local import_type = xlua.import_type
local import_generic_type = xlua.import_generic_type
local load_assembly = xlua.load_assembly
function metatable:__index(key)
--获取key为".fqn"的值
local fqn = rawget(self, ".fqn")
--拼接".fqn"的值和本次调用的key
fqn = ((fqn and fqn .. ".") or "") .. key
--查询CS类型
local obj = import_type(fqn)
if obj == nil then
-- It might be an assembly, so we load it too.
obj = {[".fqn"] = fqn}
setmetatable(obj, metatable)
elseif obj == true then
return rawget(self, key)
end
-- Cache this lookup
rawset(self, key, obj)
return obj
end
CS = CS or {}
setmetatable(CS, metatable)
这部分描述了CS表中__index元方法的实现,在我们调用CS表中一个不存在的字段时便会调用这个函数。
我们用CS.UnityEngine.Debug为例来解释这个函数是怎么执行的:
1)首先CS为一个全局的空表,访问CS.UnityEngine由于UnityEngine字段不存在而直接调用到此函数。第一行获取key为".fqn"的值,fqn为空,所以第二行结束后fqn=UnityEngine
2)调用import_type去查询UnityEngine这个类型对应的lua表。
3)对返回值obj进行判断,UnityEngine很明显不是一个类型所以走==nil的分支,创建一个表并且设置".fqn"="UnityEngine",并且将此metatable也设置为这个表的元表
4)将CS表中将obj这个表设置给“UnityEngine”。
5)接下来访问CS.UnityEngine.Debug就相当于从CS.UnityEngine中访问字段Debug,由于没有Debug这个字段所以也会调用到__index这个函数中,这时候fqn就有值了,第二行结束后fqn的值为“UnityEngine.Debug”
6)这时候再通过import_type函数去查询这个类型就有值了,它会走obj==true这个分支,而在这个import_type函数中实际上已经将CS.UnityEngine.Debug的值给设置好了,这时候通过rawget函数来拿到这个lua表返回
以上应该很清晰的解释了CS.XXX的写法实现调用C#代码的过程,而import_type内部是怎么去查找C#类型的,接下来说明:
(注:接下来代码中含有大量关于LuaAPI的部分,想要深入了解的可以查阅《lua程序设计(第4版)》27章之后的内容)
import_type = xlua.import_type。而这个xlua是一个全局table,在C代码中声明,位置为build/xlua.c:
xlua.c:
LUA_API void luaopen_xlua(lua_State *L) {
luaL_openlibs(L);
#if LUA_VERSION_NUM >= 503
luaL_newlib(L, xlualib);
lua_setglobal(L, "xlua");
#else
luaL_register(L, "xlua", xlualib);
lua_pop(L, 1);
#endif
}
这段代码属于xlua.dll,在创建LuaEnv时会调用,设置一个全局表xlua。同时,luaEnv创建时会注册import_type这个函数,当调用xlua.import_type时会调用到对应的C#委托上:
ObjectTranslator.cs:
public void OpenLib(RealStatePtr L) {
if (0 != LuaAPI.xlua_getglobal(L, "xlua")){ throw new Exception("call xlua_getglobal fail!" + LuaAPI.lua_tostring(L, -1));}
LuaAPI.xlua_pushasciistring(L, "import_type");
LuaAPI.lua_pushstdcallcfunction(L,importTypeFunction);
LuaAPI.lua_rawset(L, -3);
...
}
接下来找到这个importTypeFunction委托,会发现最终调用到这个函数:
StaticLuaCallbacks.cs:
public static int ImportType(RealStatePtr L)
{
try
{
ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
//需要查询的类名
string className = LuaAPI.lua_tostring(L, 1);
//查找C#对应的Type(此处还没去查找对应Lua的表)
Type type = translator.FindType(className);
if (type != null)
{
//这句查找Type对应的lua表
if (translator.GetTypeId(L, type) >= 0)
{
LuaAPI.lua_pushboolean(L, true);
}
else
{
return LuaAPI.luaL_error(L, "can not load type " + type);
}
}
else
{
LuaAPI.lua_pushnil(L);
}
return 1;
}
}
通过以上代码会发现,如果没有找到对应的type则直接向lua虚拟栈(lua与其他语言通信的主要组件)中推一个nil,否则则去查找对应的lua表,如果有则推一个true,否则推一个错误码。最终return 1代表的是返回的参数数量为1。总而言之,上述lua代码中xlua.import_type函数返回的obj事实上只可能为true/nil/错误码,而对错误码是没有特殊处理的。
而translator.GetTypeId内部的细节是关键,它包含了xlua实现lua调用C#代码的两种方式:一种是通过生成适配代码,另一种是通过反射来调用:
ObjectTranslator.cs
internal int getTypeId(RealStatePtr L, Type type, out bool is_first, LOGLEVEL log_level = LOGLEVEL.WARN)
{
int type_id;
is_first = false;
//查询是否缓存中有Type对应的Lua表,有就直接返回
if (!typeIdMap.TryGetValue(type, out type_id)) // no reference
{
...
is_first = true;
Type alias_type = null;
aliasCfg.TryGetValue(type, out alias_type);
//从注册表中检查Type对应的元表
LuaAPI.luaL_getmetatable(L, alias_type == null ? type.FullName : alias_type.FullName);
//元表为空,走相关注册逻辑
if (LuaAPI.lua_isnil(L, -1)) //no meta yet, try to use reflection meta
{
LuaAPI.lua_pop(L, 1);
//此处会去检查是使用反射还是生成适配代码
if (TryDelayWrapLoader(L, alias_type == null ? type : alias_type))
{
LuaAPI.luaL_getmetatable(L, alias_type == null ? type.FullName : alias_type.FullName);
}
else
{
throw new Exception("Fatal: can not load metatable of type:" + type);
}
}
//循环依赖,自身依赖自己的class,比如有个自身类型的静态readonly对象。
if (typeIdMap.TryGetValue(type, out type_id))
{
LuaAPI.lua_pop(L, 1);
}
else
{
...
LuaAPI.lua_pop(L, 1);
//缓存type与其对应到lua中的表
typeIdMap.Add(type, type_id);
}
}
return type_id;
}
public bool TryDelayWrapLoader(RealStatePtr L, Type type)
{
if (loaded_types.ContainsKey(type)) return true;
loaded_types.Add(type, true);
LuaAPI.luaL_newmetatable(L, type.FullName);
LuaAPI.lua_pop(L, 1);
Action<RealStatePtr> loader;
int top = LuaAPI.lua_gettop(L);.
//这个delayWrap字典在Xlua生成的代码中(在XLua_Gen_Initer_Register__类
//实例化时)将每一个类型的wrap注册在里面
if (delayWrap.TryGetValue(type, out loader))
{
delayWrap.Remove(type);
//将类方法,字段,成员等注册
loader(L);
}
//这里就是反射的逻辑了
else
{
...
//用反射将类方法,字段,成员等注册
Utils.ReflectionWrap(L, type, privateAccessibleFlags.Contains(type));
...
}
...
...
return true;
}
首先来看如果有生成适配代码是怎么执行的,也就是上面代码中的loader(L)的执行。以Vector3为例,实际上执行的是以下代码:
public static void __Register(RealStatePtr L)
{
ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
System.Type type = typeof(UnityEngine.Vector3);
Utils.BeginObjectRegister(type, L, translator, 6, 6, 6, 3);
//这边注册一些+/-/==等操作符相关的方法
Utils.RegisterFunc(L, Utils.OBJ_META_IDX, "__add", __AddMeta);
...
Utils.RegisterFunc(L, Utils.OBJ_META_IDX, "__eq", __EqMeta);
//这边注册的是成员函数
Utils.RegisterFunc(L, Utils.METHOD_IDX, "Set", _m_Set);
Utils.RegisterFunc(L, Utils.METHOD_IDX, "GetHashCode", _m_GetHashCode);
...
Utils.RegisterFunc(L, Utils.METHOD_IDX, "Equals", _m_Equals);
Utils.RegisterFunc(L, Utils.METHOD_IDX, "ToString", _m_ToString);
//这边注册的是成员变量的get
Utils.RegisterFunc(L, Utils.GETTER_IDX, "normalized", _g_get_normalized);
...
Utils.RegisterFunc(L, Utils.GETTER_IDX, "y", _g_get_y);
Utils.RegisterFunc(L, Utils.GETTER_IDX, "z", _g_get_z);
//这边注册的是成员变量的set
Utils.RegisterFunc(L, Utils.SETTER_IDX, "x", _s_set_x);
Utils.RegisterFunc(L, Utils.SETTER_IDX, "y", _s_set_y);
Utils.RegisterFunc(L, Utils.SETTER_IDX, "z", _s_set_z);
Utils.EndObjectRegister(type, L, translator, __CSIndexer, __NewIndexer,
null, null, null);
Utils.BeginClassRegister(type, L, __CreateInstance, 26, 10, 0);
//这边注册的是静态函数
Utils.RegisterFunc(L, Utils.CLS_IDX, "Slerp", _m_Slerp_xlua_st_);
...
Utils.RegisterObject(L, translator, Utils.CLS_IDX, "kEpsilonNormalSqrt", UnityEngine.Vector3.kEpsionNormalSqrt);
//这边注册的是静态变量的get和set(Vector3没有静态变量的set所以这里没有)
Utils.RegisterFunc(L, Utils.CLS_GETTER_IDX, "zero", _g_get_zero);
Utils.RegisterFunc(L, Utils.CLS_GETTER_IDX, "one", _g_get_one);
Utils.RegisterFunc(L, Utils.CLS_GETTER_IDX, "forward", _g_get_forward);
Utils.RegisterFunc(L, Utils.CLS_GETTER_IDX, "back", _g_get_back);
Utils.RegisterFunc(L, Utils.CLS_GETTER_IDX, "up", _g_get_up);
Utils.RegisterFunc(L, Utils.CLS_GETTER_IDX, "down", _g_get_down);
Utils.RegisterFunc(L, Utils.CLS_GETTER_IDX, "left", _g_get_left);
Utils.RegisterFunc(L, Utils.CLS_GETTER_IDX, "right", _g_get_right);
Utils.RegisterFunc(L, Utils.CLS_GETTER_IDX, "positiveInfinity", _g_get_positiveInfinity);
Utils.RegisterFunc(L, Utils.CLS_GETTER_IDX, "negativeInfinity", _g_get_negativeInfinity);
Utils.EndClassRegister(type, L, translator);
}
从上代码可看出主要分两部分:静态和对象。
首先从静态部分说起:
Utils.BeginClassRegister函数中在创建了4个lua表,分别是cls_table、cls_metatable、cls_getter_table、cls_setter_table放在栈中-4、-3、-2、-1的位置(-1指的是栈顶,-2是栈顶向下一个的位置,以此类推,栈底是1)。其中cls_table就是此类型对应的lua表,cls_metatable是这个lua表的元表,getter表和Setter表是后面辅助实现__index和__newindex元方法的。
而Utils.RegisterFunc则在这些表中注册上对应的方法:
Utils.cs
public static void RegisterFunc(RealStatePtr L, int idx, string name, LuaCSFunction func)
{
idx = abs_idx(LuaAPI.lua_gettop(L), idx);
LuaAPI.xlua_pushasciistring(L, name);
LuaAPI.lua_pushstdcallcfunction(L, func);
LuaAPI.lua_rawset(L, idx);
}
以代码“Utils.RegisterFunc(L, Utils.CLS_IDX, "Slerp", _m_Slerp_xlua_st_);”为例,相当于在cls_table中注册了Slerp字段,当调用Vector3.Slerp时就会调用到 _m_Slerp_xlua_st_函数上来。
当类型中所有东西都注册完了以后,在函数Utils.EndClassRegister中将__index元方法和__newindex元方法实现好并设置在元表中:
public static void EndClassRegister(Type type, RealStatePtr L, ObjectTranslator translator)
{
//获取栈顶索引以及4个lua表的索引
int top = LuaAPI.lua_gettop(L);
int cls_idx = abs_idx(top, CLS_IDX);
int cls_getter_idx = abs_idx(top, CLS_GETTER_IDX);
int cls_setter_idx = abs_idx(top, CLS_SETTER_IDX);
int cls_meta_idx = abs_idx(top, CLS_META_IDX);
//begin cls index
LuaAPI.xlua_pushasciistring(L, "__index");//向栈中压入“__index”
LuaAPI.lua_pushvalue(L, cls_getter_idx);//压入cls_get_table
LuaAPI.lua_pushvalue(L, cls_idx);//压入cls_table
translator.Push(L, type.BaseType());//压入BaseType的地址
//压入存放__index元方法的表的Key,这个表存放了所有类型的__index元方法
LuaAPI.xlua_pushasciistring(L, LuaClassIndexsFieldName);
//从注册表(可以看作是存放全局变量的地方)中取出这个__index集合表
LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);
//这里会创建一个闭包函数cls_indexer并压入栈,关联并弹出上面压入的除了"__index"的值作为上值
//这个闭包函数中实现了关于lua中__index元方法的功能
//这一步执行过后栈中为:"__index"|cls_indexer
LuaAPI.gen_cls_indexer(L);
//压入LuaClassIndexsFieldName
LuaAPI.xlua_pushasciistring(L, LuaClassIndexsFieldName);
//弹出key,压入__index集合表
LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);//store in lua indexs function tables
//压入当前类型
translator.Push(L, type);
//此时-3的位置是cls_indexer,这里意思是复制一份压入栈顶
LuaAPI.lua_pushvalue(L, -3);
//将这个cls_indexer存入__index的集合表,
LuaAPI.lua_rawset(L, -3);
//__index将集合表弹出
LuaAPI.lua_pop(L, 1);
//设置cls_metatable[__index] = cls_indexer,并弹出键和值
LuaAPI.lua_rawset(L, cls_meta_idx);
//end cls index
//begin cls newindex
LuaAPI.xlua_pushasciistring(L, "__newindex");
//__nexindex与__index步骤一样,不再叙述
...
//设置cls_metatable[__newindex] = cls_newindexer
LuaAPI.lua_rawset(L, cls_meta_idx);
//end cls newindex
LuaAPI.lua_pop(L, 4);
}
对象和静态实现的代码基本上一致,在Utils.BeginObjectRegister创建4个表:obj_meta,obj_method,obj_get,obj_set。之后在Utils.EndObjectRegister中将元表中设置并实现__index和__newindex,当每次使用时根据类型去全局变量中查找这个元表,完成操作映射。
当某个类型没有被生成适配代码时,实际上也是可以被lua访问到的,这时候xlua走的是一个反射的机制,反射的分支可以从ObjectTranslator.TryDelayWrapLoader看到:
public bool TryDelayWrapLoader(RealStatePtr L, Type type)
{
...
if (delayWrap.TryGetValue(type, out loader))
{
...
}
//当Type没有适配代码时会走这个else分支
else
{
...
//用反射将类方法,字段,成员等注册
Utils.ReflectionWrap(L, type, privateAccessibleFlags.Contains(type));
...
}
...
...
return true;
}
Utils.ReflectionWrap这个函数中实际上是将上面适配代码相关注册逻辑都包含在里面了,对于类型中的字段处理:
Utils.cs
static void makeReflectionWrap(RealStatePtr L, Type type, int cls_field, int cls_getter, int cls_setter,
int obj_field, int obj_getter, int obj_setter, int obj_meta, out LuaCSFunction item_getter, out LuaCSFunction item_setter, BindingFlags access)
{
...
for (int i = 0; i < fields.Length; ++i)
{
...
if (field.IsStatic && (field.IsInitOnly || field.IsLiteral))
{
//对于静态字段,直接将字段名=value保存在cls_field表(等同于之前的cls_table)中
LuaAPI.xlua_pushasciistring(L, fieldName);
translator.PushAny(L, field.GetValue(null));
LuaAPI.lua_rawset(L, cls_field);
}
else
{
//成员字段则做一个闭包函数保存在表中,
//映射于C#端的StaticLuaCallbacks.FixCSFunctionWraper这个委托
LuaAPI.xlua_pushasciistring(L, fieldName);
translator.PushFixCSFunction(L, genFieldGetter(type, field));
LuaAPI.lua_rawset(L, field.IsStatic ? cls_getter : obj_getter);
LuaAPI.xlua_pushasciistring(L, fieldName);
translator.PushFixCSFunction(L, genFieldSetter(type, field));
LuaAPI.lua_rawset(L, field.IsStatic ? cls_setter : obj_setter);
}
}
...
//事件映射到FixCSFunctionWraper委托
EventInfo[] events = type.GetEvents(flag);
for (int i = 0; i < events.Length; ++i)
{
EventInfo eventInfo = events[i];
LuaAPI.xlua_pushasciistring(L, eventInfo.Name);
translator.PushFixCSFunction(L, translator.methodWrapsCache.GetEventWrap(type, eventInfo.Name));
bool is_static = (eventInfo.GetAddMethod(true) != null) ? eventInfo.GetAddMethod(true).IsStatic : eventInfo.GetRemoveMethod(true).IsStatic;
LuaAPI.lua_rawset(L, is_static ? cls_field : obj_field);
}
...
//方法映射到FixCSFunctionWraper委托
foreach (var kv in pending_methods)
{
if (kv.Key.Name.StartsWith("op_")) // 操作符
{
LuaAPI.xlua_pushasciistring(L, InternalGlobals.supportOp[kv.Key.Name]);
translator.PushFixCSFunction(L,new LuaCSFunction(translator.methodWrapsCache._GenMethodWrap(type, kv.Key.Name, kv.Value.ToArray()).Call));
LuaAPI.lua_rawset(L, obj_meta);
}
else
{
LuaAPI.xlua_pushasciistring(L, kv.Key.Name);
translator.PushFixCSFunction(L,new LuaCSFunction(translator.methodWrapsCache._GenMethodWrap(type, kv.Key.Name, kv.Value.ToArray()).Call));
LuaAPI.lua_rawset(L, kv.Key.IsStatic ? cls_field : obj_field);
}
}
}
另外还有属性的getter、setter同样也映射到FixCSFunctionWraper委托,代码都在Utils.ReflectionWrap这个函数中,但分散在不同的地方,就不贴了。关于这个PushFixCSFunction中是如何映射的:
internal void PushFixCSFunction(RealStatePtr L, LuaCSFunction func)
{
if (func == null)
{
LuaAPI.lua_pushnil(L);
}
else
{
//这里压入一个索引
LuaAPI.xlua_pushinteger(L, fix_cs_functions.Count);
//将函数缓存在fix_cs_functions中
fix_cs_functions.Add(func);
//创建一个闭包函数并压入栈,将上面的索引作为upvalue
LuaAPI.lua_pushstdcallcfunction(L, metaFunctions.FixCSFunctionWraper, 1);
}
}
每当我们通过反射调用时,最终都会调用到FixCSFunctionWraper这个委托中:
StaticLuaCallbacks.cs
static int FixCSFunction(RealStatePtr L)
{
try
{
ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
int idx = LuaAPI.xlua_tointeger(L, LuaAPI.xlua_upvalueindex(1));
LuaCSFunction func = (LuaCSFunction)translator.GetFixCSFunction(idx);
return func(L);
}
catch (Exception e)
{
return LuaAPI.luaL_error(L, "c# exception in FixCSFunction:" + e);
}
}
从上面代码可以看到,每次调用时会取出upvalue中的索引,然后从fix_cs_functions列表中取出函数进行调用。
至此,整个调用就已经讲完了。从上可以看出,每个类型的第一次调用会进行执行一系列注册逻辑,这部分逻辑是有大量的GC Alloc的,在我们实际开发中,应该尽量将这些逻辑提前在性能不吃紧的位置执行。
另外在lua中使用到的CS类型也应该保证去生成适配代码,否则将会走反射的逻辑,而反射的效率不高并且在反射API的调用中有少量GC产生。