Cocos2dx 中 userdata 关联 Peer 表的实现

本贴最后更新于 820 天前,其中的信息可能已经事过景迁

起因

在 log 里面看到一条报错,大概情况如下,在 socket 回调里面报了一条 attempt to call a nil value(大意)。然后类结构大概是这样的:

local MessageLayer = class("MessageLayer", CC.Layer)

function MessageLayer:showDisappearEffect()
-- do something
end

local a = class("layer", MessageLayer)

local scene = cc.Director:getInstance():getRunningScene()

local instance = a:create()
instance:addTo(scene)

然后这个 instance 被移除的情况下,它上面的 luafunction 调不到了。但是这个 instance 本身还是存在的。所以我们关注的问题是:在 lua 里,对于一个 userData 的实例,当 C++ 对象析构了之后,为什么它上面的 lua 方法会失效

尝试解决

打印了一下 userdata 的原表,大概这样写:

function ThemeScene:onEnter( )
  
    for k,v in pairs(getmetatable(self)) do
        print(k,v)
    end
end

打印出的结果和我想的不太一样。

[LUA-print] getDefaultCamera	function: 0x133752a0
[LUA-print] __index	function: 0x13371448
[LUA-print] __newindex	function: 0x13371480
[LUA-print] __gc	function: 0x1336a220
[LUA-print] __eq	function: 0x1336bad8
[LUA-print] __lt	function: 0x1336bbb0
[LUA-print] __le	function: 0x1336bbe8
[LUA-print] getNavMesh	function: 0x13498df0
[LUA-print] __call	function: 0x13374888
[LUA-print] __add	function: 0x1336baa0
[LUA-print] __sub	function: 0x13371410
[LUA-print] __mul	function: 0x1336bb40
[LUA-print] __div	function: 0x1336bb78
[LUA-print] setNavMesh	function: 0x13498d88
[LUA-print] tolua_ubox	table: 0x1336c028
[LUA-print] setNavMeshDebugCamera	function: 0x13498d20
[LUA-print] setPhysics3DDebugCamera	function: 0x13498cb0
[LUA-print] getPhysics3DWorld	function: 0x13498c40
[LUA-print] createWithPhysics	function: 0x133753b0
[LUA-print] create	function: 0x13375340
[LUA-print] .classname	cc.Scene
[LUA-print] createWithSize	function: 0x13375308
[LUA-print] onProjectionChanged	function: 0x13374a00
[LUA-print] initWithPhysics	function: 0x1336e0c0
[LUA-print] setCameraOrderDirty	function: 0x13374e90
[LUA-print] render	function: 0x13374ef0
[LUA-print] stepPhysicsAndNavigation	function: 0x13374990
[LUA-print] new	function: 0x1336df70
[LUA-print] getPhysicsWorld	function: 0x13374a68
[LUA-print] initWithSize	function: 0x13375230

明显可以看出来,基本上所有的东西都是 c++ 里的方法,也就是 userdata 里的方法,那么我们自己定义的一些 lua 方法去哪里了呢?

前置知识

其实方向一开始就错了。本身 userdata 上面其实不应该直接绑定任何 lua 的东西的,正确的做法应该是套一层,然后在套娃的那个 luatable 里来做事,这个套娃一般称之为 peer

c++_obj 的 metatable 的__index 指向一个 c 函数,当访问 c++_obj 中的一个域的时候,会调用这个 c 函数,这个 c 函数会去查找各个关联表,来取得我们要访问的域,这其中就包括对 peer 表的查询。

具体解决

另外,在 functions.lua 里其实也可以找到证据
先看下 class 方法的完整代码

function class(classname, ...)
    local cls = {__cname = classname}

    local supers = {...}
    for _, super in ipairs(supers) do
        local superType = type(super)
        assert(superType == "nil" or superType == "table" or superType == "function",
            string.format("class() - create class \"%s\" with invalid super class type \"%s\"",
                classname, superType))

        if superType == "function" then
            assert(cls.__create == nil,
                string.format("class() - create class \"%s\" with more than one creating function",
                    classname));
            -- if super is function, set it to __create
            cls.__create = super
        elseif superType == "table" then
            if super[".isclass"] then
                -- super is native class
                assert(cls.__create == nil,
                    string.format("class() - create class \"%s\" with more than one creating function or native class",
                        classname));
                cls.__create = function() return super:create() end
            else
                -- super is pure lua class
                cls.__supers = cls.__supers or {}
                cls.__supers[#cls.__supers + 1] = super
                if not cls.super then
                    -- set first super pure lua class as class.super
                    cls.super = super
                end
            end
        else
            error(string.format("class() - create class \"%s\" with invalid super type",
                        classname), 0)
        end
    end

    cls.__index = cls
    if not cls.__supers or #cls.__supers == 1 then
        setmetatable(cls, {__index = cls.super})
    else
        setmetatable(cls, {__index = function(_, key)
            local supers = cls.__supers
            for i = 1, #supers do
                local super = supers[i]
                if super[key] then return super[key] end
            end
        end})
    end

    if not cls.ctor then
        -- add default constructor
        cls.ctor = function() end
    end
    cls.new = function(...)
        local instance
        if cls.__create then
            instance = cls.__create(...)
        else
            instance = {}
        end
        setmetatableindex(instance, cls)
        instance.class = cls
        instance:ctor(...)
        return instance
    end
    cls.create = function(_, ...)
        return cls.new(...)
    end

    return cls
end

关键关注 setmetatableindex 这里。

他的定义是这样的:

local setmetatableindex_
setmetatableindex_ = function(t, index)
    if type(t) == "userdata" then
        local peer = tolua.getpeer(t)
        if not peer then
            peer = {}
            tolua.setpeer(t, peer)
        end
        setmetatableindex_(peer, index)
    else
        local mt = getmetatable(t)
        if not mt then mt = {} end
        if not mt.__index then
            mt.__index = index
            setmetatable(t, mt)
        elseif mt.__index ~= index then
            setmetatableindex_(mt, index)
        end
    end
end
setmetatableindex = setmetatableindex_

其实看到这里就很明显了,所有对 self 所做的操作其实都是放在这个 peer 表中的。

首先简单些一个 lua 类,继承自 cc.Layer,并且自己在 lua 里面定义一些方法

local testLayer = class("layerClass",cc.Layer)

function testLayer:testFunc()
	print("testFunc")
end

然后我们将他实例化:

...
local layer = testLayer:create()

这个过程发生的事情,可以对照 class 方法里的东西

if not cls.ctor then
        -- add default constructor
        cls.ctor = function() end
    end
    cls.new = function(...)
        local instance
        if cls.__create then
            instance = cls.__create(...)
        else
            instance = {}
        end
        setmetatableindex(instance, cls)
        instance.class = cls
        instance:ctor(...)
        return instance
    end
    cls.create = function(_, ...)
        return cls.new(...)
    end

重点在于这个 new 方法里,如果他有__create 方法,那么就用__create 来创建实例。在此例中,便是 cc.Layer 的 create 方法。它返回了一个 layer 的 userData 实例。

接下来的关键,是这个 setmetatableindex,这是一个针对 userdata 重新定义的一个方法。看下它的实现

setmetatableindex_ = function(t, index)
    if type(t) == "userdata" then
        local peer = tolua.getpeer(t)
        if not peer then
            peer = {}
            tolua.setpeer(t, peer)
        end
        setmetatableindex_(peer, index)
    else
        local mt = getmetatable(t)
        if not mt then mt = {} end
        if not mt.__index then
            mt.__index = index
            setmetatable(t, mt)
        elseif mt.__index ~= index then
            setmetatableindex_(mt, index)
        end
    end
end
setmetatableindex = setmetatableindex_

也很好懂,首先如果是要对一个 userdata 设置 metatable,不可以直接设置,而是去创建一个这个 userdata 的 peer 表。这个 peer 表可以看作是一个纯粹的 luatable,之后所有的操作都是在这个 peer 表上进行的。对 userdata 做索引,不但可以索引到它自己导出的 C++ 方法,也可以索引到这个 peer 表。那么,对于我们上面例子所定义的 testLayer:testFunc 它其实就是关联到了 peer 表上。

那么可以有一个猜想,当 C++ 对象析构的时候,是不是这个 peer 表被清掉了导致找不到 lua 方法呢?
简单写个例子佐证一下。

local node = cc.Node:create()
    node:addTo(self)

    node._val = 1000

    print("<<<<<<<<<<<<<<")
    print("node._peer",tolua.getpeer(node))
    print("node._val:",node._val)
    print("<<<<<<<<<<<<<<")

    self:delayCall(0.01,function()
        node:removeFromParent()

        print("===========")
        print("node._peer",tolua.getpeer(node))
        print("node._val:",node._val)
        print("===========")
    end)

之后的输出验证了这点:

[LUA-print] <<<<<<<<<<<<<<
[LUA-print] node._peer	table: 0x1fcb8e50
[LUA-print] node._val:	1000
[LUA-print] <<<<<<<<<<<<<<
[LUA-print] ===========
[LUA-print] node._peer	nil
[LUA-print] node._val:	nil
[LUA-print] ===========

C++ 内部实现

清理的过程

那么当一个 CCRef removeFromParent 之后,为什么 peer 表会被清除呢。

Ref::~Ref()
{

    ScriptEngineProtocol* pEngine = ScriptEngineManager::getInstance()->getScriptEngine();
    if (pEngine != nullptr && _luaID)
    {
        // if the object is referenced by Lua engine, remove it
        pEngine->removeScriptObjectByObject(this);
    }
...

luaEngine 里的代码

void LuaEngine::removeScriptObjectByObject(Ref* pObj)
{
    _stack->removeScriptObjectByObject(pObj);
    ScriptHandlerMgr::getInstance()->removeObjectAllHandlers(pObj);
}

关于 scripthandler 的东西我们不关心,我们关心的其实是这一句
_stack->removeScriptObjectByObject(pObj);

顺着继续看下去

void LuaStack::removeScriptObjectByObject(Ref* pObj)
{
    toluafix_remove_ccobject_by_refid(_state, pObj->_luaID);
}

再继续

TOLUA_API int toluafix_remove_ccobject_by_refid(lua_State* L, int refid)
{
	void* ptr = NULL;
    const char* type = NULL;
    void** ud = NULL;
    if (refid == 0) return -1;

    // get ptr from tolua_refid_ptr_mapping
    lua_pushstring(L, TOLUA_REFID_PTR_MAPPING);
    lua_rawget(L, LUA_REGISTRYINDEX);                               /* stack: refid_ptr */
    lua_pushinteger(L, refid);                                      /* stack: refid_ptr refid */
    lua_rawget(L, -2);                                              /* stack: refid_ptr ptr */
    ptr = lua_touserdata(L, -1);
    lua_pop(L, 1);                                                  /* stack: refid_ptr */
    if (ptr == NULL)
    {
        lua_pop(L, 1);
        // Lua stack has closed, C++ object not in Lua.
        // printf("[LUA ERROR] remove CCObject with NULL ptr, refid: %d\n", refid);
        return -2;
    }

    // remove ptr from tolua_refid_ptr_mapping
    lua_pushinteger(L, refid);                                      /* stack: refid_ptr refid */
    lua_pushnil(L);                                                 /* stack: refid_ptr refid nil */
    lua_rawset(L, -3);                     /* delete refid_ptr[refid], stack: refid_ptr */
    lua_pop(L, 1);                                                  /* stack: - */


    // get type from tolua_refid_type_mapping
    lua_pushstring(L, TOLUA_REFID_TYPE_MAPPING);
    lua_rawget(L, LUA_REGISTRYINDEX);                               /* stack: refid_type */
    lua_pushinteger(L, refid);                                      /* stack: refid_type refid */
    lua_rawget(L, -2);                                              /* stack: refid_type type */
    if (lua_isnil(L, -1))
    {
        lua_pop(L, 2);
        printf("[LUA ERROR] remove CCObject with NULL type, refid: %d, ptr: %p\n", refid, ptr);
        return -1;
    }

    type = lua_tostring(L, -1);
    lua_pop(L, 1);                                                  /* stack: refid_type */

    // remove type from tolua_refid_type_mapping
    lua_pushinteger(L, refid);                                      /* stack: refid_type refid */
    lua_pushnil(L);                                                 /* stack: refid_type refid nil */
    lua_rawset(L, -3);                    /* delete refid_type[refid], stack: refid_type */
    lua_pop(L, 1);                                                  /* stack: - */

    // get ubox
    luaL_getmetatable(L, type);                                     /* stack: mt */
    lua_pushstring(L, "tolua_ubox");                                /* stack: mt key */
    lua_rawget(L, -2);                                              /* stack: mt ubox */
    if (lua_isnil(L, -1))
    {
        // use global ubox
        lua_pop(L, 1);                                              /* stack: mt */
        lua_pushstring(L, "tolua_ubox");                            /* stack: mt key */
        lua_rawget(L, LUA_REGISTRYINDEX);                           /* stack: mt ubox */
    };
  
  
    // cleanup root
    tolua_remove_value_from_root(L, ptr);

    lua_pushlightuserdata(L, ptr);                                  /* stack: mt ubox ptr */
    lua_rawget(L,-2);                                               /* stack: mt ubox ud */
    if (lua_isnil(L, -1))
    {
        // Lua object has released (GC), C++ object not in ubox.
        //printf("[LUA ERROR] remove CCObject with NULL ubox, refid: %d, ptr: %x, type: %s\n", refid, (int)ptr, type);
        lua_pop(L, 3);
        return -3;
    }

    // cleanup peertable
    lua_pushvalue(L, LUA_REGISTRYINDEX);
    lua_setfenv(L, -2);

    ud = (void**)lua_touserdata(L, -1);
    lua_pop(L, 1);                                                  /* stack: mt ubox */
    if (ud == NULL)
    {
        printf("[LUA ERROR] remove CCObject with NULL userdata, refid: %d, ptr: %p, type: %s\n", refid, ptr, type);
        lua_pop(L, 2);
        return -1;
    }

    // clean userdata
    *ud = NULL;

    lua_pushlightuserdata(L, ptr);                                  /* stack: mt ubox ptr */
    lua_pushnil(L);                                                 /* stack: mt ubox ptr nil */
    lua_rawset(L, -3);                             /* ubox[ptr] = nil, stack: mt ubox */

    lua_pop(L, 2);
    //printf("[LUA] remove CCObject, refid: %d, ptr: %x, type: %s\n", refid, (int)ptr, type);
    return 0;
}

这里清除了好几个东西,我们一个个来看。对 lua 栈操作不熟悉的可以参考这个参考文档

如果需要看 5.1 的参考,参考这个文档

首先明确的是,这个 refid 是 CCRef 里的 luaid。
先看下第一部分

// get ptr from tolua_refid_ptr_mapping
    lua_pushstring(L, TOLUA_REFID_PTR_MAPPING);
    lua_rawget(L, LUA_REGISTRYINDEX);                               /* stack: refid_ptr */
    lua_pushinteger(L, refid);                                      /* stack: refid_ptr refid */
    lua_rawget(L, -2);                                              /* stack: refid_ptr ptr */
    ptr = lua_touserdata(L, -1);
    lua_pop(L, 1);                                                  /* stack: refid_ptr */
    if (ptr == NULL)
    {
        lua_pop(L, 1);
        // Lua stack has closed, C++ object not in Lua.
        // printf("[LUA ERROR] remove CCObject with NULL ptr, refid: %d\n", refid);
        return -2;
    }

tolua 在 lua5.1 的 binding 里面有一个重要的概念就是这个 LUA_REGISTRYINDEX 为 index 的全局注册表。我们姑且把这个表称之为为 Reg,暂时把他理解为一个 luatable。

通过阅读可以知道,他其实是拿出 Reg[TOLUA_REFID_PTR_MAPPING][refid]存到 ptr 这个变量里。

接下来,它清除了 Reg[TOLUA_REFID_PTR_MAPPING][refid],也就是相当于调用了 Reg[TOLUA_REFID_PTR_MAPPING][refid] = nil

// remove ptr from tolua_refid_ptr_mapping
    lua_pushinteger(L, refid);                                      /* stack: refid_ptr refid */
    lua_pushnil(L);                                                 /* stack: refid_ptr refid nil */
    lua_rawset(L, -3);                     /* delete refid_ptr[refid], stack: refid_ptr */
    lua_pop(L, 1);                                                  /* stack: - */

接下来的代码同理,只是换了一个表清.这次取得是 Reg[``TOLUA_REFID_TYPE_MAPPING ][ refid`],里面存着一个 string.

// get type from tolua_refid_type_mapping
    lua_pushstring(L, TOLUA_REFID_TYPE_MAPPING);
    lua_rawget(L, LUA_REGISTRYINDEX);                               /* stack: refid_type */
    lua_pushinteger(L, refid);                                      /* stack: refid_type refid */
    lua_rawget(L, -2);                                              /* stack: refid_type type */
    if (lua_isnil(L, -1))
    {
        lua_pop(L, 2);
        printf("[LUA ERROR] remove CCObject with NULL type, refid: %d, ptr: %p\n", refid, ptr);
        return -1;
    }

    type = lua_tostring(L, -1);
    lua_pop(L, 1);                                                  /* stack: refid_type */

    // remove type from tolua_refid_type_mapping
    lua_pushinteger(L, refid);                                      /* stack: refid_type refid */
    lua_pushnil(L);                                                 /* stack: refid_type refid nil */
    lua_rawset(L, -3);                    /* delete refid_type[refid], stack: refid_type */
    lua_pop(L, 1);                                                  /* stack: - */

接下来是拿出 ubox 表。

这个 ubox 表有两种存的方法,有可能存在 Reg[type]["tolua_ubox"]里,也有可能直接存在 Reg[“tolua_ubox”]。如果前者不存在,那么就拿后者。

// get ubox
    luaL_getmetatable(L, type);                                     /* stack: mt */
    lua_pushstring(L, "tolua_ubox");                                /* stack: mt key */
    lua_rawget(L, -2);                                              /* stack: mt ubox */
    if (lua_isnil(L, -1))
    {
        // use global ubox
        lua_pop(L, 1);                                              /* stack: mt */
        lua_pushstring(L, "tolua_ubox");                            /* stack: mt key */
        lua_rawget(L, LUA_REGISTRYINDEX);                           /* stack: mt ubox */
    };

之后,拿着这个 ubox 表,清除 tolua_value_from_root 这个表,也就是 tolua_remove_value_from_root 方法里写的。具体如下

tolua_remove_value_from_root(L, ptr);

具体定义
其实只干了一件事,那就是执行了 Reg[TOLUA_VALUE_ROOT][prt]=nil,也就是把 value_root 表里的 ptr 对应的项置空了。

TOLUA_API void tolua_remove_value_from_root (lua_State* L, void* ptr)
{
    lua_pushstring(L, TOLUA_VALUE_ROOT);
    lua_rawget(L, LUA_REGISTRYINDEX);                               /* stack: root */
    lua_pushlightuserdata(L, ptr);                                  /* stack: root ptr */
  
    lua_pushnil(L);                                                 /* stack: root ptr nil */
    lua_rawset(L, -3);                                              /* root[ptr] = nil, stack: root */
    lua_pop(L, 1);
}

接下来,就要在 ubox 表里取出来我们 ptr 所对应的值

lua_pushlightuserdata(L, ptr);                                  /* stack: mt ubox ptr */
    lua_rawget(L,-2);                                               /* stack: mt ubox ubox[ptr] */
    if (lua_isnil(L, -1))
    {
        // Lua object has released (GC), C++ object not in ubox.
        //printf("[LUA ERROR] remove CCObject with NULL ubox, refid: %d, ptr: %x, type: %s\n", refid, (int)ptr, type);
        lua_pop(L, 3);
        return -3;
    }

接下来就是我们的重点,peer 表。此时我们的栈顶是 reg ubox ubox[ptr]
这里先把 lua 的全局注册表 push 进站,然后把他作为 ubox[ptr]的新的 lua_env

// cleanup peertable
    lua_pushvalue(L, LUA_REGISTRYINDEX);
    lua_setfenv(L, -2);

然后在注册表的 env 之下,把顶层指向 userdata 的指针,也就是 ubox[ptr]取出来,把它所指向的 userdata 置空。

ud = (void**)lua_touserdata(L, -1);
    lua_pop(L, 1);                                                  /* stack: mt ubox */
    if (ud == NULL)
    {
        printf("[LUA ERROR] remove CCObject with NULL userdata, refid: %d, ptr: %p, type: %s\n", refid, ptr, type);
        lua_pop(L, 2);
        return -1;
    }

    // clean userdata
    *ud = NULL;

最后,再把 ubox 里以 ptr 为 key 的值置空。ubox[ptr]=nil

lua_pushlightuserdata(L, ptr);                                  /* stack: mt ubox ptr */
    lua_pushnil(L);                                                 /* stack: mt ubox ptr nil */
    lua_rawset(L, -3);                             /* ubox[ptr] = nil, stack: mt ubox */

至此,整个清理过程完成。

创建绑定的过程

经过上面代码的阅读我们明白了 lua 部分是如何清理的,对需要清理哪些表有了一个大概的了解。但是我们还需要知道一件事情:那就是我们的 userdata 是怎么调到 peer 表的。

那么相应的,我们需要看一下在绑定过程中,userdata 到底需要绑定哪些东西。

从开始注册绑定开始

其实观察下 lua 的注册过程,会发现这个东西:

TOLUA_API void tolua_usertype (lua_State* L, const char* type)
{
    char ctype[128] = "const ";
    strncat(ctype,type,120);

    /* create both metatables */
    if (tolua_newmetatable(L,ctype) && tolua_newmetatable(L,type))
        mapsuper(L,type,ctype);             /* 'type' is also a 'const type' */
}

然后重点是 tolua_newmetatable

static int tolua_newmetatable (lua_State* L, const char* name)
{
    int r = luaL_newmetatable(L,name);

#ifdef LUA_VERSION_NUM /* only lua 5.1 */
    if (r) {
        lua_pushvalue(L, -1);
        lua_pushstring(L, name);
        lua_settable(L, LUA_REGISTRYINDEX); /* reg[mt] = type_name */
    };
#endif

    if (r)
        tolua_classevents(L); /* set meta events */
  
    // metatable[".classname"] = name
    lua_pushliteral(L, ".classname");   // stack: metatable ".classname"
    lua_pushstring(L, name);            // stack: metatable ".classname" name
    lua_rawset(L, -3);                  // stack: metatable
  
    lua_pop(L,1);
    return r;
}

其中的 tolua_classevents

TOLUA_API void tolua_classevents (lua_State* L)
{
    lua_pushstring(L,"__index");
    lua_pushcfunction(L,class_index_event);
    lua_rawset(L,-3);
    lua_pushstring(L,"__newindex");
    lua_pushcfunction(L,class_newindex_event);
    lua_rawset(L,-3);

    lua_pushstring(L,"__add");
    lua_pushcfunction(L,class_add_event);
    lua_rawset(L,-3);
    lua_pushstring(L,"__sub");
    lua_pushcfunction(L,class_sub_event);
    lua_rawset(L,-3);
    lua_pushstring(L,"__mul");
    lua_pushcfunction(L,class_mul_event);
    lua_rawset(L,-3);
    lua_pushstring(L,"__div");
    lua_pushcfunction(L,class_div_event);
    lua_rawset(L,-3);

    lua_pushstring(L,"__lt");
    lua_pushcfunction(L,class_lt_event);
    lua_rawset(L,-3);
    lua_pushstring(L,"__le");
    lua_pushcfunction(L,class_le_event);
    lua_rawset(L,-3);
    lua_pushstring(L,"__eq");
    lua_pushcfunction(L,class_eq_event);
    lua_rawset(L,-3);

    lua_pushstring(L,"__call");
    lua_pushcfunction(L,class_call_event);
    lua_rawset(L,-3);

    lua_pushstring(L,"__gc");
    lua_pushstring(L, "tolua_gc_event");
    lua_rawget(L, LUA_REGISTRYINDEX);
    /*lua_pushcfunction(L,class_gc_event);*/
    lua_rawset(L,-3);
}

是不是这里的内容就很熟悉了,正如我们正常操作原表来做继承的过程。只不过这里很多东西都会存到注册表里,比纯粹 lua 里面的继承要复杂很多。
那当然我们要看下熟悉的__index 方法注册的静态方法。

static int class_index_event (lua_State* L)
{
    int t = lua_type(L,1);
    if (t == LUA_TUSERDATA)
    {
        /* Access alternative table */
#ifdef LUA_VERSION_NUM /* new macro on version 5.1 */
        lua_getfenv(L,1);
        if (!lua_rawequal(L, -1, TOLUA_NOPEER)) {
            lua_pushvalue(L, 2); /* key */
            lua_gettable(L, -2); /* on lua 5.1, we trade the "tolua_peers" lookup for a gettable call */
            if (!lua_isnil(L, -1))
                return 1;
        };
#else
        lua_pushstring(L,"tolua_peers");
        lua_rawget(L,LUA_REGISTRYINDEX);        /* stack: obj key ubox */
        lua_pushvalue(L,1);
        lua_rawget(L,-2);                       /* stack: obj key ubox ubox[u] */
        if (lua_istable(L,-1))
        {
            lua_pushvalue(L,2);  /* key */
            lua_rawget(L,-2);                      /* stack: obj key ubox ubox[u] value */
            if (!lua_isnil(L,-1))
                return 1;
        }
#endif
        lua_settop(L,2);                        /* stack: obj key */
        /* Try metatables */
        lua_pushvalue(L,1);                     /* stack: obj key obj */
        while (lua_getmetatable(L,-1))
        {   /* stack: obj key obj mt */
            lua_remove(L,-2);                      /* stack: obj key mt */
            if (lua_isnumber(L,2))                 /* check if key is a numeric value */
            {
                /* try operator[] */
                lua_pushstring(L,".geti");
                lua_rawget(L,-2);                      /* stack: obj key mt func */
                if (lua_isfunction(L,-1))
                {
                    lua_pushvalue(L,1);
                    lua_pushvalue(L,2);
                    lua_call(L,2,1);
                    return 1;
                }
            }
            else
            {
                lua_pushvalue(L,2);                    /* stack: obj key mt key */
                lua_rawget(L,-2);                      /* stack: obj key mt value */
                if (!lua_isnil(L,-1))
                    return 1;
                else
                    lua_pop(L,1);
                /* try C/C++ variable */
                lua_pushstring(L,".get");
                lua_rawget(L,-2);                      /* stack: obj key mt tget */
                if (lua_istable(L,-1))
                {
                    lua_pushvalue(L,2);
                    lua_rawget(L,-2);                      /* stack: obj key mt value */
                    if (lua_iscfunction(L,-1))
                    {
                        lua_pushvalue(L,1);
                        lua_pushvalue(L,2);
                        lua_call(L,2,1);
                        return 1;
                    }
                    else if (lua_istable(L,-1))
                    {
                        /* deal with array: create table to be returned and cache it in ubox */
                        void* u = *((void**)lua_touserdata(L,1));
                        lua_newtable(L);                /* stack: obj key mt value table */
                        lua_pushstring(L,".self");
                        lua_pushlightuserdata(L,u);
                        lua_rawset(L,-3);               /* store usertype in ".self" */
                        lua_insert(L,-2);               /* stack: obj key mt table value */
                        lua_setmetatable(L,-2);         /* set stored value as metatable */
                        lua_pushvalue(L,-1);            /* stack: obj key met table table */
                        lua_pushvalue(L,2);             /* stack: obj key mt table table key */
                        lua_insert(L,-2);               /*  stack: obj key mt table key table */
                        storeatubox(L,1);               /* stack: obj key mt table */
                        return 1;
                    }
                }
            }
            lua_settop(L,3);
        }
        lua_pushnil(L);
        return 1;
    }
    else if (t== LUA_TTABLE)
    {
        lua_pushvalue(L,1);
        class_table_get_index(L);
        return 1;
    }
    lua_pushnil(L);
    return 1;
}

不要被长段代码吓到,其实我们关注的只有这些代码

if (t == LUA_TUSERDATA)
    {
        /* Access alternative table */
#ifdef LUA_VERSION_NUM /* new macro on version 5.1 */
        lua_getfenv(L,1);
        if (!lua_rawequal(L, -1, TOLUA_NOPEER)) {
            lua_pushvalue(L, 2); /* key */
            lua_gettable(L, -2); /* on lua 5.1, we trade the "tolua_peers" lookup for a gettable call */
            if (!lua_isnil(L, -1))
                return 1;
        };

其实这里就很明显了,就是拿下这个 userdata 对应的 env 表。然后如果没有设置过 peer 表,此时的 env 就是 TOLUA_NOPEER,TOLUA_NOPEER 其实就是 LUA_REGISTRYINDEX

#define TOLUA_NOPEER    LUA_REGISTRYINDEX /* for lua 5.1 */

然后在这个 env 表中索引。注意,lua_gettable 是会触发元方法的,比如__index。这也就是为什么我们会在前面的 lua 代码中看到我们的 peer 上不会直接设置属性,而是给他设置原表通过__index 触发。

同样的,__newindex 也是同样的套路,这里就不展开说明了,读者可以自行考究。

tolua 中的 peer 函数

最后,为了证明我们的想法,我们看下 tolua 中的 getpeer 和 setpeer,来佐证我们的想法。

#ifdef LUA_VERSION_NUM /* lua 5.1 */
        tolua_function(L, "setpeer", tolua_bnd_setpeer);
        tolua_function(L, "getpeer", tolua_bnd_getpeer);
#endif

setpeer 的实现

static int tolua_bnd_setpeer(lua_State* L) {

    /* stack: userdata, table */
    if (!lua_isuserdata(L, -2)) {
        lua_pushstring(L, "Invalid argument #1 to setpeer: userdata expected.");
        lua_error(L);
    };

    if (lua_isnil(L, -1)) {

        lua_pop(L, 1);
        lua_pushvalue(L, TOLUA_NOPEER);
    };
    lua_setfenv(L, -2);

    return 0;
};

getpeer 的实现

static int tolua_bnd_getpeer(lua_State* L) {

    /* stack: userdata */
    lua_getfenv(L, -1);
    if (lua_rawequal(L, -1, TOLUA_NOPEER)) {
        lua_pop(L, 1);
        lua_pushnil(L);
    };
    return 1;
};

实现与我们前面的观察基本一致,也是利用了 fenv 来造一个 peer_table。当然这只是 lua5.1 中的实现,lua 后续的版本已经不用这套实现了,但是基本思路也大同小异,读者可以自行考究。

没想到一个简单的问题引起了这么多看代码的过程。纸上得来终觉浅,绝知此事要躬行。如果只是浅尝辄止地看了下大概的实现,可能这次看代码经历也就没什么意义了。希望以后遇到了一个看起来简单或者直觉型的问题能够有时间,也有心思去细细看一遍整个代码的流程。带着问题多看源码永远都是一个好处理方法。希望今后遇到问题能多看看源码,有时候也没有想象中那么复杂。

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...