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 longjmps 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 longjmps 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