Catching Lua Exceptions |
|
Lua Version: 5.0 (VersionNotice)
This article presents a set of patches that facilitate lua exception handling in C and C++.
An exception generated in a Lua script has two possible outcomes:
Lua's C API provides the function lua_pcall() for making protected calls [1].
When a program "throws" an exception it is making a "non-local jump". It is jumping to a point in the code that may not be in the same block, the same function, or even the same module. The Lua manual calls the destination of these non-local jumps "Recover Points".
Because the Lua runtime is written in C it cannot use C++ exceptions, so Lua implements this behavior using the C functions setjmp()
and longjmp()
[2].
Note: As of Lua 5.1, when compiled as C++, Lua uses native C++ exceptions instead of setjmp()
and longjmp()
.
setjmp()/longjmp()
The setjmp()/longjmp()
idiom is very efficient and easy to use. It is used in many C libraries to provide exception handling support. It is not, however, without its problems.
setjmp()
stores the current state of the CPU's execution environment (registers, stack pointers, etc) to a buffer. Calling longjmp()
with that buffer restores the saved execution environment. Essentially, longjmp()
"time travels" back to the setjmp()
call. The program continues execution from that point with its execution environment appearing as if it hasn't changed.
jmp_buf jb; char* mystr = "Original Value"; /* start */ int i_except = setjmp( jb ); if( 0 == i_except ) /* try */ { mystr = "New Value"; /* Throw an exception. Jumps back to "start", and setjmp() returns 33 */ longjmp( jb, 33 ); } else /* catch */ { /* Handle the exception */ } print( mystr );
Many compilers will optimize references to local variables by caching their values in registers. This is fine when code execution is sequential. If the compiler runs out of registers it can store the value to memory and load it later. A call to longjmp()
, however, restores the state of registers saved by setjmp()
. If the compiler has cached a variable's value in a register and its value changes after setjmp()
but before longjmp()
, its new value will be lost when longjmp()
restores its old value.
In the example above the variable mystr
is susceptible to this problem. Depending on how the compiler has optimized the code, when print()
is called mayvar
may contain "New Value", or it may contain "Original Value".
-- Actually, the context of the setjmp invocation in the example above is non-conforming and the resulting behaviour is undefined. The example below is OK in this respect. -- Wim Couwenberg
This problem can be mitigated with careful programming. The solutions are:
setjmp()/longjmp()
.
setjmp()/longjmp()
block should be declared volatile
.
In the example above, declaring mystr
as volatile
will prevent the compiler from caching its value to a register:
char* volatile mystr;
Declare a volatile int
:
volatile int myint;
Note that function arguments must be included in the set of volatile
variables:
int func( int* volatile pInt ) /* pointer declared volatile */ { jmp_buf jb; if( 0 == setjmp( jb ) ) { pInt++; longjmp( jb, 97 ); } else { } }