Lua 与 C++ 之间的错误处理

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++ 代码和带 longjmp 的 Lua

C++ 异常处理(它能正确展开堆栈并调用析构函数)与 Lua 的 longjmp(它只是简单地抛弃堆栈)之间存在不匹配,因此如果 Lua 在 C 中编译,则需要格外小心,以确保调用所有必要的 C++ 析构函数,从而防止内存或资源泄漏。

当 C++ 调用 Lua 作为扩展语言时,Lua 操作通常(但不总是)需要包装在对 lua_CFunctionpcall 中。例如,请参阅 [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(),即 longjmp(如果 Lua 在 C 中编译)。lua_cpcall(不在保护调用之外)是安全的,因为它不会生成 lua_error()(不像使用 lua_pushcfunction 然后是 lua_pcall,这可能会在内存分配失败时 lua_error)。与 C++ 异常处理不同,longjmp 会跳过堆栈上对象的任何析构函数(这通常用于 C++ 中的 RAII)。

另一个问题是,如果 lua_cpcall 返回失败结果,我们该如何处理?我们有可能在原地处理错误,lua_pop 它,然后继续。更常见的情况是,错误需要在调用链中更浅的某个点进行处理。也许更好的解决方案是将错误消息保留在 Lua 堆栈中,确保在捕获块中消耗时进行 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 函数,该函数又调用 C++ 代码,而 C++ 代码又调用 Lua 代码。在这种情况下,C++ 代码可能会 pcall 到 Lua 并将任何错误消息转换为 C++ 异常,然后该异常会传播到 C 函数。C 函数然后需要将 C++ 异常转换为 lua_error(),以 longjmp 到 Lua。只有当调用链中的 C++ 代码以 RAII 方式分配内存时,才需要转换为 C++ 异常。

--DavidManura

引发错误的 Lua C API 函数

有很多 API 函数永远不会抛出 Lua 错误。在 5.1.3 版的参考手册中,引发错误的 API 函数已标识。首先,没有任何堆栈调整函数会抛出错误;这包括 lua_poplua_gettoplua_settoplua_pushvaluelua_insertlua_replacelua_remove。如果您向这些函数提供了错误的索引,或者您没有调用 lua_checkstack,那么您将会得到垃圾数据或段错误,而不是 Lua 错误。

推送原子数据的函数 — lua_pushnumberlua_pushnillua_pushbooleanlua_pushlightuserdata — 永远不会抛出错误。推送复杂对象(字符串、表、闭包、线程、完整用户数据)的 API 函数可能会抛出内存错误。任何类型查询函数 — lua_is*lua_typelua_typename — 都永远不会抛出错误,设置/获取元表和环境的函数也不会。lua_rawgetlua_rawgetilua_rawequal 也永远不会抛出错误。除了 lua_tostring 之外,任何 lua_to* 函数都不会抛出错误,您可以通过首先使用 lua_type 检查对象是否为字符串来避免 lua_tostring 抛出内存不足错误的可能性。lua_rawsetlua_rawseti 可能会抛出内存不足错误。可能抛出任意错误的函数是那些可能调用元方法的函数;这些包括所有非原始的 getset 函数,以及 lua_equallua_lt

如果您正在创建可能需要释放的完整用户数据(full userdata),您应该始终先创建用户数据,并将其各个槽清空,然后附加带有 __gc 元方法的正确元表。然后,您应该创建可能需要释放的对象并将其放入用户数据中。这将避免资源泄漏,因为如果之后抛出错误,__gc 方法最终会被调用。标准 liolib.c 是一个很好的例子,它使用此策略来避免文件描述符泄漏。-- RiciLake

另请参阅


RecentChanges · preferences
编辑 · 历史
最后编辑于 2008 年 10 月 18 日 晚上 10:18 GMT (diff)