Error Handling Between Lua And Cplusplus |
|
[ Note: this page is somewhat of a work in progress. Additional comments are welcome. ]
If you compile Lua in C, you must surround your #include <lua.h>
in an extern "C"
, or use the #include <lua.hpp>
equivalent, else you will get linker errors.
// alternately do #include <lua.hpp> extern "C" { #include <lua.h> }
Lua itself is normally compiled under C but may alternately be compiled under C++. If compiled in C, Lua uses [longjmp]'s to implement error handling (lua_error
). If compiled in C++, Lua by default uses C++ exceptions. See the declaration of LUAI_THROW
in luaconf.h
. See also LuaList:2007-10/msg00473.html .
There is a mismatch between C++ exception handling that properly unwinds the stack and call destructors v.s Lua longjmp
s that merely toss the stack, so more care is needed if Lua is compiled in C to ensure all necessary C++ destructors are called, preventing memory or resource leaks.
When C++ calls Lua as an extension language, the Lua operations often (but not always) need to be wrapped in a pcall
to a lua_CFunction
. For example, see [PIL 25.2] or PIL2 25.3. (Details on these conditions are given later below by Rici.) It's often the case that this lua_CFunction
is used by only one caller. Therefore, it can be useful to make the lua_CFunction local to the calling function (like a closure). In C++, the lua_CFunction can be defined inside a struct like this:
int operate(lua_State * L, std::string & s, int x, int y) { std::string msg = "Calling " + s + "\n"; // can raise exception; must be destroyed cout << msg; // caution: this code by raise exceptions but not longjump. struct C { static int call(lua_State * L) { // caution: this code may longjump but not raise exceptions. C * p = static_cast<C*>(lua_touserdata(L, 1)); assert(lua_checkstack(L, 4)); lua_getglobal("add"); // can longjump assert(lua_isfunction(L, -1)); lua_pushstring(L, s); // can longjump lua_pushnumber(L, p->x); lua_pushnumber(L, p->y); lua_call(L, 3, 1); // can longjump p->z = lua_tonumber(L, -1); assert(lua_isnumber(L, -1)); return 0; } const char * s; int x; int y; int z; } p = {s.c_str(), x, y, 0}; int res = lua_cpcall(L, C::call, &p); // never longjumps if (res != 0) { handle_error(L); // do something with the error; can raise exception //note: we let handle_error do lua_pop(L, 1); } return p.z; }
Now, error handling is a bit tricky at first glance. The lua_getglobal
, lua_pushstring
, and lua_call
calls can generate a lua_error()
, i.e. a longjmp
if Lua is compiled in C. The lua_cpcall
, which is outside the protected call, is safe because it does not generate a lua_error()
(unlike using a lua_pushcfunction
followed by a lua_pcall
, which could lua_error
on memory allocation failure). Unlike C++ exception handling, the longjmp
will skip any destructors of objects up the stack (often used for RAII in C++).
Another issue is if lua_cpcall
returns a failure result, what do we do with it? There is a possibility we could handle the error in-place, lua_pop
it, and continue. More often, the error needs to be dealt with at a more shallow point in the call chain. Possibly a better solution is to keep the error message in the Lua stack, making sure to do a lua_pop
if consumed in a catch block:
#include <stdexcept> #include <boost/shared_ptr.hpp> /** * C++ exception class wrapper for Lua error. * This can be used to convert the result of a lua_pcall or * similar protected Lua C function into a C++ exception. * These Lua C functions place the error on the Lua stack. * The LuaError class maintains the error on the Lua stack until * all copies of the exception are destroyed (after the exception is * caught), at which time the Lua error object is popped from the * Lua stack. * We assume the Lua stack is identical at destruction as * it was at construction. */ class LuaError : public std::exception { private: lua_State * m_L; // resource for error object on Lua stack (is to be popped // when no longer used) boost::shared_ptr<lua_State> m_lua_resource; LuaError & operator=(const LuaError & other); // prevent public: // Construct using top-most element on Lua stack as error. LuaError(lua_State * L); LuaError(const LuaError & other); ~LuaError(); virtual const char * what() const throw(); }; static void LuaError_lua_resource_delete(lua_State * L) { lua_pop(L, 1); } LuaError::LuaError(lua_State * L) : m_L(L), m_lua_resource(L, LuaError_lua_resource_delete) { } LuaError::LuaError(const LuaError & other) : m_L(other.m_L), m_lua_resource(other.m_lua_resource) { } const char * LuaError::what() const throw() { const char * s = lua_tostring(m_L, -1); if (s == NULL) s = "unrecognized Lua error"; return s; } LuaError::~LuaError() { }
Example usage:
for(int n=1; n < 100; n++) { try { string s = "123123123123123123"; // note: may throw bad_alloc // ... int res = lua_cpcall(L, call, NULL); if (res != 0) throw LuaError(L); } catch(exception & e) { cout << e.what() << endl; } }
There is also the case if Lua calls a C function that call C++ code that calls Lua code. In such case, the C++ code might pcall into Lua and convert any error message to a C++ exception, which propogates up to the C function. The C function then needs to convert the C++ exception to a lua_error()
which longjmp
s to Lua. This conversion to a C++ exception is only needed if the C++ code in the call chain allocated memory in the RAII fashion.
lua_pop
, lua_gettop
, lua_settop
, lua_pushvalue
, lua_insert
, lua_replace
and lua_remove
. If you provide incorrect indexes to these functions, or you haven't called lua_checkstack
, then you're either going to get garbage or a segfault, but not a Lua error.
lua_pushnumber
, lua_pushnil
, lua_pushboolean
and lua_pushlightuserdata
ever throw an error. API functions which push complex objects (strings, tables, closures, threads, full userdata) may throw a memory error. None of the type enquiry functions -- lua_is*
, lua_type
and lua_typename
-- will ever throw an error, and neither will the functions which set/get metatables and environments. lua_rawget
, lua_rawgeti
and lua_rawequal
will also never throw an error. Aside from lua_tostring
, none of the lua_to*
functions will throw an error, and you can avoid the possibility of lua_tostring
throwing an out of memory error by first checking that the object is a string, using lua_type
. lua_rawset
and lua_rawseti
may throw an out of memory error. The functions which may throw arbitrary errors are the ones which may call metamethods; these include all of the non-raw get
and set
functions, as well as lua_equal
and lua_lt
.
__gc
metamethod. Then you should create the object which may need to be freed and put it in the userdata. This will avoid resource leaks because the __gc
method will eventually be called if an error is subsequently thrown. A good example of this is the standard liolib.c
which uses this strategy to avoid leaking file descriptors. -- RiciLake