LuaとC++異常処理について
25385 ワード
最近Luaに関する小物を作っていて、異常処理に問題があった.
Luaはコンパクトで、純粋なCで書かれた言語です.ただし、C++によるコンパイルもサポートされています.makefileが使用可能な環境では、CCをg++として指定します(clangではwarningが与えられ、
Cでは、異常処理は、
Luaは
LuaがCでコンパイルされると
C++APIがLua異常を放出することを望む場合(すなわち、
実行結果:
ここから分かるように、
実はLua usersにも似たような書き方があります.興味があれば行ってみてもいいです.
LuaがC++でコンパイルされると
LuaをC++としてコンパイルするのはもっと良い方法かもしれませんが、これはあなたのコードが他のCモジュールに参照されていないことに限られます.Luaの拡張の大部分はCで書かれているか、少なくともC ABIに従っている.オープンソースを拡張して同じコンパイラの下でもう一度コンパイルする気持ちがない限り、C++のABIは騒いで遊んでいるわけではありません(滑稽)
また,LuaをC++でコンパイルするとプログラムサイズが著しく増大し,実行効率が遅くなるという説がある.主な争点はC++異常処理が非常に遅いことである.
luaがc++でコンパイルされると、物事はずっと簡単に見えます.直接throwすることができて、luaも望み通りにこの異常を捕まえることができます.しかし、事は本当にこんなに完璧ですか?
Luaソースコード(ldo.c)をひっくり返すと、
そう、
実行結果は次のようになります.
pcallの2番目の戻り値はfunctionになりましたか?このfunctionはちょうどtest自身ですか?不思議そうに見えますが、実はluaは異常が発生したときのスタックトップの要素を異常オブジェクトとして返しただけです.異常を投げ出す前にスタックに要素を格納した場合、実行結果も変更されます.
Lua Panicについて...
C/C++ホストは
たとえば、次のコードによってプログラムが終了します.
この流れは、ソース
Luaはコンパクトで、純粋なCで書かれた言語です.ただし、C++によるコンパイルもサポートされています.makefileが使用可能な環境では、CCをg++として指定します(clangではwarningが与えられ、
.c
接尾辞のファイルが.cpp
として扱われていることを示します).VSでは、「構成->C/C+->アドバンスド->コンパイル」が必要で、C++(またはコマンドラインに直接/TPを追加する)にコンパイルします.Cでは、異常処理は、
setjmp(...)
およびlongjmp(...)
に基づく.C++において、例外処理は、try
,throw
およびcatch
キーワードに基づいて実現される.setjmp
およびlongjmp
は本質的にスタックポインタを操作することによって変数回収および制御フローホッピングを実現する.Cのすべてのデータ構造がtrivialであるため,Cには構造関数という説は存在せず,問題はない.ただし、C++には構造関数とダミーテーブルがあり、longjmp
という単純な制御フロージャンプではスタックバックホール(stack unwinding)は発生しないため、構造関数が正常に実行されず、メモリ漏れなどの問題を引き起こす可能性がある.CppReferenceは、setjmp
およびlongjmp
がそれぞれcatch
およびthrow
に置き換えられた場合、構造関数の実行を引き起こす場合、元のlongjmp
の動作はundefinedであると指摘している.しかし、一部のコンパイラはlongjmp
を魔改し、stack unwindingをトリガーすることができる.しかし、それは非標準的な行為です.Luaは
LUAI_TRY
,LUAI_THROW
の2つのマクロにより異常処理を実現する.LuaがC言語でコンパイル場合、マクロはsetjmp
およびlongjmp
に展開する.LuaがC++言語でコンパイル場合、マクロはtry...catch
とthrow
となる.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
ここから分かるように、
test
のtry
ブロックの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の2番目の戻り値は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++異常をLuaVMに漏らさないようにする.あるいは、C++関数にnothrow
属性を指定します.前者は前文のようにtry...catch
をつけるにほかならないが、後者はC++関数にとって...頼りにならない.Lua Panicについて...
C/C++ホストは
luaL_loadstring
などの方法でLuaコードをロードしてスタックに配置し、lua_call
を呼び出し、lua_pcall
またはlua_pcallk
を呼び出します(ここでlua_pcall
はlua_pcallk
に展開されたマクロです).lua_pcallk
で呼び出された場合、コードは保護モードで実行されます.保護モードでは、Lua側に異常が発生しても、lua_pcallk
の戻り値を0以外としてスタック上に異常を置く.lua_call
で呼び出された場合、コードは非保護モードで実行されます.このときlua側に異常が発生すると,lua_call
と同層の空間に異常が伝達される.このlua_call
がlua_pcall
またはpcall
によって呼び出された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();
}
}
}