2023年11月29日发(作者:)

浅谈LuaC++异常处理

最近在弄⼀些跟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

LuaC编译时

当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上也有⼀个。 感兴趣的话可以去看⼀下。

LuaC++编译

把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();

}

}

}