Lua Userdata 的元表 (Metatable)
在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;
}
執行結果
A ctor 001EF910
Closing A: 001EF910
A dtor 001EF910
可以看到,__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;
}
從中不難看出,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)
執行結果是:
table JustName: 0000000000779e10