本文是xlua源码分析的第一篇。之前我们比较系统地分析了tolua的源码,本系列的文章则会多侧重于两个框架之间一些特性实现细节的差异。这次我们先来看看xlua关于C#调用lua函数的实现细节。
这里以Examples/02_U3DScripting为例来进行分析。例子主要展示了在lua层实现awake,start,update,ondestroy函数,然后在C#层的MonoBehavior调用这些lua函数。例子中的主要逻辑都在Awake函数:
void Awake()
{
scriptEnv = luaEnv.NewTable();
// 为每个脚本设置一个独立的环境,可一定程度上防止脚本间全局变量、函数冲突
LuaTable meta = luaEnv.NewTable();
meta.Set("__index", luaEnv.Global);
scriptEnv.SetMetaTable(meta);
meta.Dispose();
scriptEnv.Set("self", this);
foreach (var injection in injections)
{
scriptEnv.Set(injection.name, injection.value);
}
luaEnv.DoString(luaScript.text, "LuaTestScript", scriptEnv);
Action luaAwake = scriptEnv.Get("awake");
scriptEnv.Get("start", out luaStart);
scriptEnv.Get("update", out luaUpdate);
scriptEnv.Get("ondestroy", out luaOnDestroy);
if (luaAwake != null)
{
luaAwake();
}
}
最简单的方式依旧是调用DoString方法直接把lua代码load进来执行。相比于tolua,xlua支持C#传递一个env table,这样在load结束之后还可以setfenv。
public object[] DoString(byte[] chunk, string chunkName = "chunk", LuaTable env = null)
{
...
if (LuaAPI.xluaL_loadbuffer(_L, chunk, chunk.Length, chunkName) == 0)
{
if (env != null)
{
env.push(_L);
LuaAPI.lua_setfenv(_L, -2);
}
...
}
...
}
另外,xlua的DoString是有返回值的,返回类型是object数组,也就是C#可以执行任意的lua代码,并且直接获取到lua代码的返回值。
xlua中,持有lua table的C#数据结构也叫LuaTable,例子中就是直接从这个env table中获取lua层定义的awake,start,update,ondestroy函数。说到获取,由于lua table的key和value都是不定类型的,而且要避免拆装箱,所以Get函数要使用泛型的形式来实现,不能使用object类型:
[Obsolete("use no boxing version: Get !")]
public T Get(object key)
{
T ret;
Get(key, out ret);
return ret;
}
public TValue Get(TKey key)
{
TValue ret;
Get(key, out ret);
return ret;
}
public TValue Get(string key)
{
TValue ret;
Get(key, out ret);
return ret;
}
xlua C#层持有lua table的原理与tolua类似,都是拿一个reference,这个reference是通过调用luaL_ref函数得到的,那么这里就需要调用lua_getref把table压栈。由于key的类型不同,push到lua栈的方式也可能不同,同样为了避免拆装箱,需要一个泛型的Push函数:
public void PushByType(RealStatePtr L, T v)
{
Action push_func;
if (tryGetPushFuncByType(typeof(T), out push_func))
{
push_func(L, v);
}
else
{
PushAny(L, v);
}
}
这个函数首先会根据参数的类型寻找对应的push函数,如果找到了调用即可,如果没找到,才调用有拆装箱代价的PushAny函数。默认支持的push类型如下:
push_func_with_type = new Dictionary()
{
{typeof(int), new Action(LuaAPI.xlua_pushinteger) },
{typeof(double), new Action(LuaAPI.lua_pushnumber) },
{typeof(string), new Action(LuaAPI.lua_pushstring) },
{typeof(byte[]), new Action(LuaAPI.lua_pushstring) },
{typeof(bool), new Action(LuaAPI.lua_pushboolean) },
{typeof(long), new Action(LuaAPI.lua_pushint64) },
{typeof(ulong), new Action(LuaAPI.lua_pushuint64) },
{typeof(IntPtr), new Action(LuaAPI.lua_pushlightuserdata) },
{typeof(decimal), new Action(PushDecimal) },
{typeof(byte), new Action((L, v) => LuaAPI.xlua_pushinteger(L, v)) },
{typeof(sbyte), new Action((L, v) => LuaAPI.xlua_pushinteger(L, v)) },
{typeof(char), new Action((L, v) => LuaAPI.xlua_pushinteger(L, v)) },
{typeof(short), new Action((L, v) => LuaAPI.xlua_pushinteger(L, v)) },
{typeof(ushort), new Action((L, v) => LuaAPI.xlua_pushinteger(L, v)) },
{typeof(uint), new Action(LuaAPI.xlua_pushuint) },
{typeof(float), new Action((L, v) => LuaAPI.lua_pushnumber(L, v)) },
};
这里的key为string类型,对应的push函数为lua_pushstring函数。和tolua相比,这里做了一个小小的优化,xlua内部维护了一个固定长度的byte buffer,如果string参数的大小小于这个buffer,则使用这个buffer来缓存string,这样就不会产生gc了:
public static void lua_pushstring(IntPtr L, string str) //业务使用
{
if (Encoding.UTF8.GetByteCount(str) > InternalGlobals.strBuff.Length)
{
byte[] bytes = Encoding.UTF8.GetBytes(str);
xlua_pushlstring(L, bytes, bytes.Length);
}
else
{
int bytes_len = Encoding.UTF8.GetBytes(str, 0, str.Length, InternalGlobals.strBuff, 0);
xlua_pushlstring(L, InternalGlobals.strBuff, bytes_len);
}
}
然后就可以通过这个key获取对应的value,获取lua层的value的方式也类似,也是一个泛型的方法,优先根据参数的类型寻找对应的get函数,找不到再使用有拆装箱开销的GetObejct:
public void Get(RealStatePtr L, int index, out T v)
{
Func get_func;
if (tryGetGetFuncByType(typeof(T), out get_func))
{
v = get_func(L, index);
}
else
{
v = (T)GetObject(L, index, typeof(T));
}
}
默认支持的类型如下:
get_func_with_type = new Dictionary()
{
{typeof(int), new Func(LuaAPI.xlua_tointeger) },
{typeof(double), new Func(LuaAPI.lua_tonumber) },
{typeof(string), new Func(LuaAPI.lua_tostring) },
{typeof(byte[]), new Func(LuaAPI.lua_tobytes) },
{typeof(bool), new Func(LuaAPI.lua_toboolean) },
{typeof(long), new Func(LuaAPI.lua_toint64) },
{typeof(ulong), new Func(LuaAPI.lua_touint64) },
{typeof(IntPtr), new Func(LuaAPI.lua_touserdata) },
{typeof(decimal), new Func((L, idx) => {
decimal ret;
Get(L, idx, out ret);
return ret;
}) },
{typeof(byte), new Func((L, idx) => (byte)LuaAPI.xlua_tointeger(L, idx) ) },
{typeof(sbyte), new Func((L, idx) => (sbyte)LuaAPI.xlua_tointeger(L, idx) ) },
{typeof(char), new Func((L, idx) => (char)LuaAPI.xlua_tointeger(L, idx) ) },
{typeof(short), new Func((L, idx) => (short)LuaAPI.xlua_tointeger(L, idx) ) },
{typeof(ushort), new Func((L, idx) => (ushort)LuaAPI.xlua_tointeger(L, idx) ) },
{typeof(uint), new Func(LuaAPI.xlua_touint) },
{typeof(float), new Func((L, idx) => (float)LuaAPI.lua_tonumber(L, idx) ) },
};
而这里获取的是lua函数,要将其转换为Action,就得走GetObejct这个函数了,GetObject函数首先会把object视为lua层的userdata,但这里不是,所以就需要获取一个转换函数:
if (typeof(Delegate).IsAssignableFrom(type))
{
return (RealStatePtr L, int idx, object target) =>
{
object obj = fixTypeGetter(L, idx, target);
if (obj != null) return obj;
if (!LuaAPI.lua_isfunction(L, idx))
{
return null;
}
return translator.CreateDelegateBridge(L, type, idx);
};
}
CreateDelegateBridge就是最终的转换函数,它负责将lua栈上的函数转换为一个委托对象。
public object CreateDelegateBridge(RealStatePtr L, Type delegateType, int idx)
{
LuaAPI.lua_pushvalue(L, idx);
LuaAPI.lua_rawget(L, LuaIndexes.LUA_REGISTRYINDEX);
if (!LuaAPI.lua_isnil(L, -1))
{
int referenced = LuaAPI.xlua_tointeger(L, -1);
LuaAPI.lua_pop(L, 1);
if (delegate_bridges[referenced].IsAlive)
{
if (delegateType == null)
{
return delegate_bridges[referenced].Target;
}
DelegateBridgeBase exist_bridge = delegate_bridges[referenced].Target as DelegateBridgeBase;
Delegate exist_delegate;
if (exist_bridge.TryGetDelegate(delegateType, out exist_delegate))
{
return exist_delegate;
}
else
{
exist_delegate = getDelegate(exist_bridge, delegateType);
exist_bridge.AddDelegate(delegateType, exist_delegate);
return exist_delegate;
}
}
}
else
{
LuaAPI.lua_pop(L, 1);
}
LuaAPI.lua_pushvalue(L, idx);
int reference = LuaAPI.luaL_ref(L);
LuaAPI.lua_pushvalue(L, idx);
LuaAPI.lua_pushnumber(L, reference);
LuaAPI.lua_rawset(L, LuaIndexes.LUA_REGISTRYINDEX);
DelegateBridgeBase bridge;
try
{
#if (UNITY_EDITOR || XLUA_GENERAL) && !NET_STANDARD_2_0
if (!DelegateBridge.Gen_Flag)
{
bridge = Activator.CreateInstance(delegate_birdge_type, new object[] { reference, luaEnv }) as DelegateBridgeBase;
}
else
#endif
{
bridge = new DelegateBridge(reference, luaEnv);
}
}
catch(Exception e)
{
LuaAPI.lua_pushvalue(L, idx);
LuaAPI.lua_pushnil(L);
LuaAPI.lua_rawset(L, LuaIndexes.LUA_REGISTRYINDEX);
LuaAPI.lua_pushnil(L);
LuaAPI.xlua_rawseti(L, LuaIndexes.LUA_REGISTRYINDEX, reference);
throw e;
}
if (delegateType == null)
{
delegate_bridges[reference] = new WeakReference(bridge);
return bridge;
}
try {
var ret = getDelegate(bridge, delegateType);
bridge.AddDelegate(delegateType, ret);
delegate_bridges[reference] = new WeakReference(bridge);
return ret;
}
catch(Exception e)
{
bridge.Dispose();
throw e;
}
}
与tolua一样,lua函数被C#引用时,都会存放到registry表中缓存,同时C#获取到该函数的reference之后,会生成一个负责生成具体委托的包装类对象,这里就是DelegateBridge,它是以弱引用的形式存放在delegate_bridges里的,这里使用弱引用,因为delegate_bridges只负责查询,真正引用它的是具体的委托。使用包装类的好处,一是可以在具体委托被gc的时候,做一些与lua层通信的清理工作;二是可以封装委托的创建工作,这样不同类型的委托创建都可以集中在一个类里;最后还可以控制委托的创建逻辑,实现延迟创建等特性。
与tolua不同的是,xlua只使用一个DelegateBridge来创建所有委托类型,不像tolua会生成若干个继承自LuaDelegate的导出类,如System_Action_Int类专门负责生成System的类型委托,System_Action_Bool类则专门负责生成System类型的委托。这样就不会生成庞大臃肿的代码。
public partial class DelegateBridge : DelegateBridgeBase {
public void Action();
public void Action(T1 p1);
public void Action(T1 p1, T2 p2);
public void Action(T1 p1, T2 p2, T3 p3);
public void Action(T1 p1, T2 p2, T3 p3, T4 p4);
public TResult Func();
public TResult Func(T1 p1);
public TResult Func(T1 p1, T2 p2);
public TResult Func(T1 p1, T2 p2, T3 p3);
public TResult Func(T1 p1, T2 p2, T3 p3, T4 p4);
}
但是相应地,就不能像tolua一样,能够直接静态地把委托类型和委托生成函数绑定起来。xlua在第一次生成某个委托类型时,会使用反射寻找函数,之后便将其缓存起来,这样的话性能上的开销也可以接受。具体的委托触发逻辑与tolua是相似的,就是先把pcall要的error function准备好,然后再通过reference拿到lua function,最后调一下pcall,恢复调用栈就行了:
public void Action() {
#if THREAD_SAFE || HOTFIX_ENABLE
lock(luaEnv.luaEnvLock) {
#endif
var L = luaEnv.L;
int oldTop = LuaAPI.lua_gettop(L);
int errFunc = LuaAPI.load_error_func(L, luaEnv.errorFuncRef);
LuaAPI.lua_getref(L, luaReference);
int error = LuaAPI.lua_pcall(L, 0, 0, errFunc);
if (error != 0)
luaEnv.ThrowExceptionFromError(oldTop);
LuaAPI.lua_settop(L, oldTop);
#if THREAD_SAFE || HOTFIX_ENABLE
}
#endif
}
如果你觉得我的文章有帮助,欢迎关注我的微信公众号 我是真的想做游戏啊