lua中的型別
基礎型別
#define LUA_TNIL 0
#define LUA_TBOOLEAN 1
#define LUA_TLIGHTUSERDATA 2
#define LUA_TNUMBER 3
#define LUA_TSTRING 4
#define LUA_TTABLE 5
#define LUA_TFUNCTION 6
#define LUA_TUSERDATA 7
#define LUA_TTHREAD 8
變體(或者說子型別)
/*
** tags for Tagged Values have the following use of bits:
** bits 0-3: actual tag (a LUA_T* value)
** bits 4-5: variant bits
** bit 6: whether value is collectable
*/
/*
** LUA_TFUNCTION variants:
** 0 - Lua function
** 1 - light C function
** 2 - regular C function (closure)
*/
/* Variant tags for functions */
#define LUA_TLCL (LUA_TFUNCTION | (0 << 4)) /* Lua closure */
#define LUA_TLCF (LUA_TFUNCTION | (1 << 4)) /* light C function */
#define LUA_TCCL (LUA_TFUNCTION | (2 << 4)) /* C closure */
/* Variant tags for strings */
#define LUA_TSHRSTR (LUA_TSTRING | (0 << 4)) /* short strings */
#define LUA_TLNGSTR (LUA_TSTRING | (1 << 4)) /* long strings */
/* Variant tags for numbers */
#define LUA_TNUMFLT (LUA_TNUMBER | (0 << 4)) /* float numbers */
#define LUA_TNUMINT (LUA_TNUMBER | (1 << 4)) /* integer numbers */
/* Bit mark for collectable types */
#define BIT_ISCOLLECTABLE (1 << 6)
lua中的物件都是用TValue來描述的,TValue中的tt_成員變數代表著這個TValue的型別。關於型別的具體定義,上面貼的程式碼中的註釋中已經講的比較清楚了。
一個lua物件的型別是由一個7位的bits描述的。比如一個整數,這個物件的型別就是0011000(24)表示這個物件是數字型別中的整形,是一個不可回收物件。
C#如何獲取lua物件
和c語言和lua互動其實沒啥本質區別,就是通過lua提供的c函式操作lua棧,直接從棧中取就可以了。區別在於如何把取到的值轉換為c#認識的值。
如何在C#端描述這些型別
簡介
lua的型別中boolean、string、number這幾個型別是clr所認識的型別,所以clr就可以直接把這些型別拿過來用。具體就是直接呼叫Lua提供的lua_tonumber之類的c介面。
lightUserData、table、function、userData、thread是C#不認識的類,需要通過某種標識(lua自帶的reference系統)來表示。
boolean、string、number類
這三個類上面已經說過了,直接用提供的介面轉就可以,下面寫幾個需要注意的點:
- string雖然也是一個引用型別,但是clr在拿到這個string的指標時,還需要將這個string的資料直接複製進clr中才算轉型結束(xlua也已經封裝好了,不用我們自己去複製)。
- 大部分型別轉型失敗的時候都不會報錯,而是會返回一個預設值。就拿將一個lua物件轉為int來說,最終是通過lua_tointegerx函式呼叫的,當lua物件不是number型別時,返回0:
LUA_API lua_Integer lua_tointegerx (lua_State *L, int idx, int *pisnum) {
lua_Integer res;
const TValue *o = index2addr(L, idx);
int isnum = tointeger(o, &res);
if (!isnum)
res = 0; /* call to 'tointeger' may change 'n' even if it fails */
if (pisnum) *pisnum = isnum;
return res;
}
- 當一個number型別是浮點數時,轉型整數不會進行取整操作,而是會直接返回0。因為lua預設對float轉int的操作模式LUA_FLOORN2I是0,代表碰見float轉int時返回0。
/*
** try to convert a value to an integer, rounding according to 'mode':
** mode == 0: accepts only integral values
** mode == 1: takes the floor of the number
** mode == 2: takes the ceil of the number
*/
int luaV_tointeger (const TValue *obj, lua_Integer *p, int mode) {
TValue v;
again:
if (ttisfloat(obj)) {
lua_Number n = fltvalue(obj);
lua_Number f = l_floor(n);
if (n != f) { /* not an integral value? */
if (mode == 0) return 0; /* fails if mode demands integral value */
else if (mode > 1) /* needs ceil? */
f += 1; /* convert floor to ceil (remember: n != f) */
}
return lua_numbertointeger(f, p);
}
else if (ttisinteger(obj)) {
*p = ivalue(obj);
return 1;
}
else if (cvt2num(obj) &&
luaO_str2num(svalue(obj), &v) == vslen(obj) + 1) {
obj = &v;
goto again; /* convert result from 'luaO_str2num' to an integer */
}
return 0; /* conversion failed */
}
userData
userData主要是lua對c#物件的引用,這裡只簡單說一下。
代表c#物件的userData主要分兩種。
- 把c#物件存在ObjectTranslator中,用下標作為引用(類似於lua中的reference)。
- 經過GC優化的結構體和列舉,不存在ObjectTranslator中,而是把所有內容都打包到userdata中一起傳入lua中。比如一個Vector3,那麼xlua會把這個Vector3的x、y、z作為3個連續的float一起打包到userdata中。這樣就避免了c#層的裝箱、拆箱和gc操作。
對table與function的引用簡介
這兩個型別都是通過lua的reference系統來讓c#持有對lua物件的引用。
lua reference系統
c#就是通過這個系統來持有不認識的lua物件的。
一共就兩個介面:
- luaL_ref:把棧頂元素加入一個lua的表中,並返回下標。
- luaL_unref:把一個下標所代表元素從表中刪除。
這樣就可以用一個整數來讓lua外的環境持有這個lua物件。
具體可以看下官方說明lua References
luaBase類
所有lua物件在c#中的基類,在初始化時通過luaL_ref生成lua物件的引用,在析構時通過luaL_unref移除引用。
對table的引用
LuaTable
一般情況下table在C#中被包裝為LuaTable類,沒啥特別的,只是在LuaBase的基礎上增加了幾個常用的函式。比如Get、Set之類的,讓開發者可以避開一些不直觀的棧操作。
Array、List、Dictionary
這幾個都差不多。都是把table中的key和value全部拿出來,組成Array或Dictionaray。
介面、其他類
這兩種轉型是嘗試把這個table看作對應的介面或類。
比如將一個table轉為IEnumberator就是把table轉為SystemCollectionsIEnumeratorBridge類(繼承了LuaBase、實現了IEnumerator的類,由Xlua生成),這個類實現了MoveNext和Reset。實現方法就是呼叫一下table中對應名稱的函式。
對function的引用
lua函式在c#中有兩種表示:
LuaFunction
LuaFunction和luaTable差不多,也是在LuaBase的基礎上增加了幾個常用函式,Call、Action之類的。
DelegateBridge
為什麼已經有LuaFunction還要一個DelegateBridge類?
因為我們在c#中拿到一個lua函式時,大多數時候是要作為一個委託來時用的。DelegateBridge就是用來化簡這個轉型操作的。
DelegateBridge的功能就是在持有lua函式引用的同時,將這個函式包裝成各種各樣的委託,讓整個轉型過程對開發人員無感知。
下面是一個不使用DelegateBridge,自己轉型的例子,比較繁瑣:
//將一個LuaFunction作為一個Action<int>使用
//其實LuaFunction.Cast就是幹這個的,這裡只是用簡單的方式表達出來
public static Action<int> LuaFunctionToActionInt(XLua.LuaFunction luaFunction)
{
//由於luaFunction已經提供了Call操作封裝了函式呼叫的各種棧操作,所以我們這裡只需要用一個Action<int>把這個操作包裝起來即可
return (x) =>
{
luaFunction.Call(x);
};
}
public static void Test()
{
XLua.LuaEnv luaEnv = new XLua.LuaEnv();
object[] rets = luaEnv.DoString("return function(x) CS.UnityEngine.Debug.LogError(\"print x: \"..x) end");
var luaFunction = (XLua.LuaFunction)rets[0];
Action<int> actionInt = LuaFunctionToActionInt(luaFunction);
actionInt(10);
}
DelegateBridge重要成員
xlua在將lua函式轉型的時候做了什麼
Tips
- 通過ObjectTranslator.getDelegateUsingGeneric生成委託時,會對返回值和引數進行不為值型別的約束。因為值型別在il2cpp下會有jit異常。這也是為什麼我們發現有的委託型別不用註冊也可以使用,但是有的就不行。
- 在編輯器模式下,沒有進行程式碼生成時,會通過Emit直接生成一個XLuaGenDelegateImplx類,內容和通過程式碼生成後的DelegateBridge一樣,而不是全部通過反射來進行轉型。讓沒有進行程式碼生成時的環境和真機環境更接近。
- DelegateBridge一般不會被直接引用,而是被bindto中的委託生成的閉包引用和被delegate_bridges作為弱引用持有。當一個DelegateBridge的bindto中的委託沒有被任何物件引用時,這個DelegateBridge就會在下次gc時被gc掉。
其他
這裡主要寫了常用lua型別轉型的簡介和一些關鍵點。可能不夠全面和細節。
如果有什麼錯誤或者問題可以在下面留言。