Lua中的userdata


【話從這里說起】

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

【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。下面就以設置姓名和獲取姓名為例子。

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中。

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

【元表】

上述的代碼有一個很嚴重的問題,為什么這么說呢?我先把上一個例子的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呢?我們需要一種這樣的機制來確保參數的合法性。

一種辨別不同類型的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,使用上面代碼中的第7、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);


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM