建議去看《Lua程序設計》24-28章,里面詳細介紹了Lua和C語言之間的通信原理,多看函數是怎么調用的,就會理解了虛擬棧是怎么操作的,以下是我看完后的總結。
為什么Lua可以作為熱更新語言
首先我們得知道什么是熱更新,簡單來說,就是在用戶通下載安裝APP之后,打開App時遇到的即時更新。本質是代碼更新而不是資源更新,大型手游都是將補丁資源放在專門的WEB服務器上,游戲啟動時動態下載並放入到游戲的持久化目錄中。
由於不同類型的語言有不同的運行機制,編譯型語言如C#,是先編譯成一整塊中間碼然后在不同平台上被.NET運行時解釋執行,這就是說使用C#編寫的APK或IPA安裝到手機上后是沒有任何C#文件的。這樣就算運行時將作為補丁的C#文件從WEB服務器上下載到持久化目錄也運行不了。而lua解釋型語言,並不需要事先編譯成塊,而是運行時動態解釋執行的。這樣LUA就和普通的游戲資源如圖片,文本沒有區別,因此可以在運行時直接從WEB服務器上下載到持久化目錄並被其它LUA文件調用。
lua是個嵌入式腳本語言,本身就是C寫的,所以Lua腳本可以很容易的被C/C++代碼調用,也可以反過來調用C/C++的函數。lua語法、解釋器、執行原理都與python相似唯一差距就是lua沒有強大的類庫作為支撐,Lua只是具備了一些比如數學運算和字符串處理等簡單的基本功能。所以lua不適合作為開發獨立應用程序的語言。輕量級 LUA語言的官方版本只包括一個精簡的核心和最基本的庫。這使得LUA體積小、啟動速度快,從而適合嵌入在別的程序里。 可擴展 LUA並不象其它許多"大而全"的語言那樣,包括很多功能,比如網絡通訊、圖形界面等。但是LUA可以很容易地被擴展:由宿主語言(通常是C或C++)提供這些功能,LUA可以使用它們,就像是本來就內置的功能一樣。
Lua和C語言之間的通信原理:虛擬棧
Lua庫中沒有定義任何全局變量,他將所有的狀態都保存在動態結構lua_State中
為什么要用棧來儲存數據而不是定義類似於lua_Value的類型呢
- lua_Value很難將復雜的類型映射到其他語言中
- Lua引擎無法搜索出一個保存在C變量中的lua table, 會認為這個table是垃圾並回收它
作用:可以解決C語言與lua語言之間的差異:
- Lua要求垃圾回收而C語言要求顯式釋放內存
- Lua使用動態類型而C語言使用靜態類型
棧的基本操作:
- 傳值給lua: 先將值壓入棧,再調用Lua API, 將其值從棧中彈出
- 從lua中拿值: 調用Lua API,將指定值壓入棧中
棧的操作規則:
- 嚴格按照LIFO(last in first out,先進后出)
- 調用Lua時,Lua只會改變棧的頂部;C代碼對此棧可以查詢刪除插入中間元素
棧的操作函數
-
基礎操作函數:
luaL_newstate:用於創建一個新環境或狀態;// lua_State *L = luaL_newstate();
luaL_openlibs: 輔助庫函數可以打開所有的標准庫;
luaL_loadbuff: 編譯用戶輸入的每行內容並將編譯后的程序塊壓入棧中
lua_pcall: 將程序塊從棧中彈出,並在保護模式下進行,成功返回0,若執行發生錯誤則會向棧中壓入一條錯誤消息
lua_tostring: 可獲取棧中的錯誤消息
lua_pop: 將程序塊從棧中彈出刪除
- 壓入元素:每個C類型,lua都有對應的壓入函數: lua_pushnumber, lua_pushboolean, lua_pushinteger, lua_pushstring...
需要確保棧中有足夠的空間,一般是20個空閑的槽(由MINSTACK定義)
- 查詢元素:API使用索引來引用棧中的元素,第一個壓入棧的是1,最后一個壓入棧的是-1,以此類推
一般跟着類型檢測函數: lua_isnumber, lua_istable etc.
或者是類型轉換函數: lua_tonumber, lua_tostring etc.
- 對棧的增加查詢刪除操作函數:
lua_gettop (lua_State * L): 返回棧中元素的個數
lua_settop (lua_State * L, int index): 修改棧中元素的數量;如果比以前的多則從nil補充
lua_pushvalue (lua_State * L, int index): 將指定索引的值副本壓入棧中
lua_remove (lua_State * L, int index): 將指定索引的值移除
lua_insert (lua_State * L, int index): 將指定索引的位置上開辟新元素,再將棧頂元素移到該位置
lua_replace (lua_State * L, int index): 彈出棧頂值,並將該值設置到指定的索引上
API中的錯誤處理:
用lua_pcall來調用Lua代碼,在保護模式下運行。如果發生了內存分配錯誤,lua_pcall會返回錯誤代碼
當C函數檢測出一個錯誤時,應該調用lua_error。此函數會清理Lua中所有需要清理的東西,然后跳轉回發起執行的那個lua_pcall, 並附上一條錯誤消息
C轉Lua Table操作
將C語言的struct轉變成Lua的table
設置table, 將字段名和字段值壓入棧中,並調用lua_settable創建table void setfield(lua_State* L, const char* key, int value) lua_pushstring(L, key) lua_pushnumber(L, value/MAX_COLOR); lua_settable(L, -3) }
ColorTable("GREEN", 1, 0, 0) -> background = GREEN = {r = 1, g = 0, b = 0}
struct ColorTable{
char* name;
int red;
int green;
}
void setColor(lua_State* L, struct ColorTable *c){ lua_newTable(L) setfield(L, 'r', c->red); setfield(L, 'g', c->green); lua_setglobal(L, c->name); }
拿Lua table里的值: background = {r = 1, g = 0, b = 0}
lua_setglobal(L, "background") if lua_istable(L, -1){ red = lua_getfield(L, 'r');
green = lua_getfield(L, 'g');
blue = lua_getfield(L, 'b');
}
從C調用Lua函數
流程:
- 函數壓入棧
- 參數壓入棧
- lua_pcall進行函數調用
- 彈出返回值
// lua file function add(x, y) return x+y end // C++ lua_getglobalname(L, 'add') 1. 把lua函數壓入棧 lua_pushnumber(L, x) 2. 把參數壓入棧 lua_pushnumber(L, y) if (lua_pcall(L, 2, 1, 0) != 0) 3. 用lua_pcall進行函數調用 error(L, "wrong function: %s", lua_tostring(L, -1)); if(!lua_isnumber(L, -1) // 驗證返回值 error(L, "must return number"); z = lua_tonumber(L, -1); lua_pop(L, -1); 4. 將返回值從棧中彈出 return z;
從Lua調用C函數
所有注冊到Lua中的函數都具有相同的原型:typedef int (*lua_Cfunction) (lua_State* L); 僅有一個參數且為Lua的狀態
// 將要在Lua中調用的C函數add() static int add(luaState* L) { int x = lua_tonumber(L, 1); int y = lua_tonumber(L, 2); lua_pushnumber(L, x+y) return L; } lua使用C函數前必須先注冊這個函數 lua_pushcfunction(L, add); // 壓入C函數類型的值 lua_setglobal(L, "myAdd"); // 將該值賦予全局變量myAdd
之后就可以在lua中直接調用myAdd(x, y)
數組操作
數組操作函數:
- lua_rawgeti(lua_State* L, int index, int key),相當於:
-
- lua_pushnumber(L, key);
- lua_rawget(L, index);
- lua_rawseti(lua_State* L, int index, int key),相當於:
-
- lua_pushnumber(L, key);
- lua_insert(L, -2,); // key放在前一個元素下面,因為前一個元素為賦值value
- lua_rawset(L, t);
- index表示table在棧中的位置,key表示元素在table的位置
例子:一個變換函數在C中,對lua table里的值應用了一個給定函數
int turnMap(lua_State* L) { luaL_checkType(L, 1, LUA_TABLE); luaL_checkType(L, 2, LUA_TFUNCTION); int n = lua_objlen(L, 1) for(int i = 1; i <=n; i++) { lua_pushvalue(L, 2);// 將索引位置的拷貝值壓入棧頂,也就是f lua_rawgetti(L, 1, i); // 壓入t[i] lua_call(L, 1, 1); // 調用 f(t[i]),結果壓入棧頂 lua_rawseti(L, 1, i); // t[i] = 結果 } return 0; }
upvalue
類似於C語言中的靜態變量機制,只在一個特定的函數里可見
關鍵函數:
- lua_pushclosure(lua_State* L, lua_function* function, int number): 第二個參數是基礎函數,第三個參數是upvalue的數量
- upvalue的初始化必須是在創建closure之前
- 每個closure可以有不同的upvalue
- 一個函數可以創建多個closure
- lua_upvalueindex(int index): 生成upvalue的偽索引,可以像其他棧索引一樣使用,不同的是它不在棧上
- index不能為負數
示例1:不斷增加的counter
static int count(lua_State* L) { int val = lua_tointeger(L, lua_upvalueindex(1)); //拿到第一個也是唯一一個upvalue lua_pushinteger(L, ++val); lua_pushvalue(L, -1); // 復制結果壓入棧中 lua_replace(L, lua_upvalueindex(1)); // 更新upvalue return 1 } int newCount(lua_State* L) { lua_pushnumber(L, 0); // 設置upvalue的初始值為0 lua_pushclosure(L, &count, 1) // 創建closure,建立該函數與upvalue之間的聯系 }
示例2:upvalue實現tuple
tuple_new是用於創建tuple的函數,由於參數已經在棧中,所以只需要將這些參數作為upvalue,並調用lua_pushcclosure來創建基於t_tuple的closure即可。數組tuplelib和luaopen_tuple是創建庫的標准代碼,tuple庫中只有new函數
關鍵函數:
- luaL_Reg: 數組元素結構,有兩個字段,一個字符串和一個函數指針
- 必須以NULL, NULL結尾代表結束
- luaL_register(lua_State* L, string functionName, const struct luaL_Reg* lib):根據給定的名字“functionName”創建一個table,並用數組lib中的信息填充這個table
- 函數返回時會把這個table留在棧中
- luaL_opint(lua_State* L, int index, int num): 類似於luaL_checkint, 但它允許參數為null,若不存在則返回默認值(0)
- lua_isnone(lua_State* L, lua_upvalueindex(int i)): 用於測試upvalue是否存在
- lua_argcheck(lua_State* L, bool condition, int index, string errMsg): 用於檢測condition是否滿足,若不滿足則返回error message
tuple的使用 x = tuple.new(10, 'a', {}, 3) print(x(1)) // 10 int luaopen_tuple(lua_State* L) { luaL_register(L, "tuple", tuplelib); // 給tuple注冊函數列表 return 1; } static const struct luaL_Reg tuplelib[] = { {"new", tuple_new}, {NULL, NULL}, //必須以null結尾 }; int tuple_new(lua_State* L) { lua_pushcclosure(L, t_tuple, lua_gettop(L); // 創建closure,參數的個數為upvalue的數量 return 1; } int t_tuple(lua_State* L) { int op = luaL_opint(L, 1, 0);
if (op == 0)
{
// 遍歷所有的upvalue並壓入棧中
for(int i = 1; !lua_isnone(L, lua_upvalueindex(i)); i++)
{
lua_pushvalue(L, lua_upvalueindex(i);
}
return i - 1; // return upvalue的數量
}
else
{
luaL_argcheck(L, op > 0, 1, "out of range");
if (lua_isnone(L, lua_upvalueindex(op)) // 無此字段
return 0;
lua_pushvalue(L, lua_upvalueindex(op);
return 1;
}
}
元表
一種辨別不同類型的userdata的方法是為每種類型創建一個唯一的元表,每當得到一個userdata,就檢查它是否擁有正確的元表
- luaL_setmetatable: 創建一個新的table作為元表,並將其壓入棧頂,然后將這個table與注冊表中的指定名稱關聯起來
- luaL_getmetatable:在注冊表中搜索與tname關聯的元表
面對對象的操作
local metaarray = getmetatable(array.new(1)) metaarray.--index = metaarray // 當a時userdata時是沒有size key的,因此lua會通過a元表的--index,找到自己本身metaarray,然后再找到size metaarray.size = array.size static const struct luaL_Reg arraylib_m [] = { {"--newindex", setarray}, //元方法 將a:setarray(i, 0) 變成 a[i] = 0 {"--index", getarray}, //將a:get(i) 變成 a[i] {"--len", getsize}, {NULL, NULL} }
luaL_register(L, NULL, arraylib_m): 以NULL作為庫名,是不會創建任何用於儲存函數的table,而是以棧頂的table作為存儲函數的table
這樣就可以直接調用metaarray.getarray(10)
C#與Lua的交互很容易產生GC,該怎么優化呢?
首先看個例子,下面func1的性能比func2的性能好幾十倍,造成這個差異的“元凶”就是裝箱和拆箱
int func1(int i) { return i + 1; } object func2(object o) { return (int)o + 1; }
交互的優化的方向主要是減少裝箱和拆箱的次數
SLua的優化思路:把lua的棧操作api暴露出來,一個個參數的壓棧,調用完一個個返回值的取。這些壓棧和取返回值的接口都是確定類型的,也就是說都是類似於func1的接口
- 用lua重新實現了Vector3的所有方法
- 不要直接傳Vector3/Quaternion等unity值類型, 改為傳三個float x, floaty, float z
- 頻繁調用的函數,參數的數量要控制
- 優先使用static函數導出,減少使用成員方法導出。一個object要訪問成員方法或者成員變量,都需要查找lua userdata和c#對象的引用,或者查找metatable,耗時甚多。直接導出static函數,可以減少這樣的消耗。比如 LuaUtil.SetPos(obj, pos.x, pos.y,pos.z)
Reference:
- 《Lua程序設計》第24-28章
- https://gameinstitute.qq.com/community/detail/125117
- https://blog.csdn.net/sm9sun/article/details/68946343