Lua異常處理


Lua代碼運行過程中,可能會出現異常狀態,比如非法地址訪問、遇到未定義符號、或者斷言失敗等,由於異常出現的地方不定,所以我們需要用一些方法來獲取異常信息,找到出現異常的原因。

在C語言代碼中處理Lua腳本運行產生的異常:

要能在發生異常后可以在C語言代碼中獲取到異常信息,就必須通過lua_pcall,或者lua_pcallk函數來運行Lua代碼,如果直接使用lua_call函數來運行Lua代碼,當發生異常后,Lua解析器不會保留異常信息,而是會調用系統函數abort導致整個程序運行中止。

對於lua_pcalllua_pcallk這兩個函數,前者實際上是一個宏,是對后者做了一下簡單封裝后的結果,后者用得不多,這里不再做介紹。故名思意,lua_pcall函數是以保護模式運行Lua代碼,當Lua代碼中遇到異常時,lua_pcall函數不會直接調用abort函數導致程序卡死,而是會進行異常處理,然后恢復運行Lua代碼之前的狀態,然后返回。

在調用lua_pcall函數前,可以通過lua_pushcfunction函數將自己的異常處理函數壓棧,然后通過lua_pcall函數的參數指定異常處理函數在棧中所在的位置,然后當Lua腳本運行出錯時,這個自定義的異常處理函數會被調用,這時候可以繼續在自定義異常處理函數中調用luaL_traceback函數,這個函數會獲取到更多的錯誤信息,然后將錯誤信息以一個字符串的形式壓棧,然后打印棧中的信息就可以得到詳細的錯誤信息了。

當然,這個自定義異常處理函數不是必須的,如果沒有自定義異常處理函數,那么解析器會將一個稍微簡單一點的錯誤信息以字符串的形式壓棧,當lua_pcall函數返回時,判斷它的返回值,如果不為LUA_OK,就可以通過以字符串的形式將棧底打印出來獲取錯誤信息。但需要注意的是,這兩種方法不可以同時使用,換一種說法,如果使用了自定義異常處理函數,那么當lua_pcall函數返回后,棧頂的值就不會包含錯誤信息,因為在調用自定義異常處理函數的過程中,棧頂的錯誤信息會被覆蓋掉。

說那么多,還是得來點代碼。

Main.c:

int main(void)

{

    lua_State* L = NULL;

    int status;

    L = luaL_newstate();

    luaL_openlibs(L);

    luaL_loadfile(L, "test_01.lua");

    status=lua_pcall(L, 0, LUA_MULTRET,0);

lua_remove(L, 1);/*將ExceptionHandle從棧中刪掉,注意這里不用lua_pop,而是lua_remove是因為此時ExceptionHandle不一定在棧頂*/

    if (status != LUA_OK)

    {

        /*將錯誤代碼和棧頂的字符串打印出來*/

        printf("error code:%d,msg:%s\r\n",status,lua_tostring(L,-1));

    }

    lua_close(L);

    return 0;

}

 

Test_01.lua

Hello()

 

運行結果:

 

上面的代碼中,main函數通過lua_pcall函數運行Lua腳本,Lua腳本中調用Hello函數,但是Hello函數並沒有被定義,所以在運行時必定會產生異常,這里沒有使用自定義異常處理函數,而是等lua_pcall返回后再打印棧頂獲取異常信息。(C語言不同,這種符號未定義的情況在Lua編譯器編譯的時候並不會報錯,但運行的時候要還是找不到這個函數,那就不得不報錯了。)

再試一下使用自定義異常處理函數的情況,這里Main.c修改如下,Lua代碼不修改。

Main.c:

int ExceptionHandle(lua_State* L)

{

    const char* msg = lua_tostring(L, -1);

    luaL_traceback(L, L, msg, 1);

    printf("%s\r\n", lua_tostring(L, -1));

    return 0;

}

int main(void)

{

    lua_State* L = NULL;

    int status;

    L = luaL_newstate();

    luaL_openlibs(L);

    lua_pushcfunction(L, ExceptionHandle);

    luaL_loadfile(L, "test_01.lua");

    status=lua_pcall(L, 0, LUA_MULTRET,1);

lua_remove(L, 1);/*將ExceptionHandle從棧中刪掉,注意這里不用lua_pop,而是lua_remove是因為此時ExceptionHandle不一定在棧頂*/

    if (status != LUA_OK)

    {

        /*將錯誤代碼和棧頂的字符串打印出來*/

        printf("error code:%d,msg:%s\r\n",status,lua_tostring(L,-1));

    }

    lua_close(L);

    return 0;

}

 

運行結果:

 

這里首先調用lua_pushcfunction(L, ExceptionHandle),將自定義異常處理函數壓棧,由於是第一次壓棧,所以ExceptionHandle函數在棧中的位置為1,然后加載Lua文件,調用lua_pcall函數運行Lua腳本,注意這里調用lua_pcall函數時的第四個參數為1,這個1,就是自定義異常處理函數在棧中的位置。

看結果,在自定義異常處理函數中,首先獲取棧頂的值,並將其轉換成字符串,這個字符串就是前面說的,Lua解析器壓入的簡單錯誤信息,獲取到這個字符串后,再調用luaL_traceback函數獲取相信錯誤信息,最后打印棧頂字符串,將詳細錯誤信息打印出來,到這里,運行結果中的第1、2、3行便是運行的結果,然后lua_pcall函數退出后,主函數試圖再次打印棧頂字符串,但打印出來的確實null(不應該是燙燙燙嗎。。。),為啥?因為這時候棧頂已經不包含錯誤信息了,其實,如果細心看,這里運行結果的第一行和上面打印的錯誤信息是一樣的,因為luaL_traceback函數將簡單錯誤信息和詳細錯誤信息合在一起了

問題:

為啥用了自定義異常處理函數后,Lua解析器存放在棧中的簡單錯誤信息會被覆蓋掉,按道理,調用一個函數,被調用的函數不應該影響調用它的函數的棧才對,並且調用完函數后,棧的狀態應該和調用前完全一樣才對,這個讓我糾結了很久,因為一開始按照這個思路,定義了自定義異常處理函數,卻沒有在異常處理函數中將棧中的信息打印出來,而是等lua_pcall退出后才打印,總是出不來結果,后來還是找到了問題。

首先當異常發生后,Lua解析器會做什么?要知道發生異常后Lua解析器會做啥,就得知道異常會在哪里產生,這個問題簡單,當然是在Lua代碼執行的某條字節碼時產生,Lua解釋器會通過void luaV_execute (lua_State *L)函數執行Lua字節碼。

上圖是前面Lua代碼的匯編代碼,只有3條,第一條找到Hello函數,第二條執行它,第三條指令返回。經過測試,異常在第二條指令執行的時候產生,執行第二條指令時,會調用luaD_precall來執行對應的函數(只有C函數是在這個函數里被直接調用的)luaD_precall代碼如下(刪除了部分代碼)

我們要看的就是default分支,它會調用tryfuncTM函數去繼續尋找元方法(這里存疑),在尋找元方法之前,該函數首先會檢測func是不是一個函數,這里當然不是,當確定func不是函數后,tryfuncTM函數便會拋出異常,代碼如下:

static void tryfuncTM (lua_State *L, StkId func)

{

const TValue *tm = luaT_gettmbyobj(L, func, TM_CALL);

/*do something*/

if (!ttisfunction(tm))

luaG_typeerror(L, func, "call");/*拋出異常*/

/*do something*/

}

接下來luaG_typeerror函數會函數會給出錯誤原因,然后將錯誤原因傳給luaG_runerror函數,luaG_runerror函數調用luaG_addinfo函數添加文件名和行號,然后將這些信息以一個字符串的形式壓棧,這個字符串就是前面說的簡單錯誤信息,最后調用luaG_errormsg函數,而luaG_errormsg函數中,我們可以找到簡單錯誤信息被覆蓋的原因。

三個變量:文件名、行號、錯誤信息,組成了簡單錯誤信息。

接下來看luaG_errormsg函數,這個函數解釋了為啥簡單錯誤信息會被自定義異常處理函數覆蓋。

首先這個函數會判斷L->errfunc是否為0,這這個errfunc和lua_pcall的第4個參數有關,具體關系為L->errfunc=16*arg;(16為棧中一個元素的大小,arg為第四個參數值),如果arg不為0,則L->errfunc也不為0,這表示設置了自定義異常處理函數,進入if后,會調用restorestack找到壓倒棧中的異常處理函數,然后調用setobjs2s調整棧數據,setobjs2s(L,A,B)就是將L的棧中,B地址指向的變量中數據給A地址指向的變量,注意,這里L->top-1地址指向的數據包含了簡單錯誤信息,調整完后會調用luaD_callnoyield函數來掉用異常處理函數,畫個圖吧。

從上圖可以看出,當執行完調整棧數據的三行代碼后,簡單錯誤信息被往上移了一格,而當 luaD_callnoyield(L, L->top - 2, 1);執行完后,棧指針會還原到調整棧數據之前的狀態,這就回到了上圖中的第二步的那種狀態,由於Lua中的棧是空遞增的棧,所以這時候簡單錯誤信息無法被訪問到,這里做一個小測試,將棧指針自己強行加1,看能不能打印出簡單錯誤信息。

出來了,和預想的一樣,但在寫文檔的時候又發現一個問題,在luaG_errormsg函數中對L->top強行加一可以得到預想中的結果,但在main函數中,等lua_pcall函數退出后再將L->top加一卻出不來預想的結果,一頓跟蹤,發現了問題的真相,lua_pcall函數經過層層調用,會調用到luaD_rawrunprotected函數,這個函數會繼續調用其它函數來運行Lua腳本,函數的大致結構如下:

int luaD_pcall (lua_State *L, Pfunc func, void *u,ptrdiff_t old_top, ptrdiff_t ef)

{

int status;

/* do something:保存調用前的狀態 */

L->errfunc = ef;

status = luaD_rawrunprotected(L, func, u);/*執行Lua腳本 */

if (status != LUA_OK)

{

/* 如果執行得有問題 */

/* do something:獲取調用前的棧狀態 */

seterrorobj(L, status, oldtop);

/* do something:還原調用前的狀態 */

}

L->errfunc = old_errfunc;

return status;

}

當錯誤發生后,luaD_rawrunprotected函數的返回值是不等於LUA_OK的,也就是運行Lua腳本失敗了,它會調用seterrorobj函數將錯誤信息移到調用前的棧指針所指向的那個個位置,然后將L->top強制設置為調用前棧指針oldtop+1seterrorobj函數代碼如下:

static void seterrorobj (lua_State *L, int errcode, StkId oldtop)

{

switch (errcode)

{

case LUA_ERRMEM:

{

/* do something */

break;

}

case LUA_ERRERR:

{

/* do something */

break;

}

default:

{

setobjs2s(L, oldtop, L->top - 1); /* 將錯誤信息壓倒舊棧指針指向的位置 */

break;

}

}

L->top = oldtop + 1;/* 強制恢復棧頂指針,並將其加一 */

}

 

可以看到,seterrorobj函數中有3個分支,分別是LUA_ERRMEMLUA_ERRERRdefault,這個分支代表Lua5.3參考手冊中的5中錯誤代碼:

這是在luaG_errormsg函數中對L->top強制加一的情況。

這是在main函數中等lua_pcall函數退出后再將L->top加一的情況。

LUA_ERRMEMLUA_ERRERR有單獨的分支,其它的錯誤代碼都會進入default分支,調用seterrorobj函數。腳本中調用未定義函數屬於運行時出錯,所以也會進入default分支。這就解釋了為啥在luaG_errormsg函數中對L->top強行加一可以得到預想中的結果,但在main函數中,等lua_pcall函數退出后再將L->top加一卻出不來預想的結果,因為當luaG_errormsg函數退出后會做恢復原狀態的工作,所以到main函數中的棧指針已經不是luaG_errormsg函數中的棧指針了,所以在main函數中對棧指針加一是不可行的。想想也應該,保護調用按道理也應該分三段:保存調用前狀態、調用需要調用的函數、如果出錯恢復出錯前狀態

至此,這個問題就理清了。


免責聲明!

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



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