lua5.3程序設計進階

聲明:本篇博客主要對lua和c交互時,一些比較重要且有意思的特性進行闡述。如果想要了解怎樣搭建交互環境,可以參考lua5.3與c交互環境。如果想要了解博客中提到的lua c api詳細信息,可以參考官方英文文檔或者翻譯中文文檔

1.lua中常見的c文件如下:
1>.lua.h中定義LUA_開頭的基礎宏和lua_開頭的基礎函數(如:操作lua全局變量,訪問lua函數,給lua註冊函數等),主要注重簡潔和高性能。
2>.lauxlib.h中定義luaL_開頭的輔助函數(對lua.h中定義的基礎函數進行上層封裝),主要注重實用。
3>.lualib.h中定義luaopen_開頭的標準庫函數。由於lua虛擬機創建時不包含任何接口,所以可以使用luaL_openlibs來將預定義標準庫注入到lua虛擬機中,也可以使用luaopen_標準庫名(如:base,coroutine,debug,io,math,os,package,string,table,utf8)來將指定標準庫注入到lua虛擬機中,此時就可以在lua中使用注入進來的標準庫所關聯的lua api了。
4>.luaconf.h中定義配置。如:LUA_NUMBER是double/long/int/long double等;LUA_INTEGER是int/long/long long等。

2.lua是一種可擴展性的嵌入式語言。具有以下特性:
1>.lua c api是一個函數,常量和類型組成的集合,lua中的所有功能都可以通過lua c api來完成。由於lua c api大多數函數不會檢查參數的正確性,所以我們可以在調試c代碼的時候使用宏LUA_USE_APICHECK來進行檢查。
2>.可擴展性指的是可以使用c編寫擴展庫,然後以lua c api的形式注入到lua虛擬機中。
3>.嵌入式指的是可以在c中將lua作爲庫文件,然後以lua c api的形式進行調用。

3.lua和c之間使用棧進行交互,具有以下特性:
1>.解決了lua中動態數據類型和c中靜態數據類型之間匹配的問題。
2>.解決了lua中自動內存回收和c中手動內存回收造成內存泄漏的問題。
3>.使用後進先出規則來操作棧,最後壓棧的元素最先彈出。
4>.從棧底到棧頂方向的元素索引以1開始依次+1;從棧頂到棧底方向的索引以-1開始依次-1;默認棧上可以使用LUA_MINSTACK(lua.h中定義,值爲20)個索引。

4.lua中的內存申請和內存回收都是通過指定的分配函數實現的。具有以下特性:
1>.分配函數的原型爲lua.h文件中定義的lua_Alloc宏。
定義形式如下所示:

/*
** Type for memory-allocation functions
*/
typedef void * (*lua_Alloc) (void *ud, void *ptr, size_t osize, size_t nsize);

參數說明如下所示:
.ud表示用戶數據。
.ptr表示申請或者回收內存塊的地址。
.osize表示原始內存塊的大小。當ptr不爲NULL時,osize就是ptr指向內存塊的大小;否則osize就是用來存放一些調試信息,如:表示創建對象的類型(lua.h中定義的base type類型,如:LUA_TSTRING,LUA_TTABLE,LUA_TFUNCTION, LUA_TUSERDATA,LUA_TTHREAD)。
.nsize表示申請內存塊的大小。當nsize爲0時,如果ptr爲NULL就直接返回NULL;否則就會回收ptr指向內存塊的地址並返回NULL。當nsize不爲0時,如果ptr爲NULL就可以直接申請nsize大小的新內存塊並返回該新內存塊地址;否則就會對ptr指向內存塊進行重新分配並返回新的內存塊的地址。如果申請內存塊失敗就返回NULL。
2>.lua_getallocf(ud)函數可以用來獲取lua虛擬機上的內存分配函數。當ud不爲NULL時就會將內存分配函數中的用戶數據填充到ud上。
3>.lua_setallocf(ud)函數可以用來設置lua虛擬機上的內存分配函數,並且該內存分配函數的用戶數據爲ud。
4>.lauxlib.h中提供的luaL_newstate函數在創建lua虛擬機的同時調用了默認內存分配函數l_alloc,且該函數實現代碼如下:

// 由ISO C標準會託管free(NULL)以及realloc(NULL, nsize)等價於malloc(nsize)的正確性。
static void *l_alloc (void *ud, void *ptr, size_t osize, size_t nsize) {
  (void)ud; (void)osize;  /* not used */
  if (nsize == 0) {
    free(ptr);
    return NULL;
  }
  else
    return realloc(ptr, nsize);
}

5>.lua.h中提供的lua_newstate函數在創建lua虛擬機時需要自己指定lua_Alloc內存分配函數。且一個優秀的內存分配函數具有以下特性:
1>>.能夠提供常規的內存申請和回收操作。
2>>.新的分配函數必須能夠對舊的分配函數申請的內存進行回收。
3>>.緩存空餘內存以達到重用的目的。
4>>.多線程環境下,可以讓每個lua狀態從私有的內存池中申請內存,從而避免在分配函數中每個lua狀態爲了線程同步而造成的額外開銷。

5.棧和c進行數據交換時,常用的lua c api如下:
1>.lua_checkstack(n)函數確保棧上至少有n個額外空位,如果空位不夠就嘗試擴大到指定n個空位,最終空位還是不夠時就返回假,否則就返回真。
2>.lua_push*函數用來向棧中壓入指定*(可以爲boolean,number,thread等lua基礎數據類型)類型值,此時必須確保棧上有足夠的空間。
3>.lua_type函數用來獲取指定索引位置元素的類型。
4>.lua_typename函數用來獲取指定類型的名稱。
5>.lua_is*函數用來判定指定索引位置元素是否爲指定*(可以爲boolean,number,thread等lua基礎數據類型)類型。
6>.lua_to*函數用來將指定索引位置元素轉換爲指定*(可以爲boolean,number,thread等lua基礎數據類型)類型值。

6.棧和c進行通用操作時,常用的lua c api如下:
1>.lua_gettop函數用來獲取棧頂元素索引值,也就是獲取棧中元素總個數。
2>.lua_settop函數用來設置棧頂元素索引值,其中大於新棧頂元素索引值的部分會被清空回收掉,小於新棧頂元素索引值部分會填充nil。
3>.lua_pop(n)函數用來從棧頂彈出n個元素,等價於lua_setpop(L, -(n) - 1)。
4>.lua_pushvalue函數用來將指定索引位置處的元素副本壓入棧頂。
5>.lua_replace函數用來將棧頂元素替換指定索引位置處的元素,然後將棧頂元素彈出。
6>.lua_copy(fromidx, toidx)函數用來將指定fromidx索引位置處的元素替換另一指定toidx索引位置處的元素。
7>.lua_rotate(idx, n)函數用來旋轉棧中元素。當n爲正值時就從棧頂元素索引位置開始,向下找到n個元素,並將這n個元素插入到指定idx索引位置處,原先idx索引位置以及剩餘元素全部上移一位。當n爲負值時就從idx索引位置開始向上找|n|個元素,然後以找到元素索引位置開始到棧頂元素索引位置結束的所有元素插入到指定idx索引位置處,原先idx索引位置以及剩餘元素全部上移一位。
8>.lua_remove(idx)函數用來刪除指定索引位置處的元素,並將該位置以上的元素下移一位來填充。等價於lua_rotate(L, idx, -1) lua_pop(1)。
9>.lua_insert(idx)函數用來將棧頂元素旋轉到指定索引位置處,原先idx索引位置以及剩餘元素全部上移一位,等價於lua_rotate(L, idx, 1)。
10>.lua_error函數會以棧頂元素作爲錯誤對象,拋出一個lua錯誤。
11>.lua_atpanic函數會設置一個新的緊急處理函數並返回舊的緊急處理函數。當出現異常退出時,會調用這個新的緊急處理函數,處理完後可以用舊的緊急處理函數來還原設置。
12>.lua_call函數會以不安全模式調用棧頂函數。當被調用棧頂函數發生錯誤時,錯誤信息通過 longjmp一直上拋並最終調用緊急處理函數後退出程序。

7.c調用lua操作時,具有以下特性:
1>.luaL_loadfile函數用來加載指定路徑的lua文件,然後將該lua文件編譯成代碼塊但不運行。加載成功時返回0並且將代碼塊封裝成lua函數壓入到棧頂;否則返回非0值並將錯誤信息壓入棧頂。
2>.lua_pcall(nparam, nresult, nerror)函數會以保護模式調用棧中函數。流程如下:
1>>.參數nparam表示被調函數所需要參數的個數。在壓入被調函數後,需要緊接着壓入nparam個參數。
2>>.參數nresult表示被調函數返回結果的個數。在被調函數返回結果不足nresult部分就填nil,大於nresult部分就捨棄。
3>>.nerror表示異常處理函數在棧中索引位置。爲0時表示沒有異常處理函數;否則表示有異常處理函數,且該函數應該位於被調函數下面。
4>>.當被調函數調用結束後會將被調函數以及參數從棧中彈出。運行成功時返回LUA_OK狀態碼以及將nresult個返回值壓入到棧中;運行失敗時返回異常錯誤狀態碼以及將異常錯誤信息(LUA_ERRMEM或者LUA_ERRERR或者沒有異常處理函數時就是被調函數自身返回的異常信息;否則就是異常處理函數處理後的異常信息)壓入到棧中。
3>.lua_getglobal(var)函數用來將lua代碼塊中的全局變量var的值壓入棧,並返回該值的類型(LUA_TBOOLEAN,LUA_TNUMBER,LUA_TTABLE)。
4>.lua_setglobal(var)函數會將棧頂元素值彈出,然後將lua代碼塊中的全局變量var的值設置爲棧頂元素值。
5>.lua_createtable(narr, nrec)函數用來創建一個空表,然後將這個空表壓入棧中。其中該空表的整數索引部分預分配narr個;該空表的哈希索引部分預分配nrec個。lua_newtable等價於lua_createtable(0, 0)。
6>.lua_gettable (idx)函數用來獲取指定索引位置idx處的表,然後將棧頂的鍵彈出並且獲取表中該鍵的值,最後將該值壓入棧頂並返回該值類型。
7>.lua_getfield(idx, key)函數用來獲取指定索引位置idx處的表,然後將表中指定鍵key的值壓入棧頂並返回該值得類型。
8>.lua_settable(idx)函數用來獲取指定索引位置idx處的表,然後將棧頂的值和棧頂下一位置的鍵組成鍵值對一起彈出,然後用該鍵值對修改表。
9>.lua_setfield(idx, key)函數用來獲取指定索引位置idx處的表,然後將棧頂的值彈出並將該值和指定鍵key組成鍵值對,然後用該鍵值對修改表。
10>.c中調用lua函數時基本流程爲:根據lua_pcall函數中是否指定異常信息處理函數索引來決定是否通過lua_pushcfunction函數來壓入異常信息處理函數 -> 通過lua_getglobal函數來將lua代碼塊中的全局函數壓入棧中 -> 根據lua_pcall函數中參數個數來決定是否通過lua_push*(*可以爲number,string,integer等)函數來壓入參數值 -> 使用lua_pcall函數調用壓入棧中的lua函數 -> 當運行成功時返回LUA_OK狀態碼以及從棧中取出lua_pcall函數中指定的返回值數量個結果;當運行失敗時返回異常錯誤狀態碼以及從棧中取出異常信息。
11>.c中調用lua函數時通用代碼爲:

// func:lua代碼塊中的函數名
// sig:描述參數類型和結果類型的字符串。其中>左邊表示參數類型,右邊表示返回結果類型
// ...:參數列表以及存放結果的指針
void call_va(lua_State* L, const char* func, const char* sig, ...)
{
	va_list vl;
	va_start(vl, sig);
	
	// 壓入lua函數到棧中
	lua_getglobal(L, func);
	// 循環壓入參數列表到棧中
	int narg = 0;
	for (narg = 0; *sig; narg++)
	{
		// 擴展空間到top+1
		luaL_checkstack(L, 1, "too many arguments");

		switch (*sig++)
		{
		case 'd':
			lua_pushnumber(L, va_arg(vl, double));
			break;
		case 'i':
			lua_pushinteger(L, va_arg(vl, int));
			break;
		case 's':
			lua_pushstring(L, va_arg(vl, char*));
			break;
		case '>':
			goto endargs; // 從循環中跳出
		default:
			error(L, "invalid option (%c)", *(sig - 1));
			break;
		}
	}
	endargs: {}

	// 獲取返回結果個數
	int nres = strlen(sig);
	// 調用lua函數
	if (lua_pcall(L, narg, nres, 0) != LUA_OK)
	{
		error(L, "error calling '%s':%s", func, lua_tostring(L, -1));
	}
	// 循環接收返回結果
	nres = -nres;
	while (*sig)
	{
		switch (*sig++)
		{
		case 'd': {
			int isnum;
			double n = lua_tonumberx(L, nres, &isnum);
			if (!isnum)
			{
				error(L, "error result type");
			}
			*va_arg(vl, double*) = n;
			break;
		}
		case 'i': {
			int isnum;
			int n = lua_tointegerx(L, nres, &isnum);
			if (!isnum)
			{
				error(L, "error result type");
			}
			*va_arg(vl, int*) = n;
			break;
		}
		case 's': {
			const char* n = lua_tostring(L, nres);
			if (n == NULL)
			{
				error(L, "error result type");
			}
			*va_arg(vl, const char**) = n;
			break;
		}
		default:
			error(L, "invalid option (%c)\n", *(sig - 1));
			break;
		}

		nres++;
	}

	va_end(vl);
}

8.lua調用c操作時,具有以下特性:
1>.lua調用c函數的原型爲lua_CFunction。宏定義如下:

/*
** Type for C functions registered with Lua
*/
typedef int (*lua_CFunction) (lua_State *L);

其中參數值爲lua虛擬機對象;返回值爲一個整型值,代表函數返回值的個數。
2>.lua調用c函數有2種方式。如下所示:
1>>.使用lua_pushcfunction函數將lua_CFunction壓入棧頂,然後使用lua_setglobal(var)函數來將棧頂的lua_CFunction賦值給lua中的全局變量var。這樣就可以在lua中以全局變量var的形式來調用lua_CFunction。
2>>.在lua中以"模塊名.lua函數名"的形式來調用lua函數名所關聯的lua_CFunction。流程如下:
1>>>.新建一個c模塊文件。該文件名沒有限制,但是一般都是"l+模塊名+lib"的形式命名。
2>>>.在c模塊文件中定義要被lua調用的static lua_CFunction。該函數名沒有限制,但是一般都是"模塊名_lua函數名"的形式命名。
3>>>.在c模塊文件中定義一個static const struct luaL_Reg數組變量。該數組變量名沒有限制,但是一般都是"模塊名lib"的形式命名。其中luaL_Reg的定義如下所示:

typedef struct luaL_Reg {
  const char *name;
  lua_CFunction func;
} luaL_Reg;

name表示lua函數名,func表示c函數地址。數組最後一行需要加入一個結束哨兵{NULL, NULL}。
4>>>.在c模塊文件中定義一個LUAMOD_API lua_CFunction作爲打開函數。該函數名一般都是"luaopen_模塊名"的形式命名。該函數內部主要是用luaL_newlib函數創建一個新的表,然後用記錄lua函數名和lua_CFunction關係的luaL_Reg數組變量去填充該表。
5>>>.如果將c模塊文件編譯成靜態庫或者動態庫的話,就要將生成的庫文件放入到lua的庫查找路徑中。然後lua端在調用"require 模塊名"時會從庫路徑中查找"模塊名"對應的庫文件。找到庫文件後就調用庫文件中的"luaopen_模塊名"打開方法來將lua函數名和lua_CFunction以表的形式賦值給"模塊名"全局變量。此時就可以通過模塊名調用lua函數名關聯的lua_CFunction。
6>>>.如果將c模塊文件和lua解釋器(一般爲lua.c文件)一起編譯的話,就要在lualib.h中定義模塊名的宏和聲明c模塊的打開函數,模塊名的宏命名沒有限制,但是一般都是"LUA_模塊名NAME"的形式命名。然後將模塊名的宏和c模塊的打開函數作爲條目加入到linit.c文件中的loadedlibs數組中。最後在調用luaL_openlibs函數解析loadedlibs數組時,該函數內部會使用luaL_requiref函數將lua函數名和lua_CFunction以表的形式賦值給模塊名全局變量。此時就可以通過模塊名調用lua函數名關聯的lua_CFunction。
3>.lua每次調用c函數時都會開闢一個私有局部棧。此時c函數中常見的操作如下:
1>>.lua傳遞給c函數的參數值列表會從棧底依次向上壓入到棧中。此時可以使用luaL_check*(*可以爲number,integer等)函數來檢查棧中指定索引位置的參數值是否是指定類型*,如果不是就會拋出異常,否則就會返回該參數值。
2>>.c函數傳遞給lua的返回值列表會依次使用lua_push*(*可以爲number,integer等)函數來向棧頂壓入返回值。
3>>.c函數返回值個數爲0的話,lua端就直接清空棧;否則lua端會從棧頂開始依次向下獲取指定返回值個數的元素值,然後將獲取的元素值按照逆序的方式依次賦值給lua端的接收變量,最後清空棧。
4>.lua協程的resume和yiled狀態之間存在調用c函數時,由於c函數的調用信息會被丟失從而導致c函數調用異常報錯。此時可以使用延續函數結合lua_pcallk函數來解決這個問題,延續函數的定義如下:

/*
** Type for continuation functions
*/
typedef int (*lua_KFunction) (lua_State *L, int status, lua_KContext ctx);

其中第一個參數表示lua虛擬機,第二個參數表示棧頂函數調用的狀態碼,第三個參數表示上下文整形值。返回值爲一個整型值,代表函數返回值的個數。
5>.lua_pcallk(nparam, nresult, error, context, kfunc)函數會以保護模式調用棧頂函數。當棧頂函數不需要yield時,lua_pcallk的行爲和lua_pcall完全一致;否則就會將狀態碼(此時爲LUA_YIELD或者異常錯誤碼)以及上下文context作爲參數來調用延續函數kfunc。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章