1. 程式人生 > >Lua Userdata 的元表 (Metatable)

Lua Userdata 的元表 (Metatable)

在Lua C API程式設計上,經常有一些部落格會說,必須使用luaL_newmetatableluaL_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.hlauxlib.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