tolua源码分析(八)lua扩展继承C#类

tolua源码分析(八)lua扩展继承C#类

上一节我们阐述了lua调用带out参数的C#函数机制,本节我们来看下lua层是如何扩展C#类的。这次的例子在example 17,主要都是lua代码:

LuaTransform = 
{                          
}                                                   

function LuaTransform.Extend(u)         
    local t = {}                        
    local _position = u.position      
    tolua.setpeer(u, t)     

    t.__index = t
    local get = tolua.initget(t)
    local set = tolua.initset(t)   

    local _base = u.base            

    --重写同名属性获取        
    get.position = function(self)                              
        return _position                
    end            

    --重写同名属性设置
    set.position = function(self, v)                 	                                            
        if _position ~= v then         
            _position = v                    
            _base.position = v                                                                      	            
        end
    end

    --重写同名函数
    function t:Translate(...)            
        print('child Translate')
        _base:Translate(...)                   
    end    
                   
    return u
end


--既保证支持继承函数,又支持go.transform == transform 这样的比较
function Test(node)        
    local v = Vector3.one           
    local transform = LuaTransform.Extend(node)                                                         

    local t = os.clock()            
    for i = 1, 200000 do
        transform.position = transform.position
    end
    print('LuaTransform get set cost', os.clock() - t)

    transform:Translate(1,1,1)                                                                     
                
    local child = transform:Find('child')
    print('child is: ', tostring(child))
    
    if child.parent == transform then            
        print('LuaTransform compare to userdata transform is ok')
    end

    transform.xyz = 123
    transform.xyz = 456
    print('extern field xyz is: '.. transform.xyz)
end

C#层负责传一个transform对象给Test方法调用,运行的结果如下:

tolua源码分析(八)1

可以发现,lua层在Extend中重写的同名属性和方法都被调用到了,甚至最后给transform新增的字段xyz也生效了。这是怎么做到的呢?我们来细细研究下函数LuaTransform.Extend

首先,可以看到第8行有一句:tolua.setpeer(u, t),这句话会调用到tolua_bnd_setpeer

static int tolua_bnd_setpeer(lua_State *L) 
{
    // stack: userdata, table
    if (!lua_isuserdata(L, -2)) 
    {
        return luaL_error(L, "Invalid argument #1 to setpeer: userdata expected.");        
    }

    if (lua_isnil(L, 2)) 
    {
        lua_pop(L, 1);
        lua_pushvalue(L, TOLUA_NOPEER);
        lua_setfenv(L, -2);
    }        
    else
    {
        lua_pushvalue(L, 2);                //stack: u p p
        lua_setfenv(L, -3);                 //stack: u p
        lua_newtable(L);                    //stack: u p vt        
        
        lua_pushlightuserdata(L, &vptr);    
        lua_pushvalue(L, 1);
        lua_rawset(L, -3);
        //lua_pushvalue(L, 1);
        //lua_rawseti(L, -2, 1);

        lua_getref(L, LUA_RIDX_VPTR);       //stack: u p vt mt
        lua_setmetatable(L, -2);            //stack: u p vt

        lua_pushstring(L, "base");          //stack: u p vt "base"
        lua_pushvalue(L, -2);               //stack: u p vt "base" vt
        lua_rawset(L, 2);                   //stack: u p vt    
        lua_pop(L, 1);
    }

    return 0;
};

这个函数接收两个参数,第一个是userdata,表示C#类的对象,第二个是table,表示用来扩展该对象的lua数据结构。如果table为空,则userdata没有扩展table,那么就为它设置一个空的env table,这里TOLUA_NOPEER就是registry table。其实到这,我们应该就已经猜到,tolua是使用env table的方式,把lua的table绑定到userdata上,从而实现对userdata的扩展。

由于扩展后的userdata它的类型依旧是userdata,我们希望能够通过它灵活访问到扩展前和扩展后的内容,所以这里又引入了一个vptr的table。它用一个light userdata的key,绑定了对应的userdata,以及名为LUA_RIDX_VPTR的metatable。最后,我们设置env table的base域为这个vptr。

第11-12行就是获取tolua里绑定C# get/set属性的table,将其绑定到新的env table上。通过修改get/set table,就可以改变C# get/set属性的行为了。听上去似乎云里雾里,我们不如直接来看下执行过这些步骤后,访问C# userdata的逻辑会发生哪些改变。在例子中,我们重写了transform的postion属性,和translate方法,那么在lua层实际调用时,会触发到class_index_event函数中的如下逻辑:

lua_getfenv(L,1);

if (!lua_rawequal(L, -1, TOLUA_NOPEER))     // stack: t k env
{
    while (lua_istable(L, -1))                       // stack: t k v mt 
    {      
        lua_pushvalue(L, 2); 
        lua_rawget(L, -2);
    
        if (!lua_isnil(L, -1))
        {                    
            return 1;
        }

        lua_pop(L, 1);
        lua_pushlightuserdata(L, &gettag);          
        lua_rawget(L, -2);                      //stack: obj key env tget
    
        if (lua_istable(L, -1))
        {                    
            lua_pushvalue(L, 2);                //stack: obj key env tget key
            lua_rawget(L, -2);                  //stack: obj key env tget func 

            if (lua_isfunction(L, -1))
            {                        
                lua_pushvalue(L, 1);
                lua_call(L, 1, 1);
                return 1;
            }    

            lua_pop(L, 1);                 
        }

        lua_pop(L, 1); 

        if (lua_getmetatable(L, -1) == 0)               // stack: t k v mt mt
        {
            lua_pushnil(L);
        }

        lua_remove(L, -2);                              // stack: t k v mt
    }        
}

如果userdata上绑定的env table是有效table,那么优先访问该table,这很好理解,毕竟子类如果重写了父类的方法,调用时应当优先调用子类的实现。对于translate方法,就是直接去env table中获取key为translate的value;对于position属性,就是走get属性的table,获取到对应的value,它应该是一个function,调用之后返回的值为get返回的属性。当然,如果访问的是子类没有重写过的父类方法,则这里的判断都不会命中,会进而获取userdata的metatable,也就是前面我们所说过的class table,进行递归查找。

在子类的实现中,我们往往需要直接调用父类的方法。tolua这里类似,可以通过访问userdata的base域,由上文可知,这里返回的会是一个vptr的table。对它访问会触发它的metatable的__index元方法,也就是vptr_index_event函数:

static int vptr_index_event(lua_State *L)
{    
    lua_pushlightuserdata(L, &vptr);
    lua_rawget(L, 1);                                   // stack: t key u
    lua_replace(L, 1);                                  // stack: u key
    lua_pushvalue(L, 1);                                // stack: u key u

    while (lua_getmetatable(L, -1) != 0)
    {
        lua_remove(L, -2);                              // stack: u key mt
        lua_pushvalue(L, 2);                            // stack: u key mt key
        lua_rawget(L, -2);                              // stack: u key mt value        

        if (!lua_isnil(L, -1))
        {
            return 1;
        }
        
        lua_pop(L, 1);        
        lua_pushlightuserdata(L, &gettag);          
        lua_rawget(L, -2);                              //stack: u key mt tget

        if (lua_istable(L, -1))
        {
            lua_pushvalue(L, 2);                        //stack: obj key mt tget key
            lua_rawget(L, -2);                          //stack: obj key mt tget value 

            if (lua_isfunction(L, -1))
            {
                lua_pushvalue(L, 1);
                lua_call(L, 1, 1);
                return 1;
            }
        }

        lua_settop(L, 3);
    }
}

访问vptr table的效果其实应当于直接访问扩展之前的userdata效果一样。所以这里有一个比较trick的实现,就是将vptr table中绑定过的userdata取出,再对userdata进行访问,即对它的metatable进行递归查找。之所以对所有的vptr table都设置同样一个这样的metatable,其原因是它们的逻辑是完全相同的,唯一不同就是各自绑定的userdata不同,因此只需要在metatable的元方法中,把vptr table绑定的userdata传进去就行了,没必要为每个vptr table都设置一个单独的metatable。这里的实现是比较巧妙的。

在例子中,我们还看到对扩展后的userdata进行判等的操作。由于扩展后的userdata依旧是userdata,它自身的value其实是不变的,所以这里判等一定是成立的。例子的最后展示了对userdata设置各种value的操作,如果是扩展前的C#类userdata,这里就会报错了,因为事先注册的set table中没有这些key。而扩展后就不一样了,这里的各种set,最终会往env table中进行set,而env table只是一个普通的lua table,当然就没有问题了。可以看到,对userdata进行扩展,能够极大地提高它使用的灵活性。

下一节我们将讨论lua层如何使用反射调用没有wrap过的C#函数。

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

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