2023年11月29日发(作者:)
浅谈Lua和C++异常处理
最近在弄⼀些跟Lua相关的⼩玩意, 在异常处理上遇到了⼀些问题.
Lua是⼀门⼩巧的, ⽤纯C写的语⾔。不过也⽀持按照C++编译。在可以使⽤makefile的环境下,指定CC为g++即可(clang可能会给出
warning,表明正在将后缀的⽂件当作)。在VS下需要【配置 -> C/C++ -> ⾼级 -> 编译为】,然后选编译为C++(或者直接在命
.c.cpp
令⾏中添加/TP)
在C中,异常处理是基于和的。在C++中,异常处理是基于,和关键字实现的。和本质
setjmp(...)longjmp(...)trythrowcatchsetjmplongjmp
上是通过操作栈指针来实现变量回收和控制流跳转的。由于C中所有数据结构都是trivial,C中不存在析构函数这⼀说,从⽽没有问题。但是
C++有析构函数和虚表,这种单纯的控制流跳转不会引起堆栈退解(stack unwinding),因⽽析构函数⽆法正常被执⾏,从⽽可能
longjmp
导致内存泄漏等问题。,如果和分别被替换成和时会引起析构函数的执⾏,那么原的⾏为是undefined
setjmplongjmpcatchthrowlongjmp
的。不过有的编译器会对进⾏魔改,使其能够触发stack unwinding。但那是⾮标准⾏为了。
longjmp
Lua通过,两个宏来实现异常处理. 当Lua以C语⾔形式被编译时,宏被展开为和. 当Lua以C++语⾔
LUAI_TRYLUAI_THROWsetjmplongjmp
形式被编译时,宏展开为和.
atchthrow
当Lua以C编译时
当C++ API希望抛出⼀个Lua异常时(即通过抛出),由于是个,栈上的局部变量没法被正确析构,所以可能需
lua_errorlua_errorlongjmp
要借助⼿动触发堆栈解退。考虑到C++库函数也可能抛出异常,因此可以这么写:
atch
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
从中可以看出,在的块中的A被构造,当运⾏到时,构造了⼀个异常对象,然后析构掉块中的其他变量,随后控制流转
testtrythrowtry
到块,对于带有⽅法的异常,调⽤其⽅法获取异常说明,传⼊并跳转回Lua Kernel。 在跳转之前,异常对象
catchwhat()what()luaL_error
也被正确析构。 因此在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),能够看到的C++版实现:
LUAI_TRY
/* 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 */
没错,确实能够捕捉所有异常,但是Lua并不管捕捉到的异常到底是什么. 假如将函数改写成:
catch(...)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
如果看的实现 (lauxlib.c) ,会发现其本⾝也是构造了⼀个字符串放在了栈顶,然后调⽤.
luaL_errorlua_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);
}
使⽤是没问题的,问题在于我们需要保证⾃⼰的代码不要抛出异常。换句话说,不要让C++异常泄露到Lua VM中。或者说,给
luaL_error
C++函数指定属性。前者⽆⾮就是像前⽂⼀样套上,后者对于C++函数来说… 并不靠谱。
nothrowatch
关于Lua Panic…
C/C++宿主通过等⽅法载⼊⼀段Lua代码放到栈上,并调⽤,或调⽤(其中是个展开
luaL_loadstringlua_calllua_pcalllua_pcallklua_pcall
到的宏)
lua_pcallk
如果通过调⽤,则代码运⾏在下。在保护模式下,即使Lua⼀侧发⽣了异常,也只是将的返回值设为⾮0,并将异常放
lua_pcallklua_pcallk
在栈上。
如果通过调⽤,则代码运⾏在下。此时,如果lua⼀侧发⽣了异常,会将异常传递到与同层的空间中。如果此是
lua_calllua_calllua_call
由或调⽤的C API,那么对应的返回到该调⽤点。如果运⾏在主函数中,或者上层没有异常处理,那么lua会调⽤通
lua_pcallpcalllua_call
过设置的函数。 并在该函数返回之后调⽤结束程序。
lua_atpanicabort
例如如下的代码会导致程序终⽌:
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();
}
}
}


发布评论