• tolua源码分析(六) C#委托使用lua函数的机制实现


    tolua源码分析(六) C#委托使用lua函数的机制实现

    上一节我们讨论lua层如何使用C#的enum以及使用enum需要注意的事项。这一节我们将讨论项目日常开发中经常会用到的委托注册机制,即C#层暴露若干委托和事件,然后在lua层进行注册,为它们绑定lua层的函数,使得C#层可以通过委托和事件调用到lua函数。这个最常见的用法要数UI了,例如C#的UI Button上有个ActionClick的委托,它会在Button被玩家按下的时候触发,而触发后具体执行的逻辑我们希望在lua层实现,因此我们就需要在lua层,往这个C#委托上绑定函数。那么,这个机制具体是怎样实现的呢?在这便利的背后又隐藏了哪些陷阱呢?还是老样子,我们先从官方给的例子example 11看起,首先是测试用的lua代码:

    function DoClick1(go)                
        print('click1 gameObject is '..go.name)                    
    end
    
    function DoClick2(go)                
        print('click2 gameObject is '..go.name)                              
    end                       
    
    function AddClick1(listener)       
        if listener.onClick then
            listener.onClick = listener.onClick + DoClick1                                                    
        else
            listener.onClick = DoClick1                      
        end                
    end
    
    function AddClick2(listener)
        if listener.onClick then
            listener.onClick = listener.onClick + DoClick2                      
        else
            listener.onClick = DoClick2
        end                
    end
    
    function SetClick1(listener)
        if listener.onClick then
            listener.onClick:Destroy()
        end
    
        listener.onClick = DoClick1         
    end
    
    function RemoveClick1(listener)
        if listener.onClick then
            listener.onClick = listener.onClick - DoClick1      
        else
            print('empty delegate')
        end
    end
    
    function RemoveClick2(listener)
        if listener.onClick then
            listener.onClick = listener.onClick - DoClick2    
        else
            print('empty delegate')                                
        end
    end
    
    function TestOverride(listener)
        listener:SetOnFinished(TestEventListener.OnClick(DoClick1))
        listener:SetOnFinished(TestEventListener.VoidDelegate(DoClick2))
    end
    
    function TestEvent()
        print('this is a event')
    end
    
    function AddEvent(listener)
        listener.onClickEvent = listener.onClickEvent + TestEvent
    end
    
    function RemoveEvent(listener)
        listener.onClickEvent = listener.onClickEvent - TestEvent
    end
    
    local t = {name = 'byself'}
    
    function t:TestSelffunc()
        print('callback with self: '..self.name)
    end       
    
    function AddSelfClick(listener)
        if listener.onClick then
            listener.onClick = listener.onClick + TestEventListener.OnClick(t.TestSelffunc, t)
        else
            listener.onClick = TestEventListener.OnClick(t.TestSelffunc, t)
        end   
    end     
    
    function RemoveSelfClick(listener)
        if listener.onClick then
            listener.onClick = listener.onClick - TestEventListener.OnClick(t.TestSelffunc, t)
        else
            print('empty delegate')
        end   
    end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86

    然后C#层获取这些需要测试的函数,逐一进行测试:

    SetClick1 = state.GetFunction("SetClick1");
    AddClick1 = state.GetFunction("AddClick1");
    AddClick2 = state.GetFunction("AddClick2");
    RemoveClick1 = state.GetFunction("RemoveClick1");
    RemoveClick2 = state.GetFunction("RemoveClick2");
    TestOverride = state.GetFunction("TestOverride");
    AddEvent = state.GetFunction("AddEvent");
    RemoveEvent = state.GetFunction("RemoveEvent");
    
    AddSelfClick = state.GetFunction("AddSelfClick");
    RemoveSelfClick = state.GetFunction("RemoveSelfClick");
    
    if (GUI.Button(new Rect(10, 10, 120, 40), " = OnClick1"))
    {
        CallLuaFunction(SetClick1);
    }
    else if (GUI.Button(new Rect(10, 60, 120, 40), " + Click1"))
    {
        CallLuaFunction(AddClick1);
    }
    else if (GUI.Button(new Rect(10, 110, 120, 40), " + Click2"))
    {
        CallLuaFunction(AddClick2);
    }
    else if (GUI.Button(new Rect(10, 160, 120, 40), " - Click1"))
    {
        CallLuaFunction(RemoveClick1);
    }
    else if (GUI.Button(new Rect(10, 210, 120, 40), " - Click2"))
    {
        CallLuaFunction(RemoveClick2);
    }
    else if (GUI.Button(new Rect(10, 260, 120, 40), "+ Click1 in C#"))
    {
        tips = "";
        LuaFunction func = state.GetFunction("DoClick1");
        TestEventListener.OnClick onClick = (TestEventListener.OnClick)DelegateTraits.Create(func);
        listener.onClick += onClick;
    }        
    else if (GUI.Button(new Rect(10, 310, 120, 40), " - Click1 in C#"))
    {
        tips = "";
        LuaFunction func = state.GetFunction("DoClick1");
        listener.onClick = (TestEventListener.OnClick)DelegateFactory.RemoveDelegate(listener.onClick, func);
        func.Dispose();
        func = null;
    }
    else if (GUI.Button(new Rect(10, 360, 120, 40), "OnClick"))
    {
        if (listener.onClick != null)
        {
            listener.onClick(gameObject);
        }
        else
        {
            Debug.Log("empty delegate!!");
        }
    }
    else if (GUI.Button(new Rect(10, 410, 120, 40), "Override"))
    {
        CallLuaFunction(TestOverride);
    }
    else if (GUI.Button(new Rect(10, 460, 120, 40), "Force GC"))
    {
        state.LuaGC(LuaGCOptions.LUA_GCCOLLECT, 0);
        GC.Collect();
    }
    else if (GUI.Button(new Rect(10, 510, 120, 40), "event +"))
    {
        CallLuaFunction(AddEvent);
    }
    else if (GUI.Button(new Rect(10, 560, 120, 40), "event -"))
    {
        CallLuaFunction(RemoveEvent);
    }
    else if (GUI.Button(new Rect(10, 610, 120, 40), "event call"))
    {
        listener.OnClickEvent(gameObject);
    }
    else if (GUI.Button(new Rect(200, 10, 120, 40), "+self call"))
    {
        CallLuaFunction(AddSelfClick);
    }
    else if (GUI.Button(new Rect(200, 60, 120, 40), "-self call"))
    {
        CallLuaFunction(RemoveSelfClick);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87

    测试代码很长,这里我们拆解拆解,先看看SetClick1的调用,它接受一个listener的参数,这个listener是从C#层传入的,它是TestEventListener这个类的对象:

    public sealed class TestEventListener : MonoBehaviour
    {
        public delegate void VoidDelegate(GameObject go);
        public delegate void OnClick(GameObject go);    
        public OnClick onClick = delegate { };
    
        public event OnClick onClickEvent = delegate { };
    
        public Func TestFunc = null;
    
        public void SetOnFinished(OnClick click)
        {
            Debugger.Log("SetOnFinished OnClick");
        }
        
        public void SetOnFinished(VoidDelegate click)
        {
            Debugger.Log("SetOnFinished VoidDelegate");
        }
    
        [NoToLuaAttribute]
        public void OnClickEvent(GameObject go)
        {
            onClickEvent(go);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    函数SetClick1长这样:

    function SetClick1(listener)
        if listener.onClick then
            listener.onClick:Destroy()
        end
    
        listener.onClick = DoClick1         
    end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    由于TestEventListener类构造时会将onClick初始化为一个空的委托,因此这里会先走到Destroy上:

    static int Destroy(IntPtr L)
    {
        Delegate arg0 = (Delegate)ToLua.CheckObject(L, 1);
        Delegate[] ds = arg0.GetInvocationList();
    
        for (int i = 0; i < ds.Length; i++)
        {
            LuaDelegate ld = ds[i].Target as LuaDelegate;
    
            if (ld != null)
            {                
                ld.Dispose();                
            }
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    函数会先将栈上的对象转换为Delegate,获取它绑定的所有函数,通过Target属性可以获得绑定函数所属的实例对象。如果是在lua层绑定的,tolua都会使用一个LuaDelegate包装类的对象来完成真正绑定。所以这里的Destroy只会把lua层绑定到委托的函数进行清理。显然这里默认的空函数是不会清理掉的。

    回到lua代码,第6行会触发C#层的set_onClick函数:

    static int set_onClick(IntPtr L)
    {
        object o = null;
    
        try
        {
            o = ToLua.ToObject(L, 1);
            TestEventListener obj = (TestEventListener)o;
            TestEventListener.OnClick arg0 = (TestEventListener.OnClick)ToLua.CheckDelegate(L, 2);
            obj.onClick = arg0;
            return 0;
        }
        catch(Exception e)
        {
            return LuaDLL.toluaL_exception(L, e, o, "attempt to index onClick on a nil value");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    函数最关键的地方在第9行,我们来看看lua函数是如何转换为TestEventListener.OnClick类型的委托的:

    public static Delegate CheckDelegate(IntPtr L, int stackPos)
    {
        LuaTypes luatype = LuaDLL.lua_type(L, stackPos);
    
        switch (luatype)
        {
            case LuaTypes.LUA_TFUNCTION:
                LuaFunction func = ToLua.ToLuaFunction(L, stackPos);
                return DelegateTraits.Create(func);
                ...
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    第8行首先把lua函数转换为LuaFunction包装类的对象。第9行的DelegateTraits和我们之前提到的TypeTraits类似,会根据不同的类型,调用不同的Create函数来创建不同类型的委托:

    static public Delegate Create(LuaFunction func)
    {
        if (func != null)
        {
            LuaState state = func.GetLuaState();
            LuaDelegate target = state.GetLuaDelegate(func);
    
            if (target != null)
            {
                return Delegate.CreateDelegate(typeof(T), target, target.method);
            }
            else
            {
                Delegate d = _Create(func, null, false);
                target = d.Target as LuaDelegate;
                state.AddLuaDelegate(target, func);
                return d;
            }
        }
    
        return _Create(null, null, false);            
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    LuaFunction类似,tolua使用LuaDelegate来表示C#层当前引用的lua层委托,第6行的GetLuaDelegate就是从C#的delegateMap缓存中查找LuaFunction对应的LuaDelegate是否存在,如果存在就直接取出,调用C#的CreateDelegate函数创建委托,注意这里的target设置成了当前的LuaDelegate对象,这样Destroy的时候就不怕找不到它了。另外一点值得注意的是,它和LuaFunction不同,LuaFunction对于同一个lua函数,只会在C#层中保存一份,然后C#层可以直接通过LuaFunction.PCall调用lua函数;而LuaDelegate对于同一个lua函数,虽然也只会在C#层保存一份,但是转换成C#委托进行调用时,每次都会创建一个新的委托。也就是说,我们在lua层对C#委托赋值时,最好只在初始化的时候赋值一次。

    如果LuaDelegate不存在,则会调用对应类型的Create函数创建委托。例如这里的TestEventListener.OnClick

    class TestEventListener_OnClick_Event : LuaDelegate
    {
        public TestEventListener_OnClick_Event(LuaFunction func) : base(func) { }
        public TestEventListener_OnClick_Event(LuaFunction func, LuaTable self) : base(func, self) { }
    
    
        public void Call(UnityEngine.GameObject param0)
        {
            func.BeginPCall();
            func.Push(param0);
            func.PCall();
            func.EndPCall();
        }
    
        public void CallWithSelf(UnityEngine.GameObject param0)
        {
            func.BeginPCall();
            func.Push(self);
            func.Push(param0);
            func.PCall();
            func.EndPCall();
        }
    
    }
    
    public static TestEventListener.OnClick TestEventListener_OnClick(LuaFunction func, LuaTable self, bool flag)
    {
        if (func == null)
        {
            TestEventListener.OnClick fn = delegate(UnityEngine.GameObject param0) { };
            return fn;
        }
    
        if(!flag)
        {
            TestEventListener_OnClick_Event target = new TestEventListener_OnClick_Event(func);
            TestEventListener.OnClick d = target.Call;
            target.method = d.Method;
            return d;
        }
        else
        {
            TestEventListener_OnClick_Event target = new TestEventListener_OnClick_Event(func, self);
            TestEventListener.OnClick d = target.CallWithSelf;
            target.method = d.Method;
            return d;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    TestEventListener_OnClick就是创建委托的函数,它有个标志位flag表示是否带有self参数,这里由于直接set赋值是没有的,所以flag为false。TestEventListener_OnClick_Event就是包装类,它继承自LuaDelegate,拥有CallCallWithSelf两个方法,可以看出带有self的方法,就是把构造时传入的self,当作函数的一个参数压入lua栈,给lua层调用,这就支持了lua层带有self语法糖的函数,这样的函数也可以绑定到C#的委托上。

    成功创建委托之后,需要把委托记录到C#缓存中,避免对同一个lua函数多次创建LuaDelegate包装类对象。此时,如果我们在C#层触发listener.onClick,就会调用到TestEventListener_OnClick_Event.Call,进而调用到lua层的DoClick1函数,输出如下:

    tolua源码分析(六)1

    接下来我们来看下委托的+=机制在lua层的实现。对应到例子里就是AddClick1AddClick2这两个函数:

    function AddClick1(listener)       
        if listener.onClick then
            listener.onClick = listener.onClick + DoClick1                                                    
        else
            listener.onClick = DoClick1                      
        end                
    end
    
    function AddClick2(listener)
        if listener.onClick then
            listener.onClick = listener.onClick + DoClick2                      
        else
            listener.onClick = DoClick2
        end                
    end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    tolua对+操作符进行了重载,即在metatable中设置了__add元方法,会调用到C#的op_Addition

    static int op_Addition(IntPtr L)
    {
        try
        {                        
            LuaTypes type = LuaDLL.lua_type(L, 1);
    
            switch (type)
            {
                case LuaTypes.LUA_TUSERDATA:
                    Delegate a0 = ToLua.ToObject(L, 1) as Delegate;
                    Delegate a1 = ToLua.CheckDelegate(a0.GetType(), L, 2);
                    Delegate ret = Delegate.Combine(a0, a1);
                    ToLua.Push(L, ret);
                    return 1;
                    ...
            }
        }
        catch (Exception e)
        {
            return LuaDLL.toluaL_exception(L, e);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    +左边的listener.onClick是个C#对象,显然是个userdata,根据其index,从C#缓存中查找对应的Delegate,右侧为lua函数,同样需要将其转换为Delegate,过程与前面类似,这里就不重复了。得到两个Delegate之后,调用C#的Combine函数合成最终的委托,压入lua层。调用AddClick1AddClick2后再触发触发listener.onClick的结果输出如下:

    tolua源码分析(六)2

    类似地,tolua对-操作符也进行了重载,会调用到C#的op_Subtraction,它的逻辑就是找到持有相同LuaFunctionLuaDelegate,然后从委托中移除:

    static int op_Subtraction(IntPtr L)
    {
        try
        {            
            Delegate arg0 = (Delegate)ToLua.CheckObject(L, 1);
            LuaTypes type = LuaDLL.lua_type(L, 2);
    
            if (type == LuaTypes.LUA_TFUNCTION)
            {
                LuaState state = LuaState.Get(L);
                LuaFunction func = ToLua.ToLuaFunction(L, 2);
                Delegate[] ds = arg0.GetInvocationList();
    
                for (int i = 0; i < ds.Length; i++)
                {
                    LuaDelegate ld = ds[i].Target as LuaDelegate;
    
                    if (ld != null && ld.func == func && ld.self == null)
                    {
                        arg0 = Delegate.Remove(arg0, ds[i]);
                        state.DelayDispose(ld.func);
                        break;
                    }
                }
    
                func.Dispose();
                ToLua.Push(L, arg0);
                return 1;
            }
            else
            {
                ...
            }
        }
        catch (Exception e)
        {
            return LuaDLL.toluaL_exception(L, e);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    这里需要特别注意的是DelayDispose这个函数。tolua中C#引用的lua table/function对象均继承自LuaBaseRef这个类,它通过引用计数的形式来判断lua对象是否仍被C#引用。如果引用计数为0,说明C#不再引用,则包装类对象可被清理等待C# gc回收,同时lua层缓存在注册表中的对象也可被清理,即lua层也可gc掉该对象、而对于这里的委托来说,情况稍微复杂一些,由于LuaDelegate对象是被缓存在delegateMap中的,而它本身又引用住了一个LuaFunction对象,所以如果这里直接Dispose掉LuaFunction对象,其引用计数很可能在出函数作用域时变为0,导致LuaFunction里的数据无效,而LuaDelegate对象并不一定会被Dipose掉,这就可能导致一个有效的LuaDelegate对象持有一个无效的LuaFunction对象。所以,为了避免这种情况发生,DelayDispose函数会把LunFunction对象放在一个延迟列表里引用住,避免出作用域被Dispose。

    然后我们来看看带有self的lua函数是怎么绑定的:

    function AddSelfClick(listener)
        if listener.onClick then
            listener.onClick = listener.onClick + TestEventListener.OnClick(t.TestSelffunc, t)
        else
            listener.onClick = TestEventListener.OnClick(t.TestSelffunc, t)
        end   
    end 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    可以发现这里没有优雅的写法,是通过传入lua函数和self table,构造出C#对象的方式,来绑定委托的。TestEventListener.OnClick的实现也比较简单,就是调用对应Delegate类型的Create函数:

    static int TestEventListener_OnClick(IntPtr L)
    {
        try
        {
            int count = LuaDLL.lua_gettop(L);
            LuaFunction func = ToLua.CheckLuaFunction(L, 1);
    
            if (count == 1)
            {
                Delegate arg1 = DelegateTraits.Create(func);
                ToLua.Push(L, arg1);
            }
            else
            {
                LuaTable self = ToLua.CheckLuaTable(L, 2);
                Delegate arg1 = DelegateTraits.Create(func, self);
                ToLua.Push(L, arg1);
            }
            return 1;
        }
        catch(Exception e)
        {
            return LuaDLL.toluaL_exception(L, e);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    带有self table的Create函数大同小异,在缓存给C#的delegateMap上做了一点特殊处理。delegateMap的key实际是对应到lua层的reference,带table意味着有两个reference,所以这里的key是64位的long型,两个reference各占高低32位,这样就能通过一个key把两个reference都记录下来了。这时的输出结果如下:

    tolua源码分析(六)3

    最后,我们来看一下event。C#的event规定了只能使用+=/-=操作,不允许在外部使用=修改event。

    function AddEvent(listener)
        listener.onClickEvent = listener.onClickEvent + TestEvent
    end
    
    • 1
    • 2
    • 3

    那么lua层是如何实现这样的机制呢?首先是get_onClickEvent,它返回的并不是一个委托,而是一个EventObject,注意每次调用该函数时,都会新建一个EventObject对象,它是有gc开销的:

    static int get_onClickEvent(IntPtr L)
    {
        ToLua.Push(L, new EventObject(typeof(TestEventListener.OnClick)));
        return 1;
    }
    
    public class EventObject
    {
        [NoToLuaAttribute]
        public EventOp op = EventOp.None;
        [NoToLuaAttribute]
        public Delegate func = null;
        [NoToLuaAttribute]
        public Type type;
    
        [NoToLuaAttribute]
        public EventObject(Type t)
        {
            type = t;
        }
    
        public static EventObject operator +(EventObject a, Delegate b)
        {
            a.op = EventOp.Add;
            a.func = b;
            return a;
        }
    
        public static EventObject operator -(EventObject a, Delegate b)
        {
            a.op = EventOp.Sub;
            a.func = b;
            return a;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    EventObject类保存了持有的委托,委托的类型,以及当前执行的操作。在对应的op_Addition中,会对这些值进行修改:

    static int op_Addition(IntPtr L)
    {
        try
        {
            EventObject arg0 = (EventObject)ToLua.CheckObject(L, 1, typeof(EventObject));
            arg0.func = ToLua.CheckDelegate(arg0.type, L, 2);
            arg0.op = EventOp.Add;
            ToLua.Push(L, arg0);
            return 1;
        }
        catch (Exception e)
        {
            return LuaDLL.toluaL_exception(L, e);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    然后,在set_onClickEvent中,会对lua栈上的变量进行检查,只有可以表示EventObject类的userdata才是合法的,这就阻止了直接将一个lua函数赋值给event。根据EventObject类保存的信息,是+还是-,对真正的event进行操作:

    static int set_onClickEvent(IntPtr L)
    {
        try
        {
            TestEventListener obj = (TestEventListener)ToLua.CheckObject(L, 1, typeof(TestEventListener));
            EventObject arg0 = null;
    
            if (LuaDLL.lua_isuserdata(L, 2) != 0)
            {
                arg0 = (EventObject)ToLua.ToObject(L, 2);
            }
            else
            {
                return LuaDLL.luaL_throw(L, "The event 'TestEventListener.onClickEvent' can only appear on the left hand side of += or -= when used outside of the type 'TestEventListener'");
            }
    
            if (arg0.op == EventOp.Add)
            {
                TestEventListener.OnClick ev = (TestEventListener.OnClick)arg0.func;
                obj.onClickEvent += ev;
            }
            else if (arg0.op == EventOp.Sub)
            {
                TestEventListener.OnClick ev = (TestEventListener.OnClick)arg0.func;
                obj.onClickEvent -= ev;
            }
    
            return 0;
        }
        catch (Exception e)
        {
            return LuaDLL.toluaL_exception(L, e);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    运行的结果如下:

    tolua源码分析(六)4

    总结一下,一个lua函数想要绑定到C#委托时,首先它会被包装为C#LuaFunction类对象,然后被更上层的C#LuaDelegate类对象所引用,最后绑定到指定的委托上。其中,LuaFunction对象和LuaDelegate对象都会被缓存,在使用时注意lua层需要在用完C#委托时及时释放掉,不要一直保留对其的引用,否则可能会出现内存泄漏的问题。绑定到C#事件时,还要再多一步,LuaDelegate会被EventObject类型的对象所引用,它还包含了当前是绑定还是解绑的操作类型,只有该类型的对象才能赋值到C#事件,根据操作类型,绑定lua函数或是解绑lua函数。需要注意的是,每次在lua层获取C#事件时,都会创建一个新的EventObject对象,C#层没有专门针对该类型对象的缓存。总之,在实际项目中,要对委托和事件的使用慎之又慎。

    下一节我们将探讨C#中带out参数的函数是如何在lua层使用的。

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

  • 相关阅读:
    2022牛客多校十 E-Reviewer Assignment(匈牙利算法)
    【中国平安社招校招】【内推】【当天内推】
    变压器(电抗器) 红外测温作业指导书
    CEP插件-图标ICON-图标不显示-manifest.xml文件范例-CEFCommandLine命令
    到底什么是LPO?
    CMMI可以直接办理五级吗?
    提高软件测试覆盖率的5个重点
    Jetson Orin平台4-16路 GMSL2/GSML1相机采集套件推荐
    视频知识点(19)- YUV420好,还是YUV444好?
    【源码+项目部署】Java项目实战_Java学生成绩在线管理_Java课程设计_Java毕业设计_Java开源项目_Java项目开发_课设项目_毕设项目
  • 原文地址:https://blog.csdn.net/weixin_45776473/article/details/130900019