浅谈Lua和C++异常处理

最近在弄一些跟Lua相关的小玩意, 在异常处理上遇到了一些问题.

Lua是一门小巧的, 用纯C写的语言。不过也支持按照C++编译。在可以使用makefile的环境下,指定CC为g++即可(clang可能会给出warning,表明正在将.c后缀的文件当作.cpp)。在VS下需要【配置 -> C/C++ -> 高级 -> 编译为】,然后选编译为C++(或者直接在命令行中添加/TP)

在C中,异常处理是基于setjmp(...)longjmp(...)的。在C++中,异常处理是基于trythrowcatch关键字实现的。setjmplongjmp本质上是通过操作栈指针来实现变量回收和控制流跳转的。由于C中所有数据结构都是trivial,C中不存在析构函数这一说,从而没有问题。但是C++有析构函数和虚表,longjmp这种单纯的控制流跳转不会引起堆栈退解(stack unwinding),因而析构函数无法正常被执行,从而可能导致内存泄漏等问题。CppReference指出,如果setjmplongjmp分别被替换成catchthrow时会引起析构函数的执行,那么原longjmp的行为是undefined的。不过有的编译器会对longjmp进行魔改,使其能够触发stack unwinding。但那是非标准行为了。

Lua通过LUAI_TRYLUAI_THROW两个宏来实现异常处理. 当Lua以C语言形式被编译时,宏被展开为setjmplongjmp. 当Lua以C++语言形式被编译时,宏展开为try...catchthrow.

当Lua以C编译时

当C++ API希望抛出一个Lua异常时(即通过lua_error抛出),由于lua_error是个longjmp,栈上的局部变量没法被正确析构,所以可能需要借助try...catch手动触发堆栈解退。考虑到C++库函数也可能抛出异常,因此可以这么写:

class A
{
public:
    A() { cout << "A ctor " << this << endl; }
    ~A() { cout << "A dtor " << this << endl; }
};

class LuaError : public std::exception
{
public:
    LuaError(const std::string& str) : _what(str) {
        cout << "LuaError ctor " << this << endl;
    }
    ~LuaError() { cout << "LuaError dtor " << this << endl; }
    virtual const char* what() const override {
        return _what.c_str();
    }
private:
    std::string _what;
    A x;
};

int test(lua_State* L)
{
    try {
        A a;

        throw LuaError("Error in C API");
    }
    catch (LuaError& e) {
        cout << "Lua Error catched. " << e.what() << endl;
        return luaL_error(L, e.what());
    }
    catch (std::exception& e) {
        cout << "STD exception catched." << e.what() << endl;
        return luaL_error(L, e.what());
    }
    catch (...) {
        cout << "General exception catched." << endl;
        return luaL_error(L, "General Error in C API.");
    }
}

int main()
{
    auto L = luaL_newstate();
    luaL_openlibs(L);
    lua_register(L, "test", test);
    cout << "test: " << test << endl;
    luaL_dostring(L, "a,b=pcall(test) print(a,b)");
    lua_close(L);
    return 0;
}

运行结果是:

test: 002756C7
A ctor 00CFDAEB
A ctor 00CFDA04
LuaError ctor 00CFD9DC
A dtor 00CFDAEB
Lua Error catched. Error in C API
LuaError dtor 00CFD9DC
A dtor 00CFDA04
false   Error in C API

从中可以看出,在testtry块中的A被构造,当运行到throw时,构造了一个异常对象,然后析构掉try块中的其他变量,随后控制流转到catch块,对于带有what()方法的异常,调用其what()方法获取异常说明,传入luaL_error并跳转回Lua Kernel。 在跳转之前,异常对象也被正确析构。 因此在C++ API层中,没有对象被泄露。

其实在Lua users上也有一个类似的写法。 感兴趣的话可以去看一下。

当Lua以C++编译

把Lua当成C++编译或许是更好的方法,但这仅限于你的代码没有引用到其他C模块。大部分Lua的扩展都是C写的,或者至少遵循C ABI。除非扩展开源并且你有心情去在同一编译器下再编译一次,C++的ABI可不是闹着玩的(滑稽)

另外有说法称,将Lua以C++编译会显著增大程序大小,并拖慢运行效率。主要争议点在于C++异常处理非常缓慢。

当lua以c++编译时,事情看起来简单了很多。可以直接throw了,lua也会如愿的截获这个异常。但事情真的这么完美么?

翻一翻Lua源代码 (ldo.c),能够看到LUAI_TRY的C++版实现:

/* C++ exceptions */
#define LUAI_THROW(L,c)		throw(c)
#define LUAI_TRY(L,c,a) \
    try { a } catch(...) { if ((c)->status == 0) (c)->status = -1; }
#define luai_jmpbuf		int  /* dummy variable */

没错,catch(...)确实能够捕捉所有异常,但是Lua并不管捕捉到的异常到底是什么. 假如将test函数改写成:

int test(lua_State* L)
{
    A a;
    throw runtime_error("Here is the exception.");
    return 0;
}

那么运行结果将会变为:

test: 013956DB
A ctor 006FDA47
A dtor 006FDA47
false   function: 013956DB

pcall的第二个返回值变成了一个function?而这个function刚好是test自身?看起来好像很神奇,但其实lua只是将发生异常时栈顶的元素当作异常对象返回了而已. 如果我们在抛出异常前已经向栈中存入一些元素,那么运行结果也会发生改变。

int test(lua_State* L)
{
    A a;
    lua_pushinteger(L, 123);
    throw runtime_error("Here is the exception.");
    return 0;
}
test: 00BE56DB
A ctor 008FDBBB
A dtor 008FDBBB
false   123

如果看luaL_error的实现 (lauxlib.c) ,会发现其本身也是构造了一个字符串放在了栈顶,然后调用lua_error.

/*
** Again, the use of 'lua_pushvfstring' ensures this function does
** not need reserved stack space when called. (At worst, it generates
** an error with "stack overflow" instead of the given message.)
*/
LUALIB_API int luaL_error (lua_State *L, const char *fmt, ...) {
  va_list argp;
  va_start(argp, fmt);
  luaL_where(L, 1);
  lua_pushvfstring(L, fmt, argp);
  va_end(argp);
  lua_concat(L, 2);
  return lua_error(L);
}

使用luaL_error是没问题的,问题在于我们需要保证自己的代码不要抛出异常。换句话说,不要让C++异常泄露到Lua VM中。或者说,给C++函数指定nothrow属性。前者无非就是像前文一样套上try...catch,后者对于C++函数来说… 并不靠谱。

关于Lua Panic…

C/C++宿主通过luaL_loadstring等方法载入一段Lua代码放到栈上,并调用lua_calllua_pcalllua_pcallk调用(其中lua_pcall是个展开到lua_pcallk的宏)

如果通过lua_pcallk调用,则代码运行在保护模式下。在保护模式下,即使Lua一侧发生了异常,也只是将lua_pcallk的返回值设为非0,并将异常放在栈上。

如果通过lua_call调用,则代码运行在非保护模式下。此时,如果lua一侧发生了异常,会将异常传递到与lua_call同层的空间中。如果此lua_call是由lua_pcallpcall调用的C API,那么对应的返回到该调用点。如果lua_call运行在主函数中,或者上层没有异常处理,那么lua会调用通过lua_atpanic设置的函数。 并在该函数返回之后调用abort结束程序。

例如如下的代码会导致程序终止:

int main()
{
    auto L = luaL_newstate();
    luaL_openlibs(L);

    luaL_loadstring(L, "error('just an error')");
    lua_call(L, 0, 0);

    lua_close(L);
}
PANIC: unprotected error in call to Lua API ([string "error('just an error')"]:1: just an error)

在源码luaD_throw中可以更清晰的看到这个流程;

l_noret luaD_throw (lua_State *L, int errcode) {
  if (L->errorJmp) {  /* thread has an error handler? */
    L->errorJmp->status = errcode;  /* set status */
    LUAI_THROW(L, L->errorJmp);  /* jump to it */
  }
  else {  /* thread has no error handler */
    global_State *g = G(L);
    L->status = cast_byte(errcode);  /* mark it as dead */
    if (g->mainthread->errorJmp) {  /* main thread has a handler? */
      setobjs2s(L, g->mainthread->top++, L->top - 1);  /* copy error obj. */
      luaD_throw(g->mainthread, errcode);  /* re-throw in main thread */
    }
    else {  /* no handler at all; abort */
      if (g->panic) {  /* panic function? */
        seterrorobj(L, errcode, L->top);  /* assume EXTRA_STACK */
        if (L->ci->top < L->top)
          L->ci->top = L->top;  /* pushing msg. can break this invariant */
        lua_unlock(L);
        g->panic(L);  /* call panic function (last chance to jump out) */
      }
      abort();
    }
  }
}
### 回答1: LuaC++可以通过Lua C API来实现互相调用。具体步骤如下: 1. 在C++中,使用Lua C API创建一个Lua状态机。 2. 在C++中,将需要在Lua中调用的函数或对象注册到Lua状态机中。 3. 在Lua中,使用require函数加载C++编写的模块。 4. 在Lua中,调用已注册的C++函数或对象。 例如,假设我们有一个C++中的函数add,它可以将两个整数相加。我们将它注册到Lua状态机中,并在Lua中调用它: C++代码: ```cpp int add(lua_State* L) { int a = luaL_checkint(L, 1); int b = luaL_checkint(L, 2); int sum = a + b; lua_pushinteger(L, sum); return 1; } int main() { lua_State* L = luaL_newstate(); luaL_openlibs(L); lua_register(L, "add", add); lua_close(L); return 0; } ``` Lua代码: ```lua require "example" print(add(1, 2)) -- 输出3 ``` 需要注意的是,使用Lua C API需要对LuaC++有一定的了解,同时需要注意内存管理等问题,以避免出现内存泄漏等问题。 ### 回答2: Lua是一种脚本语言,而C是一种编程语言。它们可以互相调用,这使得开发者可以利用各自的优势来实现更强大和高效的应用程序。 Lua与C的互相调用主要是通过提供Lua与C之间的接口,使得它们可以共享数据和函数。在Lua中,可以使用C API(应用程序编程接口)来调用C函数,而在C中,可以使用Lua API来调用Lua函数。 在Lua中调用C函数可以通过使用C API的相关函数来实现。开发者可以在C中编写函数,然后在Lua中通过调用lua_pcall函数来调用这些函数。在C函数中,可以通过lua_push*函数将结果返回给Lua,使得Lua可以进一步处理。 而在C中调用Lua函数可以通过使用Lua API的相关函数来实现。首先,需要创建一个Lua状态机,然后加载和执行Lua脚本。在C中可以使用lua_get*函数获取Lua函数的引用,以便后续调用。使用lua_call函数可以直接调用Lua函数,也可以使用lua_pcall函数来进行错误处理。 通过Lua与C的互相调用,可以发挥Lua的灵活和简洁的脚本特性,同时利用C的强大性能和控制能力。这使得开发者可以在Lua中快速编写脚本逻辑,同时使用C来处理性能敏感的计算和底层操作。通过这种方式,可以有效地提高应用程序的性能和效率。 总而言之,Lua和C是可以互相调用的,通过适当的API使用,可以将两者的优势结合在一起,实现更加灵活高效的应用程序。 ### 回答3: 在Lua和C之间进行互相调用是非常常见且有用的操作。这种能力使得我们可以利用C的性能优势来提高Lua脚本的执行效率,并且可以在C代码中引用Lua的功能和特性。 Lua和C之间的互调可以通过一些接口函数来完成。首先,C代码可以通过嵌入式Lua库来初始化和创建一个Lua状态机。然后,我们可以通过C代码将一些C函数注册到Lua状态机中,使得这些函数可以被Lua脚本中的代码所调用。 另一方面,我们也可以在Lua脚本中调用C函数。首先,我们需要使用`require()`函数来加载和执行C代码中注册的Lua模块。然后,我们可以直接通过模块中定义的函数来调用C函数,并且可以传递参数和获取返回值。 通过这种互相调用机制,Lua和C之间可以实现近乎无缝的交互。对于Lua脚本来说,我们可以利用C函数来完成一些复杂、高性能的任务,例如图形渲染,网络通信等。而对于C代码来说,我们可以利用Lua的易用性和快速开发的特性来编写和调试一些快速迭代的功能。 总而言之,Lua和C之间的互相调用能够充分发挥各自的优势,为我们提供了更强大、灵活和高效的开发和编程环境。无论是在游戏开发、嵌入式系统还是其他领域,这种能力都是非常有价值的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值