LuaとC++異常処理について

25385 ワード

最近Luaに関する小物を作っていて、異常処理に問題があった.
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...catchthrowとなる.
LuaがCでコンパイルされると
C++APIがLua異常を放出することを望む場合(すなわち、lua_errorによって放出される)、lua_errorlongjmpであるため、スタック上の局所変数は正しく解析できないため、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の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_pcalllua_pcallkに展開されたマクロです).lua_pcallkで呼び出された場合、コードは保護モードで実行されます.保護モードでは、Lua側に異常が発生しても、lua_pcallkの戻り値を0以外としてスタック上に異常を置く.lua_callで呼び出された場合、コードは非保護モードで実行されます.このときlua側に異常が発生すると,lua_callと同層の空間に異常が伝達される.このlua_calllua_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();
    }
  }
}