(一)通過一個簡單的例子來看一下userdata的用法:
寫一個C的Lua庫,讓Lua能夠通路C的數組,借助userdata來實作。
(1)VS中建立一個DLL工程,設定好lua庫的包含目錄、連結庫;
(2)建立一個源檔案main.cpp,代碼如下:
(3)編譯生成名為array.dll的檔案,并将array.dll放在luaforwindows的clibs子目錄下,該目錄下都是為lua寫的c庫,或者将其放到本地注冊的Lua環境變量的某個目錄下;
(4)lua測試:
上面代碼中的關鍵函數:
void *lua_newuserdata (lua_State *L, size_t size);
建立full userdata。
(1)配置設定一塊指定大小的記憶體;
(2)将該full userdata壓棧;
(3)傳回該記憶體塊的位址給主機程式,主機程式能夠随意使用這塊記憶體。
void luaL_argcheck (lua_State *L,int cond,int arg,const char *extramsg);
檢查條件是否滿足。
(二)利用metatable辨別userdata來增加代碼的安全性
上面的C庫是有缺陷的,比如我們怎麼確定例子中setarray的第一個參數就是我們想要的數組userdata,而不是别的不相關的userdata呢?userdata是一種lua類型,它可以用來表示宿主語言中的各種自定義類型對象,為了區分特定類型,我們使用的方法是:
我們單獨為該數組建立一個metatable,每次建立數組userdata時,我們設定其和metatable的關聯。每次我們通路數組的時候,都檢查一下其是否有一個正确的metatable即可。也就是利用不同的metatable來标記不同類型的userdata。因為Lua代碼不能夠改變userdata的metatable,是以Lua不會僞造我們的代碼。
是以我們對上面的例子進行一些改進,給數組userdata添加一個類型辨別,C庫代碼如下:
void luaL_newmetatable (lua_State *L, const char *tname);
建立userdata可用的metatable。
如果registry已經有tnme鍵值,則函數傳回0;
否則,建立一個[tname, metatable],并放入registry,并傳回1。
兩種情況下,都會講tname對應的值入棧。
堆棧+1
void *luaL_checkudata (lua_State *L, int index, const char *tname);
檢查在棧中指定位置的對象是否為帶有給定名字的metatable(registry中鍵tname對應的值)的usertata。是則傳回userdata位址,否則傳回NULL。
void luaL_getmetatable (lua_State *L, const char *tname);
擷取registry中的tname對應的metatable,并入棧。注意區分lua_getmetatable函數。
void luaL_setmetatable (lua_State *L, const char *tname);
将棧頂對象的metatable設定為registry表中鍵tname對應的值。注意區分lua_setmetatable函數。
int lua_getmetatable (lua_State *L, int index);
擷取index對應的table的metatable,并入棧。如果該table沒有metatable,則傳回0,且堆棧不變。
void lua_setmetatable (lua_State *L, int index);
将棧頂的table出棧并設定給index處的值作為metatable。
堆棧-1
(三)将上面的代碼改造成面向對象的方式
類型為對象的userdata,可以使用如下的文法來操作對象的執行個體:
思路大緻如下:
(1)array表隻包含一個方法,也就是用來生成數組對象的new方法;
(2)數組userdata帶有metatable用于類型識别;
(3)userdata的metatable定義__index,那麼,每當通路數組的方法時,都會觸發__index這個metamethod(對于userdata來講,每次被通路的時候元方法都會被調用,因為userdata根本就沒有任何key);
(4)将metatable.__index設為該表metatable本身(__index可以為函數或者表,這裡使用後者);
(5)metatable包含其餘所有的數組操作函數。
那麼每當調用userdata的某個方法時,比如a:size(),它等同于a.size(a),這時會觸發userdata的名為__index的metamethod,metatable的__index就是它本身,而metatable表中有size域,是以調用metatable的size(a)函數,就ok了。
(四)以數組下标的形式通路
怎樣實作支援下表操作的文法來通路userdata呢,就像下面一樣:
可以直接在lua中通過以下代碼實作:
對應到C中的實作方式如下:
将metatable的__index設為array的get方法,__newindex設為set方法即可。在讀取a[i]的時候會觸發__index,并将對象本身和參數同時傳遞給__index對應的函數,寫a[i]的時候原理一緻。
(五)light userdata
light userdata不同于full userdata,它有如下特點:
(1)full userdata代表Lua中的C對象,light userdata代表一個C指針的值(也就是一個void *類型的值)。由于它是一個值,我們不能建立他們(同樣的,我們也不能建立一個數字)。
(2)僅僅是一個指針,像數字一樣,沒有metatables,light userdata不需要垃圾收集器來管理她。
(3)可以用于表示不同類型的對象,我們在Lua中使用light userdata表示C對象。
因為它是一個值,任何指向同一個C位址的light userdata都相等。
void lua_pushlightuserdata (lua_State *L, void *p);
将一個light userdata入棧。
(六)userdata相關的資源釋放
Lua以__gc元方法的方式提供了finalizers。這個元方法隻對userdata類型的值有效。當一個userdata将被收集的時候,并且userdata有一個__gc域,Lua會調用這個域的值(應該是一個函數):以userdata作為這個函數的參數調用。這個函數負責釋放與userdata相關的所有資源,比如說檔案描述符、視窗句柄等。