xlua源码分析(一) C# Call lua的实现

xlua源码分析(一) C# Call lua的实现

本文是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
}

如果你觉得我的文章有帮助,欢迎关注我的微信公众号 我是真的想做游戏啊

你可能感兴趣的:(xlua源码分析,c#,lua,xlua)