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

Object methods and fields co-exist in Lua library

编辑:事实证明,这对于 Lua 来说是不可能的,它具有 __index 方法和 class 实例方法之类的方法。要么。

试图让我的 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' 绝对是一个有效的非零值。

您尝试做的事情可能的,但不是您尝试做的方式。

我认为有必要回顾一下方法调用和 metatables/metamethods 的工作原理,以及您编写的代码实际上在做什么。 tl;dr 是:

  • 方法调用只是普通的字段查找
  • 元tables,以及它们包含的元方法,是运算符重载,而不是方法定义
  • 如果要为用户数据实现此功能,则需要一个 __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

请注意,在此过程中,除了在元table 中查找的 __index 之外,没有其他字段。 metatable 存在 告诉 Lua 如何为通常没有它们的类型实现运算符;在这种情况下,字段查找(“索引”)运算符([] 及其别名 .:)用于特定类型的用户数据。这完全取决于 __index 本身来处理将字段名称转换为值的实际过程,或者通过成为可以重试查找的 table 或成为可以 return 的函数关联值。

所以,这给我们带来了如何支持(设置table)字段和(可调用)方法的答案:

  • __newindex 需要了解如何设置字段
  • __index 需要了解如何 return both 字段值 and 方法实现

因为,从Lua的角度来看,字段查找和方法查找都是相同的操作,因此__index用于两者。


鉴于此,我们应该如何组织代码以同时支持这两者,以及我们如何重组您的代码以使其正常工作?有很多方法可以做到这一点,尽管出于这个答案的目的,我将做出一些假设:

  • 字段全部存储在C端,Lua
  • 中没有对应的数据管理
  • 方法不能被Lua代码覆盖
  • 元方法实例方法
  • 分开存储

最后一个不是绝对必要的;事实上,将元方法和实例方法存储在同一个 table 中是很常见的(我通常自己这样做)。但是,我认为这也容易让 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;
}

分解,这给了我们三个 tables:

  • player,其中包含实际库 API,如 player.head()
  • REGISTRY["player"],其中包含所有玩家对象共享的 metatable
    • __tostring 为 prettyprinting
    • 调用
    • __newindex 为字段写入调用
    • __index 为字段读取调用(包括方法查找!)
  • REGISTRY["player-methods"],其中包含所有实例方法
    • __index 在此处查找方法实现

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