捕捉 Lua 例外

lua-users home
wiki

作者: KevinBaca

Lua 版本: 5.0 (版本說明)

本文提供一組修正程式,用於簡化 C 和 C++ 中的 lua 例外處理。

簡介

在 Lua 腳本中產生的例外有兩種可能的結果

1. 它傳播到呼叫鏈的上方。Lua 執行時期會引發錯誤,並且通常會結束程式。

2. 它會在呼叫鏈中已執行受保護呼叫的時間點停止。然後執行受保護呼叫的函式可以處理例外。

Lua 的 C API 提供函式 lua_pcall() 用於執行受保護呼叫 [1]

Lua 實作

當程式「引發」例外時,它會進行「非區域跳躍」。它會跳躍到可能不在同個區塊、同個函式、甚至同個模組的程式碼時間點。Lua 手冊將這些非區域跳躍的目的地稱為「復原點」。

由於 Lua 執行時期是用 C 編寫,因此它無法使用 C++ 例外,所以 Lua 會使用 C 函式 setjmp()longjmp() 實作此行為 [2]

注意:從 Lua 5.1 開始,當以 C++ 編譯時,Lua 會使用原生 C++ 例外,而不是 setjmp()longjmp()

setjmp()/longjmp() 的問題

setjmp()/longjmp() 慣用寫法非常有效率且易於使用。它用於許多 C 函式庫,以提供例外處理支援。但是,它並非沒有問題。

setjmp() 會將 CPU 執行環境的目前狀態(暫存器、堆疊指標等)儲存在一個緩衝區中。使用該緩衝區呼叫 longjmp(),會復原保存的執行環境。基本上,longjmp() 會「時光倒流」回到 setjmp() 呼叫。程式會從那個時間點繼續執行,而它的執行環境看起來像沒有任何變動。

jmp_buf jb;

char* mystr = "Original Value";

/* start */
int i_except = setjmp( jb );

if( 0 == i_except ) /* try */
{
    mystr = "New Value";

    /* Throw an exception.
       Jumps back to "start", and setjmp() returns 33
     */
    longjmp( jb, 33 );
}
else /* catch */
{
    /* Handle the exception */
}

print( mystr );

許多編譯器會最佳化對區域變數的參照,方法是將它們的值快取到暫存器中。當程式碼執行順序執行時,這個方式很好。如果編譯器用盡暫存器,它可以將值儲存在記憶體中,然後稍後載入該值。但是,呼叫 longjmp() 會將 setjmp() 保存的暫存器狀態回復。如果編譯器已將變數的值快取在暫存器中,而它的值在 setjmp() 之後但在 longjmp() 之前變動,那麼在 longjmp() 復原其舊值時,新的值就會遺失。

在前述範例中,變數 mystr 容易發生這個問題。呼叫 print() 時,mayvar 可能包含「新值」,或可能包含「原始值」,這取決於編譯器如何最佳化程式碼。

-- 實際上,上述範例中 setjmp 呼叫的脈絡並不符合規定,而且產生的行為未定義。以下的範例在這方面是可以接受的。 -- Wim Couwenberg

這個問題可以使用謹慎的程式設計來減輕。解決方案有

1. 不要在呼叫 setjmp()/longjmp() 之間變更區域變數。

2. 在 setjmp()/longjmp() 區塊外部宣告的變數應該宣告為 volatile

在上述範例中,將 mystr 宣告為 volatile 可以防止編譯器快取其值到暫存器

char* volatile mystr;

宣告一個 volatile int

volatile int myint;

請注意,函數參數必須包含在 volatile 變數中

int func( int* volatile pInt ) /* pointer declared volatile */
{
    jmp_buf jb;

    if( 0 == setjmp( jb ) )
    {
        pInt++;
        longjmp( jb, 97 );
    }
    else
    {
    }
}


RecentChanges · 喜好設定
編輯 · 歷程
上次編輯 2009 年 7 月 3 日 上午 9:00 GMT (差異)