代码之家  ›  专栏  ›  技术社区  ›  user1054922

对象方法和字段在Lua库中共存

lua
  •  0
  • user1054922  · 技术社区  · 4 年前

    编辑:事实证明,这在Lua中是不可能的,因为它有_索引方法和类实例方法之类的方法。非此即彼。

    试图让我的Lua接口在同时支持字段和实例方法的地方工作。似乎通过操纵初始化,我只能让函数(_f)或方法(_m)工作,而不是两者都工作。

    我觉得这很简单,我只是错过了。

    如何初始化库:

    void PushUserdata(const void *data, const char *metatable)
    {
        const void **wrapped_ptr = (const void**)lua_newuserdata(l, sizeof(const void*));
        *wrapped_ptr = data;
        luaL_getmetatable(l, metatable);
        lua_setmetatable(l, -2);
    }
    
    static int player_head(lua_State *L)
    {
        if (!Player::playerHead)
            lua_pushnil(L);
        else
            PushUserdata(Player::playerHead, "player");
    
        return 1;
    }
    
    static int playerget(lua_State *L)
    {
        Player *player = *CHECKPLAYER(L, 1); // Get double pointer and dereference to get real pointer
    
        const char *field = luaL_checkstring(L, 2);
    
        if (!strcmp(field, "next"))
        {
            if (!player->next)
                lua_pushnil(L);
            else
                PushUserdata(player->next, "player");
        }
        else if (!strcmp(field, "prev"))
        {
            if (!player->prev)
                lua_pushnil(L);
            else
                PushUserdata(player->prev, "player");
        }
        else if (!strcmp(field, "obj"))
        {
            if (!player->obj)
                lua_pushnil(L);
            else
                PushUserdata(player->obj, "wobj");
        }
        else if (!strcmp(field, "AddCollisionObjHook")) // This ends up here if __index is in the table below...
        {
        }
        else
            return 0;
    
        return 1;
    }
    
    static const struct luaL_Reg playerlib_f[] = {
        {"head", player_head},
        {"AddPreThinker", AddPreThinker},
        {"AddPostThinker", AddPostThinker},
        {NULL, NULL}
    };
    
    static const struct luaL_Reg playerlib_m[] = {
        {"__tostring", player2string},
        {"__index", playerget},
        {"__newindex", playerset},
        {"AddCollisionObjHook", AddCollisionObjHook},
        {NULL, NULL}
    };
    
    int Lua_PlayerLib(lua_State *L)
    {
        luaL_newmetatable(L, "player");
        lua_pushvalue(L, -1); // duplicates the metatable...but why?
        luaL_setfuncs(L, playerlib_m, 0);
        luaL_newlib(L, playerlib_f, 0);
        lua_setglobal(L, "player");
    
        return 1;
    }
    

    Lua脚本:

    me = playerlib.head()
    me:AddCollisionObjHook(playerHitObj)
    

    错误消息:

    Warning: [string "postload.lua"]: attempt to call method 'AddCollisionObjHook' (a nil value)
    

    “me”绝对是一个有效的非零值。

    0 回复  |  直到 4 年前
        1
  •  1
  •   ToxicFrog    4 年前

    你想做什么 有可能,但不是以你想要的方式。

    我认为有必要回顾一下方法调用和元表/元方法是如何工作的,以及您编写的代码实际上在做什么。tl;博士是:

    • 方法调用只是普通的字段查找
    • 元表及其包含的元方法是 运算符重载 ,而不是方法定义
    • 如果要为userdata实现此功能,则需要 __index 可以处理字段和方法查找的元方法

    首先,Lua在“方法”和“字段”之间没有内在的区别。在组织代码时,您可能会发现区分这两种语言很方便,但就Lua语言而言 方法和字段是一样的 方法只是一个字段,其中键是有效的lua标识符,值是函数。

    所以,当你写下 me:AddCollisionObjHook(playerHitObj) ,实际发生的事情是这样的:

    local self = me
    local method = self["AddCollisionObjHook"]
    method(self, playerHitObj)
    

    (关于这一点,请注意两点:

    • 没有真正的新本地人被创造出来;这一切都发生在Lua解释器的内部。
    • self["AddCollisionObjHook"] self.AddCollisionObjHook 是两种书写同一事物的方式;后者只是前者的捷径。)

    那么,这是怎么回事 self[“AddCollisionObjHook”] 查找工作?与任何其他字段查找的工作方式相同。Lua手册对此进行了详细介绍,包括伪代码,但与您的代码相关的部分是:

    -- We're looking up self[key] but self is userdata, not table
    local mt = getmetatable(self)
    if mt and type(mt.__index) == 'function' then
      -- user provided an __index function
      return mt.__index(self, key)
    elseif mt and mt.__index ~= nil then
      -- user provided an __index table (or table-like object)
      -- retry the lookup using it
      return mt.__index[key]
    else
      -- no metatable, or metatable lacks __index metamethod
      error(...) -- don't know how to do field lookup on this type!
    end
    

    注意 在这一过程中,除了 __索引 在元表中查找 .元表存在 只有 告诉Lua如何为通常没有运算符的类型实现运算符;在本例中,字段查找(“索引”)运算符( [] ,以及它的别名 . : )对于特定类型的用户数据。这完全取决于 __索引 它本身可以处理将字段名转换为值的实际过程,可以是一个可以重试查找的表,也可以是一个可以返回相关值的函数。

    因此,这就为我们提供了如何支持(可设置)字段和(可调用)方法的答案:

    • __newindex 需要了解如何设置字段
    • __索引 需要了解如何返回 二者都 字段值 方法实现

    因为,从Lua的角度来看,字段查找和方法查找都是相同的操作,因此 __索引 两者都习惯了。


    有鉴于此,我们应该如何组织代码来支持这两个方面,以及如何重新构造代码以使其工作?有很多方法可以做到这一点,不过为了回答这个问题,我将做一些假设:

    • 领域 全部存储在C端,没有相应的数据需要在Lua中管理
    • 方法 不能被Lua代码覆盖
    • 元方法 与单独存储 实例方法

    最后一个不是绝对必要的;事实上,在同一个表中同时存储元方法和实例方法是很常见的(我通常自己做)。然而,我认为这也会让Lua新手对他们之间的区别产生困惑,所以为了让代码尽可能清晰,我在这个答案中将他们分开。


    考虑到这一点,让我们重新编写设置代码。我看过你的编辑,试图重建你最初的想法。

    
    static int playerget(lua_State *L)
    {
        Player *player = *CHECKPLAYER(L, 1);
        const char *field = luaL_checkstring(L, 2);
    
        // Check if it's a method, by getting the method table
        // and then seeing if the key exists in it.
        // This code can be re-used (or factored out into its own function)
        // at the start of playerset() to raise an error if the lua code tries
        // to overwrite a method.
        lua_getfield(L, LUA_REGISTRYINDEX, "player-methods");
        lua_getfield(L, -1, field);
        if (!lua_isnil(L, -1)) {
          // Lookup in methods table successful, so return the method impl, which
          // is now on top of the stack
          return 1;
        } else {
          // No method, so clean up the stack of both the nil value and the
          // table of methods we got it from.
          lua_pop(L, 2);
        }
    
    
        if (!strcmp(field, "next"))
        // ... code for reading fields rather than methods goes here ... //
    }
    
    // Functions that are part of the player library rather than tied to any
    // one player instance.
    static const struct luaL_Reg playerlib_api[] = {
        // player.head() -> returns the first player
        {"head", player_head},
        {NULL, NULL}
    };
    
    // Metamethods defining legal operators on player-type objects.
    static const struct luaL_Reg playerlib_metamethods[] = {
        // Overrides the tostring() library function
        {"__tostring", player2string},
        // Adds support for the table read operators:
        // t[k], t.k, and t:k(...)
        {"__index", playerget},
        // Adds support for the table write operators:
        // t[k]=v and t.k=v
        {"__newindex", playerset},
        {NULL, NULL}
    };
    
    // Instance methods for player-type objects.
    static const struct luaL_Reg playerlib_methods[] = {
        // player_obj:AddCollisionObjHook(hook)
        {"AddCollisionObjHook", AddCollisionObjHook},
        // player_obj:AddPreThinker(thinker)
        {"AddPreThinker", AddPreThinker},
        // player_obj:AddPostThinker(thinker)
        {"AddPostThinker", AddPostThinker},
        {NULL, NULL}
    };
    
    int Lua_PlayerLib(lua_State *L)
    {
        // Create the metatable and fill it with the stuff from playerlib_metamethods.
        // Every time a player object is pushed into Lua (via player_head() or similar)
        // this metatable will get attached to it, allowing lua to see the __index,
        // __newindex, and __tostring metamethods for it.
        luaL_newmetatable(L, "player");
        luaL_setfuncs(L, playerlib_metamethods, 0);
        lua_pop(L, 1);
    
        // Create the method table and fill it.
        // We push the key we're going to be storing it in the registry under,
        // then the table itself, then store it into the registry.
        lua_pushliteral(L, "player-methods");
        luaL_newlib(L, playerlib_methods, 0);
        lua_settable(L, LUA_REGISTRYINDEX);
    
        // Initialize the `player` library with the API functions.
        luaL_newlib(L, playerlib_api, 0);
        // Set that table as the value of the global "player".
        // This also pops it, so we duplicate it first...
        lua_pushvalue(L, -1);
        lua_setglobal(L, "player");
    
        // ...so that we can also return it, so that constructs like
        // local player = require 'player'; work properly.
        return 1;
    }
    

    分解一下,我们可以得到三张表:

    • player ,其中包含实际的库API,如 player.head()
    • REGISTRY["player"] ,它保存所有玩家对象共享的元表
      • __tostring 调用它进行预打印
      • __新索引 为字段写入调用
      • __索引 为字段读取调用(包括方法查找!)
    • REGISTRY["player-methods"] ,其中包含所有实例方法
      • __索引 这里查找方法实现

    如上所述,我将元方法表和方法表分开,希望尽量减少概念上的混淆;惯用代码可能会将所有方法和元方法存储在一起,并使用 luaL_getmetafield() 一开始 playerset() playerget() 进行方法查找。