在Lua C API编程上,经常有一些博客会说,必须使用luaL_newmetatable和luaL_setmetatable来给userdata加元表。还说给userdata加的元表不是普通的table。于是本着不信邪的态度,我翻了一下源码并自己尝试写了一些测试。
普通的表能做userdata的元表么
对于这个问题,我觉得是可以的,因为Lua中只有这一种数据结构,不存在什么特别的表。
class A
{
public:
A() { cout << "A ctor " << this << endl; }
~A() { cout << "A dtor " << this << endl; }
};
int close_A(lua_State* L)
{
A* pa=static_cast<A*>(lua_touserdata(L, 1));
cout << "Closing A: " << pa << endl;
pa->~A();
return 0;
}
int test(lua_State* L)
{
auto ptr = new (lua_newuserdata(L, sizeof(A))) A;
lua_newtable(L);
lua_pushcfunction(L, close_A);
lua_setfield(L, -2, "__gc");
lua_setmetatable(L, -2);
return 1;
}
int main()
{
auto L = luaL_newstate();
luaL_openlibs(L);
lua_register(L, "test", test);
if (luaL_loadstring(L, "t=test()")) {
cout << "Failed to load string." << endl;
cout << lua_tostring(L, -1) << endl;
}
else if (lua_pcall(L, 0, 1, NULL)) {
cout << "Error: " << lua_tostring(L, -1) << endl;
}
lua_close(L);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
运行结果
A ctor 001EF910
Closing A: 001EF910
A dtor 001EF910
1
2
3
可以看到,__gc元方法被正确的调用了,因此元表无论在Lua层还是在C层都跟普通的表没有区别。
luaL_*metatable做了什么
通过翻看Lua源码可以弄清楚这一点。在lauxlib.h和lauxlib.c中:
#define luaL_getmetatable(L,n) (lua_getfield(L, LUA_REGISTRYINDEX, (n)))
LUALIB_API int luaL_newmetatable (lua_State *L, const char *tname) {
if (luaL_getmetatable(L, tname) != LUA_TNIL) /* name already in use? */
return 0; /* leave previous value on top, but return 0 */
lua_pop(L, 1);
lua_createtable(L, 0, 2); /* create metatable */
lua_pushstring(L, tname);
lua_setfield(L, -2, "__name"); /* metatable.__name = tname */
lua_pushvalue(L, -1);
lua_setfield(L, LUA_REGISTRYINDEX, tname); /* registry.name = metatable */
return 1;
}
LUALIB_API void luaL_setmetatable (lua_State *L, const char *tname) {
luaL_getmetatable(L, tname);
lua_setmetatable(L, -2);
}
LUALIB_API void *luaL_testudata (lua_State *L, int ud, const char *tname) {
void *p = lua_touserdata(L, ud);
if (p != NULL) { /* value is a userdata? */
if (lua_getmetatable(L, ud)) { /* does it have a metatable? */
luaL_getmetatable(L, tname); /* get correct metatable */
if (!lua_rawequal(L, -1, -2)) /* not the same? */
p = NULL; /* value is a userdata with wrong metatable */
lua_pop(L, 2); /* remove both metatables */
return p;
}
}
return NULL; /* value is not a userdata with a metatable */
}
LUALIB_API void *luaL_checkudata (lua_State *L, int ud, const char *tname) {
void *p = luaL_testudata(L, ud, tname);
if (p == NULL) typeerror(L, ud, tname);
return p;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
从中不难看出,luaL_newmetatable先尝试获取与tname绑定的一个表,如果不为nil,则返回0,但此时该表已经获取并放在了栈上。如果为nil,则在栈上新建一个{__name=$tname}这样的表,然后复制一份,并通过lua_setfield设置为某个表中对应键为$tname的值,返回1。
luaL_setmetatable显然是先获取绑定的表,然后将表设置为原栈顶元素的元表。需要注意的是luaL_setmetatable不会自动新建元表,因此当获取一个不存在的表时,会引起逻辑上的错误。
luaL_getmetatable是一个很简单的宏,但这里的LUA_REGISTRYINDEX多少让人有些迷惑。其实Lua官方手册已经给出了解释. 大意就是,这是一个伪索引,对应着一个全局表 (不妨成为registry) ,其真正存储位置不在Lua的虚拟栈上,但是可以通过操作栈的函数来操作这个表。这个表只能通过C API来操作访问,Lua层无法访问到这个表。可以用来存储模块数据等。
所以其实luaL_*metatable只是将registry表作为参照,将新建的表存入到registry中。luaL_checkudata也不过是将userdata的元表和registry中存储的元表进行直接比较(rawequal)。
还有一点需要注意,在luaL_newmetatable中,新建的元表有__name键。此键类似于其他语言的"类型",是Lua解释器用来显示信息所用的元属性。在Lua层中就可以尝试. 例如:
mt={__name="JustName"}
t={}
setmetatable(t,mt)
print(type(t),t)
1
2
3
4
运行结果是:
table JustName: 0000000000779e10
1
————————————————
版权声明:本文为优快云博主「Kiritow」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/Kiritow/article/details/85012879