Lua and C++: A Lua Technical Note

Michael Cuddy <mcuddy@fensende.com> Fen's Ende Software

The current implementation of Lua (Lua 4.0) makes extensive use of the setjmp() and longjmp() functions for error handling. This is a very convenient for programmers, allowing constructs like this:


	some_c_function(lua_State *L) 
	{
		const char *str = luaL_check_string(L,1);
		...
	}
This style of programming means that you do not have to check the return value of luaL_check_string() because it will longjmp() out of the function and back to the Lua interpreter if the requested argument is not an LUA_TSTRING object. However, when using C++, setjmp() and longjmp() do not adhere to the C++ stack unwinding semantics, and can cause objects to get lost because destructors will not be called. For example:

	extern "C" some_cpp_function(lua_State *L)
	{
		CppObjectWhichNeedsDtor x;
		
		x.name = luaL_check_string(L,1);
	}

In the code example above (which is, admittedly, contrived) if luaL_check_string() discovers that the first Lua argument to the function is not a LUA_TSTRING object, it will longjmp() back to the Lua interpreter, and completely miss the destructor for the CppObjectWhichNeedsDtor object.

My change modifies the setjmp()/longjmp() function calls into C++ throw ... catch. This is made trivial by the fact that Lua's code is very modular, and setjmp()/longjmp() are only referenced by one function (each). The only cog-in-the-works is that Lua is usually compiled with a C compiler, not a C++ compiler.

In my source base, I have made the change conditional on a preprocessor macro THROW_INSTEAD_OF_LONGJMP, and have tried to disturb the source base as little as possible.

The major portion of my change involves a new source file lua/src/ldoxx.cpp which moves the functions luaD_breakrun() and luaD_runprotected() into a to C++ module and changes the setjmp() in luaD_runprotected() into a try .. catch block and the longjmp() in luaD_breakrun() into a C++ throw. The ldoxx.cpp file can be down-loaded here.


/*
** $Id:$
** C++ version of luaD_protected_run and luaD_break_run.
** Written by Michael Cuddy (mcuddy@fensende.com), based on functions
** in ldo.c
**
** All rights to derived work ceded to original copyright holder.
**
** See Copyright Notice in lua.h
*/
#ifdef THROW_INSTEAD_OF_LONGJMP

#ifndef __cplusplus
#error "try compiling ldoxx.cpp with a C++ compiler"
#endif /* __cplusplus */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

extern "C" {
#include "lua.h"
#include "lvm.h"
};

class lua_exception {
	int status;
public:
	lua_exception(int status_) : status(status_) { };
	int get_status() { return status; }
};

// XXX -- hack, this function shouldn't be here, it's duplicated from
// ldo.c, where it's declared static.
static void restore_stack_limit (lua_State *L) {
  if (L->top - L->stack < L->stacksize - 1)
    L->stack_last = L->stack + (L->stacksize-1);
}

extern "C" void luaD_breakrun (lua_State *L, int errcode) {
  throw lua_exception(errcode);
}

extern "C" int luaD_runprotected (lua_State *L, void (*f)(lua_State *, void *), void *ud) {
  StkId oldCbase = L->Cbase;
  StkId oldtop = L->top;
  int allowhooks = L->allowhooks;
  int status = 0;
  try {
    (*f)(L, ud);
  } catch (lua_exception &e) {  /* an error occurred: restore the state */
    status = e.get_status();
    L->allowhooks = allowhooks;
    L->Cbase = oldCbase;
    L->top = oldtop;
    restore_stack_limit(L);
  }
  return status;
}

/* }====================================================== */

#endif /* THROW_INSTEAD_OF_LONGJMP */

In addition to the code change above, a few small changes were made to lstate.c, lstate.h and ldo.cpp:

In lstate.c, the initialization of L->errorJmp in lua_open() is #ifdef'ed out with THROW_INSTEAD_OF_LONGJMP:



88    L->allowhooks = 1;
89  #ifndef THROW_INSTEAD_OF_LONGJMP
90    L->errorJmp = NULL;
91  #endif  /* !THROW_INSTEAD_OF_LONGJMP */

In lstate.h, the forward declaration of the lua_longjmp structure and the structure member errorJmp is #ifdef'ed out of the lua_State structure:

33
34 #ifndef THROW_INSTEAD_OF_LONGJMP
35 struct lua_longjmp;  /* defined in ldo.c */
36 #endif  /* !THROW_INSTEAD_OF_LONGJMP */
37 struct TM;  /* defined in ltm.h */

...

54 StkId Cbase;  /* base for current C function */
55 #ifndef THROW_INSTEAD_OF_LONGJMP
56 struct lua_longjmp *errorJmp;  /* current error recover point */
57 #endif  /* !THROW_INSTEAD_OF_LONGJMP */
58 char *Mbuffer;  /* global buffer */


In ldo.c, the functions moved to ldoxx.cpp are also #ifdef'ed out:


57 #ifndef THROW_INSTEAD_OF_LONGJMP
58 /* mcuddy - if this function is changed, change it in ldoxx.cpp, too. */
59 static void restore_stack_limit (lua_State *L) {
60   if (L->top - L->stack < L->stacksize - 1 )
61     L->stack_last = L->stack + (L->stacksize-1);
62 }
63 #endif /* !THROW_INSTEAD_OF_LONGJMP */

...

320 /*
321 ** {======================================================
322 ** Error-recover functions (based on long jumps)
323 ** =======================================================
324 */
325 
326 #ifdef THROW_INSTEAD_OF_LONGJMP
327 /*
328 ** in the case where we're using throw() for C++ instead of longjmp
329 ** (which keeps C++ objects from being destroyed!) the functions
330 ** luaD_breakrun() and luaD_runprotected(), are defined in ldoxx.cpp
331 ** Also, the lua_longjmp structure is not needed.
332 */
333 #else
334 /* chain list of long jump buffers */
335 struct lua_longjmp {
336   jmp_buf b;
337   struct lua_longjmp *previous;
338   volatile int status;  /* error code */
339 };
340 #endif /* THROW_INSTEAD_OF_LONGJMP */

...

359 }
360 
361 #ifndef THROW_INSTEAD_OF_LONGJMP
362 void luaD_breakrun (lua_State *L, int errcode) {
363   if (L->errorJmp) {

...

391   L->errorJmp = lj.previous;  /* restore old error handler */
392   return lj.status;
393 }
394 #endif /* THROW_INSTEAD_OF_LONGJMP */

And that's it! When compiled with THROW_INSTEAD_OF_LONGJMP, Lua will now maintain the C++ guarantee of stack unwinding.

A future direction to take this would be to make the lua_exception public (and derive it from std::exception which would allow user code to catch the exception for extended error handling. This trivial, and is left as an excercise to the reader.