对象方法和字段共存于 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()
进行方法查找。
编辑:事实证明,这对于 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()
进行方法查找。