Lua中的userdata

【話從這裏提及】

在我發表《Lua中的類型與值》這篇文章時,就有讀者給我留言了,說:你應該好好總結一下Lua中的function和userdata類型。如今是時候總結了。對於function,我在《Lua中的函數》這篇文章中進行了總結,而這篇文章將會對Lua中的userdata進行仔細的總結。對於文章,你們若是有任何疑議,均可以在文章的下方給我留言,也能夠關注個人新浪微博與我互動。學習,就要分享,我期待你的加入。html

【userdata是啥?】

userdata是啥?簡單直譯就是用戶數據,若是再文藝一點,就叫作用戶自定義數據。要這貨有什麼好處呢?首先,讓咱們來想象一個場景,你能夠在C中定義struct,當你在C中定義了一個struct,你有麼有想過,如何讓Lua表示這個struct,也就是說,Lua和C要進行溝通,如何讓Lua也能正確的訪問這個struct呢?這是一個符合實際且實用的需求。遇到這種需求,怎麼辦?這個時候,實用userdata就能大展身手了,所以Lua爲此提供了一種基本的類型——userdata。userdata提供了一塊原始的內存區域,能夠用來存儲任何東西。而且,在Lua中userdata沒有任何預約義的操做。先來看看怎麼使用userdata。編程

函數lua_newuserdata會根據指定的大小分配一塊內存,並將對應的userdata壓入棧中,最後返回這個內存塊的地址:數組

void *lua_newuserdata(lua_State *L, size_t size);

下面,就經過一簡單的實例來講說userdata的使用。函數

static struct StudentTag
{
    char *strName; // 學生姓名
    char *strNum; // 學號
    int iSex; // 學生性別
    int iAge; // 學生年齡
};

定義一個學生結構體,以後的操做,都在這個學生結構體上進行,包括設置學生姓名,學號,性別和年齡。學習

static int Student(lua_State *L)
{
    size_t iBytes = sizeof(struct StudentTag);
    struct StudentTag *pStudent;
    pStudent = (struct StudentTag *)lua_newuserdata(L, iBytes);

    return 1; // 新的userdata已經在棧上了
}

建立一個新的學生結構體,使用的lua_newuserdata函數,建立完成之後,這個新的userdata就在棧上,能夠直接返回給Lua。下面就以設置姓名和獲取姓名爲例子。ui

static int GetName(lua_State *L)
{
    struct StudentTag *pStudent = (struct StudentTag *)lua_touserdata(L, 1);
    luaL_argcheck(L, pStudent != NULL, 1, "Wrong Parameter");
    lua_pushstring(L, pStudent->strName);

    return 1;
}

static int SetName(lua_State *L)
{
    // 第一個參數是userdata
    struct StudentTag *pStudent = (struct StudentTag *)lua_touserdata(L, 1);
    luaL_argcheck(L, pStudent != NULL, 1, "Wrong Parameter");

    // 第二個參數是一個字符串
    const char *pName = luaL_checkstring(L, 2);
    luaL_argcheck(L, pName != NULL && pName != "", 2, "Wrong Parameter");
    pStudent->strName = pName;
    return 0;
}

在GetName函數中,只有一個參數,那就是使用Student函數建立的userdata,而後使用C語言的方式,從中取出名字,放到棧中,返回到Lua中。lua

在SetName函數中,須要傳入兩個參數,第一個參數是userdata,第二個參數是須要設置的值,而後直接賦值就行了,使用起來比較簡單,沒有很複雜的步驟,你覺的呢?spa

【元表】

上述的代碼有一個很嚴重的問題,爲何這麼說呢?我先把上一個例子的Lua代碼貼出來:指針

require "userdatademo1"

local objStudent = Student.new()
Student.setName(objStudent, "果凍想")
Student.setAge(objStudent, 15)

local strName = Student.getName(objStudent)
local iAge = Student.getAge(objStudent)

print(strName)
print(iAge)

調用Student的new獲得一個Student實例之後,之後調用Student的其它函數時,第一個參數都是使用Student函數獲得的userdata,也就是上面代碼中的objStudent。在C模塊側,咱們只是簡單的判斷了一下傳進來的userdata是否爲NULL,並無辦法判斷傳進來的userdata參數是使用Student函數獲得的;若是我傳一個錯誤的userdata進去,程序也會繼續運行,但有可能使內存遭到破壞。那如何肯定咱們傳入的userdata正是咱們須要的userdata呢?咱們須要一種這樣的機制來確保參數的合法性。code

一種辨別不一樣類型的userdata的方法是,爲每種類型建立一個惟一的元表(什麼是元表?)。每當建立了一個userdata後,就用相應的元表來標記它。而每當獲得一個userdata後,就檢查它是否擁有正確的元表。因爲Lua代碼不能改變userdata的元表,所以也就沒法欺騙代碼了。

爲每一個userdata都建立一個元表,那就須要有個地方來存儲這個新的元表。在Lua中,一般習慣是將全部新的C類型註冊到註冊表中,以一個類型名做爲key,元表做爲value。因爲註冊表中還有其它的內容,因此必須當心地選擇類型名,以免與key衝突。

Lua的輔助庫中提供了一些函數來幫助實現上面說的內容,可使用的輔助庫函數有:

int luaL_newmetatable(lua_State *L, const char *tname);
void luaL_getmetatable(lua_State *L, const char *tname);
void *luaL_checkudata(lua_State *L, int index, const char *tname);

luaL_newmetatable函數會建立一個新的table用做元表,並將其壓入棧頂,而後將這個table與註冊表中的指定名稱關聯起來。luaL_getmetatable函數能夠在註冊表中檢索與tname關聯的元表。luaL_checkudata能夠檢查棧中指定位置上是否爲一個userdata,而且是否具備與給定名稱相匹配的元表,若是該對象不是一個userdata,或者它不具備正確的元表,就會引起一個錯誤;不然它就會返回這個userdata的地址。如今來重寫上面的那個例子:

int luaopen_userdatademo2(lua_State *L)
{
    // 建立一個新的元表
    luaL_newmetatable(L, "Student");
    luaL_register(L, "Student", arrayFunc);
    return 1;
}

建立一個新元表,做爲該userdata的惟一標識。

static int Student(lua_State *L)
{
    size_t iBytes = sizeof(struct StudentTag);
    struct StudentTag *pStudent;
    pStudent = (struct StudentTag *)lua_newuserdata(L, iBytes);

    // 設置元表
    luaL_getmetatable(L, "Student");
    lua_setmetatable(L, -2);

    return 1; // 新的userdata已經在棧上了
}

在建立userdata的時候,設置該userdata的元表。在使用的時候,咱們就能夠調用luaL_checkudata對參數進行檢查。

static int GetName(lua_State *L)
{
    struct StudentTag *pStudent = (struct StudentTag *)luaL_checkudata(L, 1, "Student");
    lua_pushstring(L, pStudent->strName);
    return 1;
}

當寫下一下Lua語句時:

Student.getAge(io.stdin)

就會拋出這樣的異常錯誤:

bad argument #1 to 'getAge' (Student expected, got userdata)

如今,我想你應該懂得了如何去簡單的使用userdata了吧。接下來,上點難的東西。單擊這裏下載完整項目工程userdatademo2.zip

面向對象的訪問

關於Lua的面向對象對象編程,我在《Lua中的面向對象編程》這篇文章中進行了總結,若是你對Lua中的面向對象編程還不是很熟悉,能夠再去閱讀一下《Lua中的面向對象編程》。

在上面的Lua代碼中,能夠看到,我都是使用如下方式調用函數的:

local strName = Student.getName(objStudent)

這種調用方式無可厚非,可是從面向對象的角度來講,我new了一個對象,這就是一個獨立的對象,我應該這樣調用,才能更好理解啊。

local strName = objStudent:getName()

是吧。這又回到了《Lua中的面向對象編程》一文中說到的問題,因爲getName、setName等這些函數都是在Student中定義的,而在objStudent對象中,並無這些函數的定義,怎麼辦?仍是老辦法,咱們須要設置objStudent的元表,設置__index字段,當在objStudent中找不到對應的函數時,就去Student中查找,以前在《Lua中的面向對象編程》一文中,也是介紹的這個辦法。來吧,實現一下吧。

int luaopen_userdatademo3(lua_State *L)
{
    // 建立一個新的元表
    luaL_newmetatable(L, "Student_Metatable");

    // 元表.__index = 元表
    lua_pushvalue(L, -1);
    lua_setfield(L, -2, "__index");
    luaL_register(L, NULL, arrayFunc_meta);
    luaL_register(L, "Student", arrayFunc);

    return 1;
}

上述代碼中,有兩個地方要特別注意。首先要設置Student.__index = Student,使用上面代碼中的第七、8行實現的。還有一個須要注意的地方是luaL_register的特殊用法。在第一次調用luaL_register時,它的第二個參數是NULL,這樣的話,luaL_register不會建立任何用於存儲函數的table,而是以棧頂的table做爲存儲函數的table,而如今棧頂的table就是luaL_newmetatable建立的元表。代碼中,兩個luaL_register的第三個參數是不同的,它們的定義以下:

static struct luaL_reg arrayFunc[] =
{
    { "new", Student },
    { NULL, NULL }
};

static struct luaL_reg arrayFunc_meta[] =
{
    { "getName", GetName },
    { "setName", SetName },
    { "getAge", GetAge },
    { "setAge", SetAge },
    { "getSex", GetSex },
    { "setSex", SetSex },
    { "getNum", GetNum },
    { "setNum", SetNum },
    {NULL, NULL}
};

最終調用luaL_register(L, 「Student」, arrayFunc);獲得的Student表中,就只有一個函數new;而元表Student_Metatable中則有arrayFunc_meta數組中包含的全部方法。我在把Lua代碼貼上來,而後再詳細的分析一下流程。

require "userdatademo3"

local objStudent = Student.new()
objStudent:setName("果凍想")
objStudent:setAge(15)
local strName = objStudent:getName()
local iAge = objStudent:getAge()

print(strName)
print(iAge)
  1. 調用require 「userdatademo3″將獲得一個Student表,在該表中,只有一個函數——new;
  2. 調用Student.new()將獲得一個Student結構體的userdata,該userdata的元表爲Student_Metatable;
  3. 因爲在userdata中自己就沒有key,因此在userdata中沒有table中那樣的key的概念;當調用objStudent:setName時,就會去元表Student_Metatable中找setName,而後完成調用;
  4. 因爲Student自己沒有被設置元表Student_Metatable;當調用Student.setName(objStudent, 「果凍想」)時,就會出錯。這樣也好,Student自己就不是一個實際的「學生」對象,只是一個模板,使用Student直接調用setName出錯,徹底符合語義。

固然了,除了__index能夠從新被定義之外,其它預約義的元方法也能夠被從新定義。在完整的項目工程中提供了__tostring元方法的實現,單擊這裏下載完整工程userdatademo3.zip

輕量級userdata

怎麼又有了一個輕量級userdata了?這貨又是什麼?專業點,叫作「light userdata」。我在前面總結的userdata叫作「full userdata」。

輕量級userdata是一種表示C指針的值(即void *)。因爲它是一個值,因此不用建立它。要將一個輕量級userdata放入棧中,只須要調用lua_pushlightuserdata便可。

void lua_pushlightuserdata(lua_State *L, void *p);

儘管兩種userdata在名稱上差很少,但它們之間仍是存在很大不一樣的。輕量級userdata不是緩衝,只是一個指針而已。它也沒有元表,就像數字同樣,輕量級userdata不受到垃圾收集器的管理。

輕量級userdata的真正用途是相等性判斷。一個徹底userdata是一個對象,它只與自身相等。而一個輕量級userdata則表示了一個C指針的值。所以,它與全部表示同一個指針的輕量級userdata相等。能夠將輕量級userdata用於查找Lua中的C對象。

如今就來講一種輕量級userdata的使用,還記的我在《再說C模塊的編寫(2)》中總結的註冊表麼?談及註冊表的key的時候,說使用UUID是一種不錯的方案,如今就使用輕量級的userdata結合static來實現無衝突的key。

// 壓入輕量級userdata,一個static變量的地址
static char key = 'k';
lua_pushlightuserdata(L, (void *)&key);
lua_pushstring(L, "JellyThink");
lua_settable(L, LUA_REGISTRYINDEX);

因爲靜態變量的地址在一個進程中具備惟一性,因此絕對不會出現重複key的問題。

// 從註冊表中取對應的值
lua_pushlightuserdata(L, (void *)&key);
lua_gettable(L, LUA_REGISTRYINDEX);
相關文章
相關標籤/搜索