Lua 與 Cplusplus 之間的錯誤處理

lua-users home
wiki

此頁面會說明一些與透過 C++/Lua 邊界來處理錯誤相關的方法和問題。

[注意:此頁面算是進行中的專案。歡迎提供其他說明。]

連結

如果您使用 C 編譯 Lua,您必須將 #include <lua.h> 放在 extern "C" 中,或使用等效的 #include <lua.hpp>,否則您會得到連結器錯誤。

// alternately do #include <lua.hpp>
extern "C" {
#include <lua.h>
}

使用 longjmp 或例外編譯 Lua

Lua 本身通常在 C 下編譯,但也可以改在 C++ 下編譯。如果在 C 下編譯,Lua 會使用 [longjmp]' 來實作錯誤處理 (lua_error)。如果在 C++ 下編譯,Lua 預設會使用 C++ 例外。請參閱 luaconf.hLUAI_THROW 的宣告。同時也請參閱 LuaList:2007-10/msg00473.html

將使用例外的 C++ 程式碼與使用 longjump 的 Lua 結合

C++ 例外處理 (正確地展開堆疊並呼叫解構函數) 與僅拋出堆疊的 Lua longjmp 之間有衝突,因此如果 Lua 在 C 下編譯,您必須更小心,以確保所有必要的 C++ 解構函數 都已呼叫,避免記憶體或資源外洩。

當 C++ 將 Lua 呼叫為擴充功能時,Lua 作業通常 (但並非總是) 需要包裝在 pcall 中為 lua_CFunction。例如,請參閱 [PIL 25.2] 或 PIL2 25.3。(關於這些條件的詳細說明,Rici 會在下面提供。) 通常這個 lua_CFunction 僅由一個呼叫者使用。因此,將 lua_CFunction 做為呼叫函式的區域變數 (如閉包) 可能很有用。在 C++ 中,lua_CFunction 可以定義在一個結構中,如下所示

int operate(lua_State * L, std::string & s, int x, int y) {
    std::string msg = "Calling " + s + "\n";  // can raise exception; must be destroyed
    cout << msg;
    // caution: this code by raise exceptions but not longjump.
    struct C {
        static int call(lua_State * L) {
            // caution: this code may longjump but not raise exceptions.

            C * p = static_cast<C*>(lua_touserdata(L, 1));
            assert(lua_checkstack(L, 4));
            lua_getglobal("add"); // can longjump
            assert(lua_isfunction(L, -1));
            lua_pushstring(L, s); // can longjump
            lua_pushnumber(L, p->x);
            lua_pushnumber(L, p->y);
            lua_call(L, 3, 1);    // can longjump
            p->z = lua_tonumber(L, -1); assert(lua_isnumber(L, -1));
            return 0;
        }
        const char * s; int x; int y; int z;
    } p = {s.c_str(), x, y, 0};
    int res = lua_cpcall(L, C::call, &p); // never longjumps
    if (res != 0) {
        handle_error(L);  // do something with the error; can raise exception
        //note: we let handle_error do lua_pop(L, 1);
    }
    return p.z;
}

現在,錯誤處理乍看之下有些棘手。lua_getgloballua_pushstringlua_call 呼叫可能會產生 lua_error(),也就是說,如果 Lua 在 C 中編譯,就會產生 longjmp。保護呼叫外部的 lua_cpcall 是安全的,因為它不會產生 lua_error() (不像使用在 lua_pushcfunction 之後的 lua_pcall,可能會在記憶體分配失敗時產生 lua_error)。與 C++ 例外處理不同,longjmp 會略過堆疊中物件的任何解構函數 (通常用於 C++ 中的 RAII)。

另一個問題是如果 lua_cpcall 回傳失敗結果,我們該如何處理?我們有可能直接在原地處理錯誤、lua_pop 它,然後繼續執行。但更常需要在呼叫鏈中更淺的位置處理錯誤。一種更好的解決方案可能是將錯誤訊息留在 Lua 堆疊中,並確保在 catch 區塊中消耗時執行 lua_pop

#include <stdexcept>
#include <boost/shared_ptr.hpp>
/**
 * C++ exception class wrapper for Lua error.
 * This can be used to convert the result of a lua_pcall or

 * similar protected Lua C function into a C++ exception.
 * These Lua C functions place the error on the Lua stack.
 * The LuaError class maintains the error on the Lua stack until
 * all copies of the exception are destroyed (after the exception is
 * caught), at which time the Lua error object is popped from the
 * Lua stack.
 * We assume the Lua stack is identical at destruction as
 * it was at construction.
 */
class LuaError : public std::exception
{
private:
    lua_State * m_L;
    // resource for error object on Lua stack (is to be popped
    // when no longer used)
    boost::shared_ptr<lua_State> m_lua_resource;
    LuaError & operator=(const LuaError & other); // prevent
public:
    // Construct using top-most element on Lua stack as error.
    LuaError(lua_State * L);
    LuaError(const LuaError & other);
    ~LuaError();
    virtual const char * what() const throw();
};

    static void
LuaError_lua_resource_delete(lua_State * L)
{
    lua_pop(L, 1);
}

LuaError::LuaError(lua_State * L)
    : m_L(L), m_lua_resource(L, LuaError_lua_resource_delete)
{

}

LuaError::LuaError(const LuaError & other)
    : m_L(other.m_L), m_lua_resource(other.m_lua_resource)
{

}
    const char *
LuaError::what() const throw()
{
    const char * s = lua_tostring(m_L, -1);
    if (s == NULL) s = "unrecognized Lua error";
    return s;
}
LuaError::~LuaError()
{
}

範例用法

for(int n=1; n < 100; n++) {
  try {
    string s = "123123123123123123";  // note: may throw bad_alloc
    // ...
    int res = lua_cpcall(L, call, NULL);
    if (res != 0) throw LuaError(L);
  }
  catch(exception & e) {
    cout << e.what() << endl;
  }
}

另外一個例子是如果 Lua 執行一個 C 函式,而此函式執行一個呼叫 Lua 程式碼的 C++ 程式碼。在這種情況下,C++ 程式碼可能 pcall 成 Lua 並將任何錯誤訊息轉換成一個 C++ 例外,而這個例外會傳播到 C 函式。C 函式隨後需要將 C++ 例外轉換成一個「lua_error()」,而這個「lua_error()」會 longjmp 到 Lua。如果呼叫鏈中的 C++ 程式碼以 RAII 方式分配記憶體,才需要轉換成 C++ 例外。

DavidManura

引發錯誤的 Lua C API 函式

有很多 API 函式永遠不會引發 Lua 錯誤。在參考手冊中標識了會引發錯誤的 API 函式,5.1.3 版本起。首先,沒有任何堆疊調整函式會引發錯誤;這包括「lua_pop」、「lua_gettop」、「lua_settop」、「lua_pushvalue」、「lua_insert」、「lua_replace」和「lua_remove」。如果您提供不正確的索引給這些函式,或者您沒有呼叫「lua_checkstack」,那麼您可能會收到垃圾或段落錯誤,但不會是 Lua 錯誤。

沒有任何用來傳送基本資料的函式(「lua_pushnumber」、「lua_pushnil」、「lua_pushboolean」和「lua_pushlightuserdata」)會引發錯誤。傳送複合物件(字串、表格、閉包、執行緒、完整使用者資料)的 API 函式可能會引發記憶體錯誤。沒有任何類型探詢函式(「lua_is*」、「lua_type」和「lua_typename」)會引發錯誤,設定/取得元表格和環境的函式也不會。lua_rawget、lua_rawgeti 和 lua_rawequal 也永遠不會引發錯誤。除了 lua_tostring 之外,沒有任何 lua_to* 函式會引發錯誤,透過先使用 lua_type 檢查物件是否為字串,您可以避免 lua_tostring 引發記憶體不足錯誤。lua_rawset 和 lua_rawseti 可能會引發記憶體不足錯誤。可能會引發任意錯誤的函式是那些可能會呼叫元方法的函式;這些函式包括所有非原始的 get 和 set 函式,以及 lua_equal 和 lua_lt。

如果您要創造一個完整的使用者資料,它是一個可能需要釋放的某個物件,您應該先創造使用者資料,並清除其各種區塊,然後附加一個正確的元表格,並加上一個 __gc 元方法。然後您應該創造可能需要釋放的物件,並將它放入使用者資料中。這樣做會避免資源外洩,因為如果稍後引發錯誤,__gc 方法最終會被呼叫。一個好的例子是標準的 liolib.c,它使用此策略來避免檔案描述詞外洩。-- RiciLake

另請參閱


近期變更 · 喜好設定
編輯 · 歷史
於格林威治時間 2008 年 10 月 19 日 上午 4:18 最後編輯 (diff)