Lua And Exceptions Hacking Notes

lua-users home
wiki

This is revived from a hacking session back in 2005, to be master-edited. -- ThomasLefort

Current algorithm

As of version 5.1 built as C++.

Data structures

The "long jump buffer" is a linked list of volatile status.
struct lua_longjmp {
  struct lua_longjmp *previous;
  int dummy;
  volatile int status;  
};
Its role is to keep track of the protected call stack. We start with an empty global buffer.

Catching errors

// chain a new local buffer, status = 0
// global buffer now points to it
try {
  // call
}
catch (...) {
  // force local status to non 0 (e.g. -1)
}
// restore global buffer
// return local status

Throwing errors

// if empty global buffer then panic
// store status in global buffer
// throw its address !

Discussion

The current algorithm makes it possible to throw exceptions from the other side of Lua, but they will never make it to the other end, for a good reason.

If indeed they weren't caught at Lua level at all, catching them later would unwind violently out of Lua's call stack, leaving us with an inconsistent Lua state. This is what happened when I tweaked Lua 5.0.2 that way.

To ensure a consistent Lua state while still carrying the exception to the other side, we would need to store a copy of it in a safe place, unwind normally to toplevel, and finally throw the copy. However, because catch matches types (Stroustrup 14.3), we lose type information when we `catch (...)`, and with that the ability to copy generic exceptions.

A partial solution

To implement the copy mechanism discussed above, we could narrow the type range of alien exceptions to the standard `std::exception`. If the programmer needs to catch other exceptions, he can wrap them into `std::exception` subclasses. The remaining exceptions would still have to be silently ignored.

This solution has the advantage of simplicity, and works well with hierarchical exception classes (one wrapper per root class). It is however limited to situations where both ends can agree on the `std::exception` wrapping.

NOTE: this means wrapper generators need to be adapted to this scheme.

As a temporary solution, one may also narrow the exception space to a set of custom exceptions, that is, manually at Lua build time.

TODO: check copy of `std::exception`

Alternative: extending tolua++

The above solution is not really proper. If we use a free software binding generator such as [tolua++], we can extend it so as to protect Lua from exceptions thrown by the bound C++ code. This has the advantage of leaving the Lua source base uncluttered.

Looking back, it appears as an appropriate solution in our very case, and it should also be enough for casual applications. However it entails translation between error systems and thus redundancies.

If the Lua source became (or forked) exception-safe, we could enable it to catch C++ exceptions from Lua, either explicitely or through a binding to an eventual Lua exception scheme. A solution is sketched in the next section.

A complete solution

Outline

catch (struct lua_longjump * p_lj) {
  // force local status to non 0
}
catch (...) {
  // do soft unwinding
  throw
}

NOTE: `p_lj` should be pointing to the current `lj`.

TODO: assert that.

This should guarantee that we don't eat alien exceptions, unless someone twisted decides to throw `(struct lua_longjump)*` all over the place from her library.

Soft unwinding

To devise our algorithm, we need to identify the vital resources that are acquired and released by `pcall`.

Outline of the pcall stack

The top and bottom levels wrap when pcalls are nested.

The D_pcall cleanup

if (status != 0) {  /* an error occurred? */
  StkId oldtop = restorestack(L, old_top);
  luaF_close(L, oldtop);  /* close eventual pending closures */
  luaD_seterrorobj(L, status, oldtop);
  L->nCcalls = oldnCcalls;
  L->ci = restoreci(L, old_ci);
  L->base = L->ci->base;
  L->savedpc = L->ci->savedpc;
  L->allowhook = old_allowhooks;
  restore_stack_limit(L);
}
L->errfunc = old_errfunc;

It restores the state previously acquired:

// ptrdiff_t old_top, ptrdiff_t ef
unsigned short oldnCcalls = L->nCcalls;
ptrdiff_t old_ci = saveci(L, L->ci);
lu_byte old_allowhooks = L->allowhook;
ptrdiff_t old_errfunc = L->errfunc;
L->errfunc = ef;

Now, we could use the Resource Acquisition Is Initialization pattern to perform cleanup code automatically at object destruction. It is the natural way to integrate with exceptions (Stroustrup 14.4.1).

Coding roadmap

Final notes

Stroustrup 14.9 is a fundamental.


RecentChanges · preferences
edit · history
Last edited October 27, 2007 12:48 am GMT (diff)