看了2天userdata, 寫了百來行代碼, 才對userdata略知一二.
userdata這東西, 可以理解為用戶自定義數據. 它是數據, 不是類型, 其實說白了, 就是一片內存. 通過一個簡單的API, 我們就能獲取一個userdata:
view sourceprint?
void *lua_newuserdata (lua_State *L, size_t size);
這個API一目了然, 創建好的userdata會被妥善安置在lua stack的頂部.
這裡有一個很有趣的地方, 就是我們能夠申請一段由lua管理的內存, 我聽說lua的gc還是蠻不錯的, 如果我可以把許多內存管理的工作扔給lua, 那真是太好了. 另一方面, 我覺得lua實在是不行, 還是自己管理內存比較靠譜, 但是我又需要讓lua能比較直接的操作我寫的C模塊所申請的一片內存. 面對2種不同的需求, lua提供的機制都能夠讓我們一一應對.
1. 申請一片比較大的內存, 將實例放在這片內存裡.
2. 申請小段內存, 在這片內存中保存實例地址, 將實例放在C/C++模塊申請的內存中.
在情況1中, 一旦lua的gc回收內存, C/C++實例就被銷毀. 第2種情況下, C/C++實例可以繼續存在.
這兩種解決方案都有可能被用來解決實際問題, 而第2種情況非常值得寫一個完整的例子來研究. 不過今天只看一下第1種情況吧.
為了研究第1種情況, 我打算做一個簡單的float數組. 下面是數組在C中的定義以及一些lua接口:
view sourceprint?
struct LuaArray
view sourceprint?
{
int size;
float data[1]; // 為了簡便, 我就這麼做了.
};
void InitArray(lua_State* pState); // 這個函數不是向lua提供的接口. 只是用作初始化.
int NewArray(lua_State* pState);
int ReleaseArray(lua_State* pState);
int GetArrayValue(lua_State* pState);
int SetArrayValue(lua_State* pState);
int GetArrayLength(lua_State* pState);
int SumArray(lua_State* pState);
下面是InitArray函數的代碼:
view sourceprint?
static const char* LuaArrayTableName = "LuaArray";
static const luaL_Reg ArrayFunction[] =
{
{"__newindex", SetArrayValue},
{"__len", GetArrayLength},
{"__gc", ReleaseArray},
{"get", GetArrayValue},
{"sum", SumArray},
{"new", NewArray},
{NULL, NULL}
};
void InitArray(lua_State* pState)
{
luaL_register(pState, LuaArrayTableName, ArrayFunction);
lua_pushvalue(pState, -1);
lua_setfield(pState, -2, "__index");
lua_pop(pState, 1);
};
我們可以采用類似MFC中消息映射的一些宏來簡化LuaArrayTableName和ArrayFunction.
這裡, 我創建了一個lua table, 並讓這張表本身作為一張元表, 存儲於lua_State的context中.
一旦我們創建了這張元表, 就能讓我們的userdata和這張元表綁定. 這樣, 我們就能在lua中, 對userdata進行元表規定的操作.
先來看一下創建的代碼:
view sourceprint?
int NewArray(lua_State* pState)
{
int elemCount = luaL_checkint(pState, 1);
int memSize = sizeof(LuaArray) + elemCount * sizeof(float);
LuaArray* pUData = (LuaArray*)lua_newuserdata(pState, memSize);
pUData->size = elemCount;
pUData->data[0] = 0.0f;
for (int i = 1; i <= elemCount; ++i)
pUData->data[i] = 0.0f;
lua_getglobal(pState, LuaArrayTableName);
lua_setmetatable(pState, -2);
// ----------------------------------------------------------------------------
// 在gc時使用, 沒有特別的意義.
float* pExData = new float[10];
memcpy_s((void*)pUData->data, sizeof(float), (void*)&pExData, sizeof(float*));
// ----------------------------------------------------------------------------
return 1;
}
第5行創建了userdata, 並在前端存儲LuaArray結構.
在lua中, 我們用這樣的代碼就能創建一個LuaArray:
view sourceprint?
arr = LuaArray.new(10) -- 創建10個元素的LuaArray
設置LuaArray中的值, 獲取LuaArray中的值(省去所有檢測):
view sourceprint?
int SetArrayValue( lua_State* pState )
{
LuaArray* pUData = (LuaArray*)lua_touserdata(pState, 1); // 這裡可以做一些檢測
int idx = luaL_checkint(pState, 2);
float val = (float)luaL_checknumber(pState, 3);
pUData->data[idx] = val;
return 0;
}
int GetArrayValue( lua_State* pState )
{
LuaArray* pUData = (LuaArray*)lua_touserdata(pState, 1);
int idx = luaL_checkint(pState, 2);
lua_pushnumber(pState, (lua_Number)pUData->data[idx]);
return 1;
}
lua中設置和獲取值的代碼如下:
view sourceprint?
arr[1] = 100
print(arr:get(1)) // 沒有arr[1]的原因在於元表中的__index屬性被用來指向元表本身.
其他函數大同小異. 有意思的是__gc事件.
在lua中的變量都是引用, 當一個對象沒有任何變量引用的時候, 就會被lua的gc回收.
在lua中這樣寫, 就會讓代碼回收:
view sourceprint?
arr = nil
在相應__gc事件的C/C++函數中, 我們就能對剛才申請的內存進行釋放:
view sourceprint?
int ReleaseArray( lua_State* pState )
{
LuaArray* pUData = (LuaArray*)lua_touserdata(pState, 1);
float* pExData;
memcpy_s((void*)&pExData, sizeof(float*), (void*)pUData->data, sizeof(float));
delete [] pExData;
return 0;
}
userdata+metatable的機制, 讓我們能從C/C++的角度為lua提供數據和類型的擴展. 本文中對這套機制的使用方法僅僅是一個簡陋的實驗方法, 具體項目中可以加入許多改進以應對不同需求.